Index: Libs/SyntaxAnalysis.js ================================================================== --- Libs/SyntaxAnalysis.js +++ Libs/SyntaxAnalysis.js @@ -269,11 +269,11 @@ return this.textWindow.Range(method.StartLine + 1, 1, method.EndLine + 1).GetText(); } /* Возвращает таблицу значений с описаниями методов модуля. */ _1CModule.prototype.getMethodsTable = function() { - return this.context._vtAllMethods; + return this.context._vtAllMethods.Copy(); } /* Возвращает описание метода по номеру строки, находящейся внутри метода. */ _1CModule.prototype.getMethodByLineNumber = function (lineNo) { Index: Tests/Automated/SyntaxAnalysis/testSyntaxAnalysis.js ================================================================== --- Tests/Automated/SyntaxAnalysis/testSyntaxAnalysis.js +++ Tests/Automated/SyntaxAnalysis/testSyntaxAnalysis.js @@ -15,11 +15,10 @@ function tearDown() { } //} setUp/tearDown //{ tests of AnalyseModule - function macrosTestAnalyseModule1() { var moduleText = "" + "Перем мПеременнаяМодуля;\n\n" + "Перем ЕщеОднаПеременная;\n" @@ -237,11 +236,11 @@ // Локальные переменные процедуры. assertArrayEqualsIgnoringOrder(['МояПерем1'], proc.DeclaredVars); assertArrayEqualsIgnoringOrder(['АвтоматическаяПеременная'], proc.AutomaticVars); -} +} function macrosTestAnalyseModule9_ОпределениеМетодаНаРазныхСтроках() { var moduleText = "" + "Процедура \n" + " Проверки ( Перем1,Перем2, Перем3)\n" @@ -251,11 +250,23 @@ assertEquals('Неправильно определено количество методов!', 1, cnt.Methods.length); assertEquals('Неправильно определено количество переменных модуля!', 0, cnt.ModuleVars.length); } -function macrosTestAnalyseModule10_ОпределениеПараметровМетодаНаРазныхСтроках() { +function macrosTestAnalyseModule10_ОпределениеМетодаНаРазныхСтроках2() { + var moduleText = "" + + "Процедура Проверки ( Перем1, \n" + + " Перем2, Перем3)\n" + + "КонецПроцедуры" + + var cnt = SyntaxAnalysis.AnalyseModule(moduleText); + + assertEquals('Неправильно определено количество методов!', 1, cnt.Methods.length); + assertEquals('Неправильно определено количество переменных модуля!', 0, cnt.ModuleVars.length); +} + +function macrosTestAnalyseModule11_ОпределениеПараметровМетодаНаРазныхСтроках() { var moduleText = "" + "Процедура Проверки ( Перем1, \n" + " Перем2, Перем3)\n" + "КонецПроцедуры" //debugger @@ -266,11 +277,11 @@ var proc = cnt.getMethodByName('Проверки'); assertNotNull("Метод Проверки не найден", proc); assertArrayEqualsIgnoringOrder(['Перем1', 'Перем2', 'Перем3'], proc.Params); } -function macrosTestAnalyseModule11_ОпределениеПеременныхМетодаПоУмолчанию() { +function macrosTestAnalyseModule12_ОпределениеПеременныхМетодаПоУмолчанию() { var moduleText = "" + "Процедура Проверки (Знач Парам1, Парам2 = Ложь)\n" + "КонецПроцедуры" //debugger var cnt = SyntaxAnalysis.AnalyseModule(moduleText); @@ -281,11 +292,11 @@ assertNotNull("Метод Проверки не найден", proc); assertArrayEqualsIgnoringOrder(['Парам1', 'Парам2'], proc.Params); } -function macrosTestAnalyseModule12_ОпределениеКонеткстаКомпиляции() { +function macrosTestAnalyseModule13_ОпределениеКонеткстаКомпиляции() { var moduleText = "" + "&НаКлиенте\n" + "Процедура Проверки (Знач Парам1, Парам2 = Ложь)\n" + "КонецПроцедуры" var cnt = SyntaxAnalysis.AnalyseModule(moduleText); @@ -296,11 +307,11 @@ assertNotNull("Метод Проверки не найден", proc); assertEquals("Конетекст компиляции не обнаружен", "НаКлиенте", proc.Context) assertArrayEqualsIgnoringOrder(['Парам1', 'Парам2'], proc.Params); } -function macrosTestAnalyseModule13_ОпределениеПараметровМетодаНаРазныхСтрокахСКомментариями() { +function macrosTestAnalyseModule14_ОпределениеПараметровМетодаНаРазныхСтрокахСКомментариями() { var moduleText = "" + "Процедура Проверки ( Перем1, //Текстовый комментарий перемменной, да и такое может быть. \n" + " Перем2, Перем3)\n" + "КонецПроцедуры" debugger @@ -311,11 +322,11 @@ var proc = cnt.getMethodByName('Проверки'); assertNotNull("Метод Проверки не найден", proc); assertArrayEqualsIgnoringOrder(['Перем1', 'Перем2', 'Перем3'], proc.Params); } -function macrosTestAnalyseModule14_ОпределениеПараметровМетодаНаРазныхСтрокахСКомментариямиИСкобками() { +function macrosTestAnalyseModule15_ОпределениеПараметровМетодаНаРазныхСтрокахСКомментариямиИСкобками() { var moduleText = "" + "Процедура Проверки ( Перем1, //Текстовый комментарий перемменной, да и такое может быть. \n" + " Перем2, // Любой текст и ссылка на процедуру или функцию МояПроцедура()\n" + " Перем3)\n" + "КонецПроцедуры" @@ -326,10 +337,9 @@ assertEquals('Неправильно определено количество переменных модуля!', 0, cnt.ModuleVars.length); var proc = cnt.getMethodByName('Проверки'); assertNotNull("Метод Проверки не найден", proc); assertArrayEqualsIgnoringOrder(['Перем1', 'Перем2', 'Перем3'], proc.Params); } - //} tests of AnalyseModule ADDED refactoring.extractMethod.ssf Index: refactoring.extractMethod.ssf ================================================================== --- refactoring.extractMethod.ssf +++ refactoring.extractMethod.ssf cannot compute difference between binary files ADDED refactoring.js Index: refactoring.js ================================================================== --- refactoring.js +++ refactoring.js @@ -0,0 +1,523 @@ +$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 (Александр Орефков) +//////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////// +////{ Вспомогательные функции +//// + + + +////} Вспомогательные функции ADDED refactoring.methodList.ssf Index: refactoring.methodList.ssf ================================================================== --- refactoring.methodList.ssf +++ refactoring.methodList.ssf cannot compute difference between binary files