﻿$engine JScript
$uname Refactoring
$dname Рефакторинг
$addin global
$addin stdlib
$addin vbs

////////////////////////////////////////////////////////////////////////////////////////
////{ Cкрипт "Рефакторинг" (refactoring.js) для проекта "Снегопат"
////
//// Описание: Реализует простейшие инструменты рефакторинга.
//// Автор: Александр Кунташов <kuntashov@gmail.com>, http://compaud.ru/blog
////}
////////////////////////////////////////////////////////////////////////////////////////

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

stdlib.require('SyntaxAnalysis.js', SelfScript);
//Для отладки: stdlib.require(profileRoot.getValue("Snegopat/MainFolder") + 'user\\Libs\\SyntaxAnalysis.js', SelfScript); 

global.connectGlobals(SelfScript);

////////////////////////////////////////////////////////////////////////////////////////
////{ Макросы
////

SelfScript.self['macrosВыделить метод (extract method)'] = function () {
    refactor(ExtractMethodRefactoring);
}

SelfScript.self['macrosПоказать список процедур и функций модуля'] = function () {
    var tw = GetTextWindow();
    if (!tw) return;
    var module = SyntaxAnalysis.AnalyseTextDocument(tw);
    var methList = new MethodListForm(module);
    if (methList.selectMethod())
        Message(methList.SelectedMethod.Name);
}

SelfScript.self['macrosСоздать заглушку для несуществующего метода'] = function () {
    refactor(CreateMethodStubRefactoring, true);
}

////} Макросы

function refactor(refactorerClass, withoutSelection) {
    
    var tw = GetTextWindow();
    if (!tw) return;
    
    var selText = tw.GetSelectedText();
    if (!selText && !withoutSelection) 
    {
        Message("Не выделен текст, к которому применяется рефакторинг!");
        return;
    }
    
    var module = SyntaxAnalysis.AnalyseTextDocument(tw);
    var refactorer = new refactorerClass(module);
    refactorer.refactor(selText);
}

////////////////////////////////////////////////////////////////////////////////////////
////{ MethodListForm
////

function MethodListForm(module) {

    this.module = module;
    this.originalMethodList = module.getMethodsTable();

    this.form = loadScriptForm(SelfScript.fullPath.replace(/\.js$/, '.methodList.ssf'), this);
    this.SelectedMethod = undefined;
    
    this.settings = SettingsManagement.CreateManager(SelfScript.uniqueName + "/MethodListForm", {
        'DoNotFilter': false, 'SortByName' : false
    });
    
    this.settings.LoadSettings();
        
    var methListForm = this;
    this.tcWatcher = new TextChangesWatcher(this.form.Controls.SearchText, 3, function(t){methListForm.fillMethodList(t)});
    
    this.icons = {
        'Proc': this.form.Controls.picProc.Picture,
        'Func': this.form.Controls.picFunc.Picture
    }
    
    this.fillMethodList();
}

MethodListForm.prototype.selectMethod = function () {
    this.SelectedMethod = this.form.DoModal();
    return this.SelectedMethod ? true : false;
}

MethodListForm.prototype.MethodListSelection = function (Control, SelectedRow, Column, DefaultHandler) {
    this.form.Close(SelectedRow.val._method);
}

MethodListForm.prototype.MethodListOnRowOutput = function (Control, RowAppearance, RowData) {
    var nameCell = RowAppearance.val.Cells.Name;
    nameCell.SetPicture( RowData.val.IsProc ? this.icons.Proc : this.icons.Func);
}

MethodListForm.prototype.CmdBarSortByName = function (button) {
    button.val.Check = !button.val.Check;
    this.form.SortByName = button.val.Check;
    this.sortMethodList(button.val.Check);
}

MethodListForm.prototype.CmdBarDoNotFilter = function (button) {
    button.val.Check = !button.val.Check;
    this.form.DoNotFilter = button.val.Check;
    this.fillMethodList(this.form.SearchText);
}

MethodListForm.prototype.CmdBarMainОК = function (Кнопка) {
    var SelectedRow = this.form.Controls.MethodList.CurrentRow;
    if (SelectedRow)
        this.form.Close(SelectedRow._method);
    else
        this.form.CurrentControl = this.form.Controls.SearchText;
}

MethodListForm.prototype.OnOpen = function () {

    this.settings.ApplyToForm(this.form);

    this.form.Controls.CmdBar.Buttons.SortByName.Check = this.form.SortByName;
    this.form.Controls.CmdBar.Buttons.DoNotFilter.Check = this.form.DoNotFilter;
    
    this.loadedOnOpen = true;
    this.tcWatcher.start();
}

