$engine JScript
$uname snippets
$dname Шаблоны кода
$addin global
$addin stdlib
////////////////////////////////////////////////////////////////////////////////////////
////{ Cкрипт "Шаблоны кода" (snippets.js) для проекта "Снегопат"
////
//// Описание: Расширение возможностей механизма шаблонов кода 1С:Предприятия 8.
//// Автор: Александр Кунташов <kuntashov@gmail.com>, http://compaud.ru/blog
////}
////////////////////////////////////////////////////////////////////////////////////////
global.connectGlobals(SelfScript);
stdlib.require('TextWindow.js', SelfScript);
stdlib.require('StreamLib.js', SelfScript);
stdlib.require('SettingsManagement.js', SelfScript);
stdlib.require("SelectValueDialog.js", SelfScript);
////////////////////////////////////////////////////////////////////////////////////////
////{ Макросы
////
/* Выполняет подстановку шаблона, выбранного из выпадающего списка шаблонов. */
function macrosВыполнитьПодстановкуШаблона() {
var sm = GetSnippetsManager();
sm.insertSnippet();
}
/* Перезагружает список шаблонов (например, после редактирования шаблона). */
function macrosПерезагрузитьШаблоны() {
var sm = GetSnippetsManager();
sm.reloadTemplates();
}
/* Открывает диалог настройки скрипта. */
function macrosОткрытьНастройкиСкрипта() {
var sm = GetSnippetsManager();
var settingsDialog = new SettingsManagerDialog(sm.settings);
settingsDialog.Open();
}
/* Позволяет вставлять расширенные управляющие конструкции шаблонов из списка выбора.
Предназначен для использования в штатном редакторе шаблонов для вставки расширенных
управляющих конструкций. */
function macrosВставитьРасширеннуюУправляющуюКонструкцию() {
var w = GetTextWindow();
if (!w) return;
var sm = GetSnippetsManager();
var params = sm.paramsManager.getAllParams();
var selParam = sm.selectValue(params);
if (selParam)
w.SetSelectedText('<%' + selParam + '>');
}
/* Возвращает название макроса по умолчанию - вызывается, когда пользователь
дважды щелкает мышью по названию скрипта в окне Снегопата. */
function getDefaultMacros() {
return 'ОткрытьНастройкиСкрипта';
}
////} Макросы
////////////////////////////////////////////////////////////////////////////////////////
////{ SnippetsManager
////
function SnippetsManager() {
SnippetsManager._instance = this;
this.settings = SettingsManagement.CreateManager(SelfScript.uniqueName, {'TemplateFilesList':getDefaultTemplatesList()});
this.settings.LoadSettings();
this._snippets = {};
this._snippetNames = new Array();
this.paramsManager = new SnippetParametersManager();
this.loadTemplates();
}
SnippetsManager.prototype.loadTemplates = function() {
var stFiles = this.settings.current.TemplateFilesList;
for(var i=0; i<stFiles.Count(); i++)
this.loadStFile(getAbsolutePath(stFiles.Get(i).Value));
}
SnippetsManager.prototype.reloadTemplates = function() {
this._snippets = {};
this._snippetNames = new Array();
this.loadTemplates();
}
SnippetsManager.prototype.loadStFile = function(filename) {
var sp = StreamFactory.CreateParser();
if (sp.readStreamFromFile(filename))
{
var arr = sp.parse()
if (!arr) return;
// Загружаем шаблоны.
return this._loadStElement(arr[1]);
}
}
SnippetsManager.prototype._loadStElement = function(stElement) {
var elCount = stElement[0];
var elProps = stElement[1];
if (elProps[1] == 1)
{
// Это группа.
for (var i=2; i<stElement.length; i++)
this._loadStElement(stElement[i]);
}
else
{
// Это элемент.
this._addSnippet(elProps);
}
}
SnippetsManager.prototype._addSnippet = function(stElement) {
var snippet = new Snippet(stElement);
if (!this._snippets[snippet.name])
{
this._snippets[snippet.name] = new Array();
this._snippetNames.push(snippet.name);
}
this._snippets[snippet.name].push(snippet);
if (snippet.hasMacros())
this.createSnippetMacros(snippet);
}
SnippetsManager.prototype.createSnippetMacros = function(snippet) {
SelfScript.self['macrosВставить шаблон ' + snippet.macrosName] = function() {
snippet.insert(GetTextWindow());
};
}
SnippetsManager.prototype.getSnippetsByName = function(name) {
return this._snippets[name];
}
SnippetsManager.prototype.insertSnippet = function() {
var textWindow = GetTextWindow();
if (!textWindow) return;
var snippetName = this.selectValue(this._snippetNames);
if (!snippetName)
return;
var snippets = this._snippets[snippetName];
if (snippets && snippets.length)
{
snippets[0].insert(textWindow);
return true;
}
return;
}
SnippetsManager.prototype.selectValue = function(values) {
//FIXME: создавать объект Svcsvc один раз, при старте скрипта.
var useSvcsvc = true;
try
{
var sel = new ActiveXObject('Svcsvc.Service')
}
catch(e)
{
//Message("Не удалось создать объект 'Svcsvc.Service'. Зарегистрируйте svcsvc.dll");
useSvcsvc = false;
}
//debugger;
if(useSvcsvc){
return sel.FilterValue(values.join("\r\n"), 1 | 4, '', 0, 0, 350, 250);
} else {
var dlg = new SelectValueDialog("Выберите шаблон", values);
dlg.form.GreedySearch = true;
sel = dlg.selectValue();
return dlg.selectedValue
}
}
SnippetsManager.prototype.onProcessTemplate = function(params) {
/* При вставке шаблона штатными средствами (например, при перетаскивании шаблона из дерева шаблонов)
удалим служебные директивы и выполним подстановку наших управляющих конструкций. */
var res = this.paramsManager.processAddMacrosDirective(params.text);
params.text = this.paramsManager.replaceExtendedParams(res.realTpl);
}
////} SnippetsManager
////////////////////////////////////////////////////////////////////////////////////////
////{ SnippetParametersManager
////
function SnippetParametersManager() {
// Для использования в замыканиях.
var sm = this;
this.initExtendedParams();
}
SnippetParametersManager.prototype.initExtendedParams = function ()
{
/* Используется для определения управляющих конструкций, значения подстановки
которых могут меняться в результате изменения конфигурации. */
function f(c){return function(){try{return eval(c);}catch(e){Message(e.description);return ''}}};
this._parameters = {
'Конфигурация.Имя' : f('Метаданные.Имя'),
'Конфигурация.Синоним' : f('Метаданные.Синоним'),
'Конфигурация.Комментарий' : f('Метаданные.Комментарий'),
'Конфигурация.Поставщик' : f('Метаданные.Поставщик'),
'Конфигурация.Версия' : f('Метаданные.Версия'),
'Конфигурация.АвторскиеПрава' : f('Метаданные.АвторскиеПрава'),
'Конфигурация.КраткаяИнформация' : f('Метаданные.КраткаяИнформация'),
'Конфигурация.ПодробнаяИнформация' : f('Метаданные.ПодробнаяИнформация'),
'Конфигурация.АдресИнформацииОКонфигурации' : f('Метаданные.АдресИнформацииОКонфигурации'),
'Конфигурация.АдресИнформацииОПоставщике' : f('Метаданные.АдресИнформацииОПоставщике'),
'ИмяПользователяОС': f('(new ActiveXObject("WScript.Shell")).ExpandEnvironmentStrings("%USERNAME%")')
};
}
SnippetParametersManager.prototype.getAllParams = function () {
var params = new Array();
for (var param in this._parameters)
params.push(param);
return params;
}
SnippetParametersManager.prototype.replaceExtendedParams = function(tpl) {
var code = tpl;
var params = this.getTemplateParams(tpl);
for(var param in params)
code = code.replace(new RegExp(StringUtils.addSlashes(param), 'g'), params[param]);
return code;
}
SnippetParametersManager.prototype.getTemplateParams = function(tpl)
{
var params = {};
// <%Конфигурация.Имя> и т.п.
var matches = tpl.match(/\<\%([\wА-я]+|[\wА-я]+\.[\wА-я]+)\>/gi);
for (var i=0; matches && i<matches.length; i++)
{
var key = matches[i];
var prm = key.substr(2, key.length - 3);
params[key] = this.calcParamValue(prm);
}
return params;
}
SnippetParametersManager.prototype.calcParamValue = function(key)
{
var param = this._parameters[key];
if (!param)
return '';
if (typeof param == 'function')
return param.call();
return param;
}
/* Обрабатывает служебную директиву шаблона <%Макрос: Имя макроса> */
SnippetParametersManager.prototype.processAddMacrosDirective = function (tpl) {
var lines = StringUtils.toLines(tpl);
var result = { 'macrosName':'', 'realTpl':'' };
if (lines.length > 1)
{
// Пример директивы для snippets.js
//<%Макрос "Авторский комментарий: Добавление">
var matches = lines[0].match(/\<\%Макрос\s+\"(.+?)\"\>/);
if (matches)
{
result.macrosName = matches[1];
// Строку с директивой удаляем из шаблона.
lines = lines.slice(1);
}
}
result.realTpl = StringUtils.fromLines(lines);
return result;
}
////} SnippetParametersManager
////////////////////////////////////////////////////////////////////////////////////////
////{ Snippet
////
function Snippet(stElement) {
// ["Имя шаблона 2",0,1,"","Шаблон, включаемый в контекстно меню"]
this.name = stElement[0];
this.includeInContextMenu = (stElement[2] == 1);
this.replacementString = stElement[3];
this.macrosName = '';
this.template = '';
this._initTemplateText(stElement[4]);
}
Snippet.prototype._initTemplateText = function(tpl) {
var sm = GetSnippetsManager();
var res = sm.paramsManager.processAddMacrosDirective(tpl);
this.macrosName = res.macrosName;
this.template = res.realTpl;
}
Snippet.prototype.hasMacros = function() {
return (this.macrosName != '');
}
/* Вычисляет относительные координаты положения маркера курсора в шаблоне.
Возвращает анонимный объект с двумя свойствами row - индекс строки и
col - индекс колонки в строке. Нумерация строк и колонок - с 0.
Возвращает null, если маркер не найден. */
Snippet.prototype.getCursorCoord = function (tpl, selectedRowsCount) {
/* Если есть выделенный текст, то позиция курсора может быть указана
в управляющей конструкции путем добавления символа "|" перед
первым символом подсказки, например <?"|Введите условие">.
Если выделенного текста нет, то позиция курсора определяется при
помощи стандартной управляющей конструкции <?>. */
var stdMarker = '<?>';
var altMarker = '<?"|';
var curMarker = selectedRowsCount ? altMarker : stdMarker;
var lines = StringUtils.toLines(tpl);
// Для определения взаимного расположения стандартного и альтернативного маркера.
var stdMarkerRow = -1;
var altMarkerRow = -1;
var coords = null;
for (var row=0; row<lines.length; row++)
{
if (stdMarkerRow < 0 && lines[row].indexOf(stdMarker) >=0)
stdMarkerRow = row;
if (altMarkerRow < 0 && lines[row].indexOf(altMarker) >=0)
altMarkerRow = row;
var col = lines[row].indexOf(curMarker);
if (col >= 0)
{
coords = { 'row': row, 'col': col };
/* Если маркер альтернативной позиции курсора ниже основного, то при
расчете координаты строки надо учесть высоту выделенного блока. */
if (curMarker == altMarker && stdMarkerRow > -1 && altMarkerRow > stdMarkerRow)
coords.row += selectedRowsCount - 1;
return coords;
}
}
return null;
}
/* Выполняет подстановку значений в шаблон. */
Snippet.prototype.parseTemplateString = function (tpl) {
var sm = GetSnippetsManager();
tpl = sm.paramsManager.replaceExtendedParams(tpl);
/* Используем штатный интерпретатор шаблонов 1С,
доступ к которому нам предоставляет Снегопат. */
return snegopat.parseTemplateString(tpl);
}
/* Выполняет подстановку шаблона в текст */
Snippet.prototype.insert = function (textWindow) {
//debugger;
var code = this.template;
// Определить, есть ли выделенный текст, который надо будет подставить вместо <?>.
var selection = this.getSelection(textWindow);
var selectedText = textWindow.GetSelectedText();
var isSelected = (selectedText != "");
/* Если в хвосте есть перевод строки (выделены с shift'ом строки и в итоге курсор
стоит на следующей строке), то нам этот перевод строки нельзя включать в выделенный
текст, а надо перенести после вставленного сниппета. */
var isTrailingNL = StringUtils.endsWith(selectedText, "\n");
if (isTrailingNL)
selectedText = selectedText.substr(0, selectedText.length - 1);
/* Определим и запомним отступ. Если выделен текст, то отступ определяем
по первой строке выделенного блока. В противном случае используем отступ строки,
в которой был установлен курсор на момент вставки шаблона. */
var ind = '';
if (isSelected)
{
ind = StringUtils.getIndent(selectedText);
}
else
{
var leftPart = textWindow.Range(selection.beginRow, 1, selection.beginRow, selection.beginCol).GetText();
ind = leftPart.match(/^\s*$/) ? leftPart : '';
}
var cursorCoords = this.getCursorCoord(code, isSelected ? StringUtils.toLines(selectedText).length : 0);
// Если был выделен текст, то подставим его вместо <?>.
if (isSelected)
{
// Удалим исходный отступ.
selectedText = StringUtils.shiftLeft(selectedText, ind);
// Отступ, установленный в шаблоне перед <?> надо распространить на весь выделенный текст.
var re = /^([ |\t]+)\<\?\>/m;
var matches = code.match(re);
if (matches)
{
selectedText = StringUtils.shiftRight(selectedText, matches[1]);
code = code.replace(re, selectedText);
}
else
{
code = code.replace(/\<\?\>/, selectedText);
}
/* Удалим альтернативный маркер позиции курсора (если он присутствует), причем
вместе с управляющей конструкцией, чтобы подстановка по шаблону для нее не выполнялась. */
code = code.replace(/\<\?\"\|.*?\".*?\>/, '');
}
else
{
/* Если в момент вставки шаблона не было выделено текста, то удалим штатный маркер
позиции курсора (если вдруг он присутствует), курсор мы будем позиционировать самостоятельно. */
code = code.replace(/\<\?\>/, '');
}
// Выполним подстановку шаблонов 1С.
code = this.parseTemplateString(code);
// Применим отступ к полученному коду сниппета.
code = StringUtils.shiftRight(code, ind);
/* Если вставляется многострочный блок в текущую позицию курсора и никакого
текста не выделено, то надо очистить отступ в первой строке вставляемого
блока, чтобы он не дублировался. */
if (!isSelected && ind != '')
code = code.replace(new RegExp('^' + ind), '');
// Вернем перевод строки в конец вставляемого текста (если он был в конце выделенного блока).
if (isTrailingNL)
code += "\n";
// Заменить выделенный текст или вставить текст в текущую позицию.
textWindow.SetSelectedText(code);
/* Если в тексте был найден маркер положения курсора, то выполним
установку курсора в позицию маркера, рассчитав его абсолютные координаты. */
if (cursorCoords)
{
var row = selection.beginRow + cursorCoords.row;
var col = selection.beginCol + cursorCoords.col + ind.length - (isSelected ? 0 : 1);
textWindow.SetCaretPos(row, col);
}
}
/* Корректирует текущее выделение блока и возвращает выделение (ISelection).
Корректировка заключается в изменении колонки в первой строке и
номера последней строки:
- если первая строка выделена не с начала, но левее выделения в первой строке только
пробельные символы, то выделение начинаем с первого символа первой строки;
- если все символы из последней строки, попавшие в выделение - пробельные, то
эту строку исключаем из выделения. */
Snippet.prototype.getSelection = function (textWindow) {
var sel = textWindow.GetSelection();
if (sel.beginRow != sel.endRow)
{
var beginCol = sel.beginCol;
/* Если левее начала выделения только пробельные символы,
то считаем началом блока начало строки. */
var leftPart = textWindow.GetLine(sel.beginRow).substr(0, beginCol - 1);
if (leftPart.match(/^\s+$/))
beginCol = 1;
/* В последней строке выделения от начала строки и до конца
выделения - пустая строка, то исключим эту строку из выделения. */
var endRow = sel.endRow;//sel.endCol > 1 ? sel.endRow : sel.endRow - 1;
leftPart = textWindow.GetLine(endRow).substr(0, sel.endCol - 1);
if (!leftPart || leftPart.match(/^\s+$/))
endRow--;
// Корректируем выделение выделение блока.
textWindow.SetSelection(sel.beginRow, beginCol, endRow, textWindow.GetLine(endRow).length + 1);
sel = textWindow.GetSelection();
}
return sel;
}
////} Snippet
////////////////////////////////////////////////////////////////////////////////////////
////{ SettingsManagerDialog
////
function SettingsManagerDialog(settings) {
this.settings = settings;
this.form = loadScriptForm("scripts\\snippets.settings.ssf", this);
this.settings.ApplyToForm(this.form);
}
SettingsManagerDialog.prototype.Open = function() {
this.form.Open();
}
SettingsManagerDialog.prototype._saveSettings = function() {
this.settings.ReadFromForm(this.form);
this.settings.SaveSettings();
this.form.Modified = false;
// Перезагрузим шаблоны после изменения настроек.
var sm = GetSnippetsManager();
sm.reloadTemplates();
}
SettingsManagerDialog.prototype.CmdBarSaveAndClose = function(button) {
this._saveSettings();
this.form.Close();
}
SettingsManagerDialog.prototype.CmdBarSave = function (button) {
this._saveSettings();
}
SettingsManagerDialog.prototype.CmdBarClose = function (button) {
this.form.Close();
}
SettingsManagerDialog.prototype.CmdBarAbout = function (button) {
RunApp('http://snegopat.ru/scripts/wiki?name=snippets.js');
}
SettingsManagerDialog.prototype.selectTemplateFiles = function (multiselect) {
var dlg = v8New('FileDialog', FileDialogMode.Open);
dlg.Multiselect = multiselect ? true : false;
dlg.CheckFileExist = true;
dlg.Filter = "Файлы шаблонов (*.st)|*.st|Все файлы|*";
if (dlg.Choose())
return multiselect ? dlg.SelectedFiles : dlg.FullFileName;
return null;
}
SettingsManagerDialog.prototype.CmdBarStListAddStFile = function (button) {
var selected = this.selectTemplateFiles(true);
if (selected)
{
this.form.Modified = true;
for (var i=0; i<selected.Count(); i++)
this.form.TemplateFilesList.Add().Value = selected.Get(i);
}
}
SettingsManagerDialog.prototype.CmdBarStListDeleteStFile = function (button) {
var curRow = this.form.Controls.TemplateFilesList.CurrentRow;
if (curRow)
{
this.form.TemplateFilesList.Delete(curRow);
this.form.Modified = true;
}
}
SettingsManagerDialog.prototype.TemplateFilesListValueStartChoice = function (Control, DefaultHandler) {
DefaultHandler.val = false;
var fname = this.selectTemplateFiles(false);
if (fname)
{
this.form.Modified = true;
Control.val.Value = fname;
}
}
SettingsManagerDialog.prototype.OnOpen = function() {
}
SettingsManagerDialog.prototype.BeforeClose = function(Cancel, StandardHandler) {
StandardHandler.val = false;
if (this.form.Modified)
{
var answ = DoQueryBox("Настройки были изменены. Сохранить?", QuestionDialogMode.YesNoCancel);
if (answ == DialogReturnCode.Cancel)
{
Cancel.val = true;
return;
}
if (answ == DialogReturnCode.Yes)
this._saveSettings();
}
Cancel.val = false;
}
////} SettingsManagerDialog
////{ Вспомогательные функции.
function getDefaultTemplatesList() {
var tplList = v8New('ValueTable');
tplList.Columns.Add('Value');
return tplList;
}
function getAbsolutePath(path) {
// Путь относительный?
if (path.match(/^\.{1,2}[\/\\]/))
{
// Относительные пути должны задаваться относительно главного каталога Снегопата.
var mainFolder = profileRoot.getValue("Snegopat/MainFolder");
return mainFolder + path;
}
return path;
}
////} Вспомогательные функции.
////////////////////////////////////////////////////////////////////////////////////////
////{ Startup
////
function GetSnippetsManager() {
if (!SnippetsManager._instance)
new SnippetsManager();
return SnippetsManager._instance;
}
events.connect(snegopat, "onProcessTemplate", GetSnippetsManager());
////} Startup