MethodListForm.prototype.BeforeClose = function (Cancel, StandardHandler) {
    this.tcWatcher.stop();
    this.saveSettings();
}

MethodListForm.prototype.fillMethodList = function (newText) {

    if (!newText || newText.match(/^\s*$/))
    {
        if (this.loadedOnOpen)
            this.loadedOnOpen = false;
        else
            this.form.Controls.MethodList.Value = this.originalMethodList.Copy();            
    }
    else 
    {
        var a = newText.split(/\s+/);
        for (var i=0; i<a.length; i++)
            a[i] = StringUtils.addSlashes(a[i]);
            
        var re = new RegExp(a.join(".*?"), 'i');    
        
        if (this.form.DoNotFilter)
        {
            var currentRow = undefined;
            
            var methList = this.originalMethodList.Copy();
            for (var rowNo = 0; rowNo < methList.Count(); rowNo++)
            {
                var row = methList.Get(rowNo);
                if (re.test(row.Name))
                {
                    currentRow = row;
                    break;
                }
            }
            
            this.form.Controls.MethodList.Value = methList;
            if (currentRow)
                this.form.Controls.MethodList.CurrentRow = currentRow;
        }
        else
        {
            var methList = this.form.Controls.MethodList.Value;
            methList.Clear();    
            for (var rowNo = 0; rowNo < this.originalMethodList.Count(); rowNo++)
            {
                var row = this.originalMethodList.Get(rowNo);
                if (re.test(row.Name))
                    FillPropertyValues(methList.Add(), row);
            }
        }
    }
    
    this.sortMethodList(this.form.SortByName);
}

MethodListForm.prototype.sortMethodList = function (sortByName) {
    this.form.MethodList.Sort(sortByName ? 'Name' : 'StartLine');
}

MethodListForm.prototype.saveSettings = function () {
    this.settings.ReadFromForm(this.form);
    this.settings.SaveSettings();
}

////} MethodListForm

////////////////////////////////////////////////////////////////////////////////////////
////{ CreateMethodStubRefactoring
////

function CreateMethodStubRefactoring(module) {

    this.module = module;
    this.textWindow = this.module.textWindow;
}

CreateMethodStubRefactoring.prototype.refactor = function (selectedText) {
    
    var methodName, methodSignature, matches;
    
    methodName = this.textWindow.GetWordUnderCursor();
    if (!methodName)
        return;
    
    var method_call_proc = new RegExp("(?:;\\s*|^\\s*)" + methodName + '(\\(.+?\\))');
    var method_call_func = new RegExp(methodName + "(\\(.*?\\))");
    
    var line = this.textWindow.GetLine(this.textWindow.GetCaretPos().beginRow);
    
    var matches = line.match(method_call_proc);
    var isProc = (matches != null);
    
    if (!isProc)
    {
        matches = line.match(method_call_func);
        if (!matches)
            return;
    }
    
    methodSignature = methodName + matches[1];
    
    var procTemplate = "\n"  
    + "Процедура ИмяМетода()\n"
    + "\t//TODO: Добавьте исходный код процедуры.\n" 
    + "КонецПроцедуры\n";

    var funcTemplate = "\n" 
    + "Функция ИмяМетода()\n"
    + "\t//TODO: Добавьте исходный код функции.\n" 
    + "\tВозврат Неопределено;\n"
    + "КонецФункции\n";
    
    var stubCode = isProc ? procTemplate : funcTemplate;
    stubCode = stubCode.replace('ИмяМетода()', methodSignature);
    
    var methodList = new MethodListForm(this.module);
    if (methodList.selectMethod())
    {
        var insertLineIndex = methodList.SelectedMethod.EndLine + 1;
        this.textWindow.InsertLine(insertLineIndex + 1, stubCode);
        this.textWindow.SetCaretPos(insertLineIndex + 3, 1);
    }
}

////} CreateMethodRefactoring

////////////////////////////////////////////////////////////////////////////////////////
////{ ExtractMethodRefactoring
////

function ExtractMethodRefactoring(module) {
    this.module = module;
    this.form = loadScriptForm(SelfScript.fullPath.replace(/\.js$/, '.extractMethod.ssf'), this);    
    this.Params = this.form.Params;
    this.ReturnValue = this.form.ReturnValue;
    this.SignaturePreview = this.form.SignaturePreview;
}

ExtractMethodRefactoring.prototype.getVarRe = function (varName) {
    return new RegExp("([^\\w\\dА-я\.]|^)" + varName + "([^\\w\\dА-я]|$)", 'i');
}

ExtractMethodRefactoring.prototype.refactor = function (selectedText) {

    var sel = this.module.textWindow.GetSelection();

    // 0. Определить переменные внутри выделенного блока кода (распарсить его).
    var extContext = this.getCodeContext(selectedText);
    var extVars = extContext.AutomaticVars;

    // 1. Определить локальные переменные части кода метода выше выделяемого кода.
    // 2. Определить параметры метода, из которого выделяется код.
    var curMethod = this.module.getActiveLineMethod();
    
    var codeBefore = this.module.textWindow.Range(curMethod.StartLine, 1, sel.beginRow-1).GetText();
    var contextBefore = this.getCodeContext(codeBefore);

    // 3. Определить, какие 1+2 инициализируются в 0 (AutomaticVars), а какие используются 
    this.fillParams(contextBefore.AutomaticVars, extVars, selectedText);    
    this.fillParams(curMethod.Params, extVars, selectedText);    
        
    // 4. Те переменные, которые используются в остальной части кода - возвращаемые значения.
    var codeAfter = this.module.textWindow.Range(sel.endRow + 1, 1, curMethod.EndLine).GetText();
    var contextAfter = this.getCodeContext(codeAfter);
    
    this.fillReturnValues(contextAfter.AutomaticVars, extVars, codeAfter);
        
    if (this.form.DoModal())
        this.extractMethod(selectedText);
}

ExtractMethodRefactoring.prototype.fillParams = function (extArgs, extVars, source) {
    for (var i=0; i<extArgs.length; i++)
    {
        var varName = extArgs[i];
        var re = this.getVarRe(varName);
        if (re.test(source) && extVars.indexOf(varName) == -1)
            this.addParam(varName, true, false);
    }
}

ExtractMethodRefactoring.prototype.fillReturnValues = function (extArgs, extVars, source) {
    //debugger;
    for (var i=0; i<extVars.length; i++)
    {
        var varName = extVars[i];
        var re = this.getVarRe(varName);
        if (re.test(source) && extArgs.indexOf(varName) == -1)
            this.addReturnValue(varName);
    }
}

ExtractMethodRefactoring.prototype.getCodeContext = function (code) {
    var extractedCode = "Процедура ВыделенныйМетод()\n" + code + "\nКонецПроцедуры";
    var extractedContext = SyntaxAnalysis.AnalyseModule(extractedCode, false);
    return extractedContext.getMethodByName("ВыделенныйМетод");
}

ExtractMethodRefactoring.prototype.addParam = function (paramName, isParam, isVal) {
    if (!this.Params.Find(paramName, 'Name'))
    {
        var paramRow = this.Params.Add();
        paramRow.Name = paramName;
        paramRow.isParam = isParam ? true : false;
        paramRow.isVal = isVal ? true : false;
    }
}

ExtractMethodRefactoring.prototype.addReturnValue = function (varName) {
    if (!this.ReturnValue.Find(varName, 'Name'))
    {
        var row = this.ReturnValue.Add();
        row.Name = varName;
    }
}

ExtractMethodRefactoring.prototype.BtOKClick = function (Control) {

    if (!this.form.Name.match(/^[_\wА-я](?:[_\w\dА-я]*)$/))
    {
        DoMessageBox("Имя метода должно быть правильным идентификатором!");
        return;
    }    
    
    this.form.Close(true);
}

ExtractMethodRefactoring.prototype.BtCancelClick = function (Control) {
    this.form.Close(false);
}

ExtractMethodRefactoring.prototype.extractMethod = function(source) {

    var tw = this.module.textWindow;
    var sel = tw.GetSelection();

    var params = new Array;
    for (var i=0; i<this.Params.Count(); i++)
    {
        var paramRow = this.Params.Get(i);
        if (paramRow.IsParam)
            params.push((paramRow.IsVal ? 'Знач ' : '') + paramRow.Name);
    }
    
    // Откорректируем отступ.
    var srcIndent = StringUtils.getIndent(source);
    source = StringUtils.shiftLeft(source, srcIndent);
    source = StringUtils.shiftRight(source, "\t");
    
    // Сформируем исходный код определения выделенного метода.
    var newMethod = this.form.IsProc ? 'Процедура' : 'Функция';
    newMethod += ' ' + this.form.Name + '(' + params.join(', ') + ')';
    if (this.form.Exported)
        newMethod += " Экспорт";   
        
    newMethod += "\n\n" + this.prepareSource(source) + "\n\n";  
    
    if (this.form.IsProc) 
    {
        newMethod += 'КонецПроцедуры';        
        
    }
    else
    {
        var retVal = "Неопределено";
        if (this.ReturnValue.Count() > 0) {
            retVal = this.ReturnValue.Get(0).Name;
        }
        newMethod += "\tВозврат " + retVal + ";";    
        newMethod += "\n\n" + 'КонецФункции';    
    }

    // Получим метод, внутри которого мы находимся.
    var curMethod = this.module.getActiveLineMethod();
    
    // Добавим в модуль определение выделенного метода.
    tw.InsertLine(curMethod.EndLine + 2, "\n" + newMethod);
        
    // Заменим выделенный код на вызов нового метода.
    var methCall = this.form.Name + '(' + params.join(', ') + ");\n";
    
    if (!this.form.IsProc && this.ReturnValue.Count() > 0) {
        retVal = this.ReturnValue.Get(0).Name;
        methCall = retVal + ' = ' + methCall;
    }    
    
    tw.SetSelection(sel.beginRow, sel.beginCol, sel.endRow, sel.endCol);
    tw.SetSelectedText(srcIndent + methCall);    
}

ExtractMethodRefactoring.prototype.prepareSource = function(source) {

    var lines = StringUtils.toLines(source);
    if (lines.length < 2)
        return source;
        
    var startIndex = 0;
    while (startIndex < lines.length && lines[startIndex].match(/^\s*$/))
        startIndex++;

    var endIndex = lines.length - 1;
    while (endIndex > 0 && lines[endIndex].match(/^\s*$/))
        endIndex--;
        
    if (startIndex <= endIndex)
        return StringUtils.fromLines(lines.splice(startIndex, endIndex - startIndex + 1));
                        
    return source;
}

////} ExtractMethodRefactoring

////////////////////////////////////////////////////////////////////////////////////////
////{ TextChangesWatcher (Александр Орефков)
////

// Класс для отслеживания изменения текста в поле ввода, для замены
// события АвтоПодборТекста. Штатное событие плохо тем, что не возникает
// - при установке пустого текста
// - при изменении текста путем вставки/вырезания из/в буфера обмена
// - при отмене редактирования (Ctrl+Z)
// не позволяет регулировать задержку
// Параметры конструктора
// field - элемент управления поле ввода, чье изменение хотим отслеживать
// ticks - величина задержки после ввода текста в десятых секунды (т.е. 3 - 300 мсек)
// invoker - функция обратного вызова, вызывается после окончания изменения текста,
//  новый текст передается параметром функции
function TextChangesWatcher(field, ticks, invoker)
{
    this.ticks = ticks
    this.invoker = invoker
    this.field = field
}

// Начать отслеживание изменения текста
TextChangesWatcher.prototype.start = function()
{
    this.lastText = this.field.Значение.replace(/^\s*|\s*$/g, '').toLowerCase()
    this.noChangesTicks = 0
    this.timerID = createTimer(100, this, "onTimer")
}
// Остановить отслеживание изменения текста
TextChangesWatcher.prototype.stop = function()
{
    killTimer(this.timerID)
}
// Обработчик события таймера
TextChangesWatcher.prototype.onTimer = function()
{
    // Получим текущий текст из поля ввода
    vbs.var0 = this.field
    vbs.DoExecute("var0.GetTextSelectionBounds var1, var2, var3, var4")
    this.field.УстановитьГраницыВыделения(1, 1, 1, 10000)
    var newText = this.field.ВыделенныйТекст.replace(/^\s*|\s*$/g, '').toLowerCase()
    this.field.УстановитьГраницыВыделения(vbs.var1, vbs.var2, vbs.var3, vbs.var4)
    // Проверим, изменился ли текст по сравению с прошлым разом
    if(newText != this.lastText)
    {
        // изменился, запомним его
        this.lastText = newText
        this.noChangesTicks = 0
    }
    else
    {
        // Текст не изменился. Если мы еще не сигнализировали об этом, то увеличим счетчик тиков
        if(this.noChangesTicks <= this.ticks)
        {
            if(++this.noChangesTicks > this.ticks)  // Достигли заданного количества тиков.
                this.invoker(newText)               // Отрапортуем
        }
    }
}

////
////} TextChangesWatcher (Александр Орефков)
////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////
////{ Вспомогательные функции
////



////} Вспомогательные функции
