История вопроса - в обычных формах была возможность вывести редактор кода, после перехода на управляемые формы механику убрали.
Из того, что осталось:
1. Простое поле ввода - нет подсветки кода
2. Поле форматированного документа - подсветку можно добавить отдельно, но в реальном времени подсветка кода не отображается
3. Внешние редакторы кода
- monaco editor (вариант VS code) - известный проект https://github.com/salexdv/bsl_console. Из проблем - нужно либо сохранять файлы в папку, либо указывать ссылку https://salexdv.github.io/bsl_console/src/index.html. Сама по себе функциональная и много где используется, но тяжёлая и нестабильная.
- ace editor
- codemirror
Хотелось чего-то простого и лёгкого. При этом важно, что в платформе старая версия Webkit 605 https://habr.com/ru/companies/1c/articles/425713/
Исходя из этого с помощью нейросетей собрал простой редактор кода.
Функционал:
1. Подсветка кода (алгоритм раскраски на базе 1С также есть в статье)
2. Номера строк (немного расходятся при прокрутке до самого низа, но не особо критично - решил оставить как есть)
3. Возможность прикрутить синтаксис подсказку через 1С. Базовый пример есть в 1С, остальное можно реализовать самостоятельно. Например анализ метаданных, чтобы подтягивались общие модули, справочники, документы.
4. Мелкие штрихи - работа с табуляцией
Как интегрировать - скопировать код и подключить его в обработчике "ПриСозданииНаСервере". Все элементы формы и команды для синтаксис подсказки создаются программно.
Для ленивых положил файл обработки за 1$CM
Код редактора
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Обязательная часть
НачальныйКод =
"Сообщить (""Установка кода при открытии формы"");
|#Область Пример
|// Комментарии
|Функция МойПример()
| Дата = '2026-05-14';
| Строка = ""Пример строки"";
| Число = 12345.678;
|КонецФункции
|#КонецОбласти";
ИнициализацияРедактора(НачальныйКод);
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#Область РедакторКодаВHML
// Обязательная часть
&НаСервере
Процедура ИнициализацияРедактора(НачальныйКод = "")
// Сам редактор - обязательно
ДобавляемыеРеквизиты = Новый Массив;
ДобавляемыеРеквизиты.Добавить(Новый РеквизитФормы("Редактор", Новый ОписаниеТипов("Строка")));
ДобавляемыеРеквизиты.Добавить(Новый РеквизитФормы("НачальныйКод", Новый ОписаниеТипов("Строка")));
ИзменитьРеквизиты(ДобавляемыеРеквизиты);
//ЭтотОбъект.Редактор = РеквизитФормыВЗначение("Объект").ПолучитьМакет("Редактор").ПолучитьТекст();
ЭтотОбъект.Редактор = МакетРедактора();
ЭтотОбъект.НачальныйКод = НачальныйКод;
ЭлементФормы = Элементы.Добавить("Редактор", Тип("ПолеФормы"));
ЭлементФормы.Вид = ВидПоляФормы.ПолеHTMLДокумента;
ЭлементФормы.ПутьКДанным = "Редактор";
ЭлементФормы.УстановитьДействие("ДокументСформирован", "РедакторДокументСформирован");
// Команда контекстной подсказки - необязательно
Команда = ЭтаФорма.Команды.Добавить("Подсказка");
Команда.Действие = "Подсказка";
Команда.СочетаниеКлавиш = Новый СочетаниеКлавиш(Клавиша.Space,, Истина);
Кнопка = ЭтаФорма.Элементы.Добавить("КнопкаПодсказка", Тип("КнопкаФормы"), Элементы.РедакторКонтекстноеМеню);
Кнопка.ИмяКоманды = "Подсказка";
КонецПроцедуры
&НаКлиентеНаСервереБезКонтекста
Функция МакетРедактора()
Возврат
"<DOCTYPE html>
|<html>
|<head>
| <meta charset='UTF-8'>
| <meta name='viewport' content='width=device-width, initial-scale=1.0'>
| <style>
| html,
| body {
| margin: 0;
| padding: 0;
| height: 100%;
| overflow: hidden;
| }
|
| .editor {
| position: absolute;
| top: 0;
| left: 0;
| width: 100%;
| height: 100%;
| overflow: hidden;
| background: #fff;
| }
|
| #lines {
| position: absolute;
| top: 0;
| left: 0;
| width: 40px;
| height: 100%;
| padding-top: 8px;
| padding-bottom: 8px;
| background: #f5f5f5;
| border-right: 1px solid #ddd;
| overflow: hidden;
| pointer-events: none;
| z-index: 3;
| font: 14px/1.5 Consolas, Monaco, monospace;
| color: #888;
| text-align: right;
| box-sizing: border-box;
| }
|
| #lines div {
| margin: 0;
| padding: 0;
| line-height: 1.5;
| }
|
| #hl,
| #code {
| position: absolute;
| top: 0;
| left: 0;
| width: 100%;
| height: 100%;
| margin: 0;
| padding: 8px 8px 8px 48px;
| font: 14px/1.5 Consolas, Monaco, monospace;
| overflow: auto;
| white-space: pre;
| box-sizing: border-box;
| tab-size: 4;
| }
|
| textarea {
| background: transparent;
| color: rgba(0, 0, 0, 0.01);
| z-index: 2;
| resize: none;
| border: 0;
| outline: 0;
| caret-color: #000;
| }
|
| pre {
| z-index: 1;
| pointer-events: none;
| color: #333;
| }
|
| /* 1С ключевые слова */
| .keyword {
| color: #f00;
| font-weight: bold;
| }
|
| .identifier {
| color: #00f;
| }
|
| /* Комментарии */
| .comment {
| color: #008000;
| }
|
| /* Препроцессор */
| .preproc {
| color: #963200;
| }
|
| /* Аннотации */
| .annotation {
| color: #963200;
| }
|
| /* Числа */
| .number {
| color: #000;
| }
|
| /* Строки */
| .string {
| color: #aaa;
| }
|
| /* Даты */
| .date {
| color: #000;
| }
|
| .operator {
| color: #f00;
| }
|
| .error {
| color: #f80;
| }
|
| /* Директивы препроцессора */
| .dir {
| color: #800080;
| }
|
| </style>
|</head>
|
|<body>
|
| <div class='editor'>
| <div id='lines'></div>
| <pre id='hl'></pre>
| <textarea id='code' spellcheck='false' autocomplete='off' wrap='off'
| placeholder='Начните вводить код...'></textarea>
| </div>
|
| <script>
| var ta = document.getElementById('code');
| var hl = document.getElementById('hl');
| var lines = document.getElementById('lines');
| const keywords = 'если if тогда then иначеесли elsif иначе else конецесли endif для for каждого each из in по to пока while цикл do конеццикла enddo ждать await процедура procedure функция function конецпроцедуры endprocedure конецфункции endfunction перем var перейти goto возврат return продолжить continue прервать break и and или or не not попытка try исключение except вызватьисключение raise конецпопытки endtry новый new выполнить execute асинх истина ложь null Неопределено'.split(' ')
|
| function endLexemExecute(element, props, endLexem) {
| const lexem = element.slice(props.beginLexem, endLexem)
| var type = props.lexemType
| var result
| if (type == 'identifier' && keywords.includes(lexem.toLowerCase())) {
| type = 'keyword'
| } else if (type == 'numberWithDot') {
| type = 'number'
| }
|
| props.beginLexem = endLexem
| if (type == undefined || lexem.length == 0) {
| result = lexem
| } else {
| result = '<span class=""' + type + '"">' + lexem + '</span>'
| }
| props.lexemType = undefined
| return result
| }
|
| function highlightCode(code) {
|
| var result = [];
| var props = {
| lexemType: undefined,
| beginLexem: 0
| }
|
| // Сначала экранируем HTML
| code = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| code.split('\n').forEach((element, index) => {
| let i = 0
| props.beginLexem = 0
| while (i < element.length) {
| let symbol = element[i]
| let symbolNext = element[i + 1]
| if (props.lexemType == undefined) {
| if (symbol == '/' && symbolNext == '/') {
| props.lexemType = 'comment'
| result.push(endLexemExecute(element, props, element.length))
| break
| } else if (symbol == '#') {
| props.lexemType = 'preproc'
| result.push(endLexemExecute(element, props, element.length))
| break
| } else if (symbol == '&') {
| props.lexemType = 'annotation'
| result.push(endLexemExecute(element, props, element.length))
| break
| } else if (symbol == '""' || symbol == '|') {
| props.lexemType = 'string'
| } else if (symbol == ""'"") {
| props.lexemType = 'date'
| } else if ('абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz_'.includes(symbol.toLowerCase())) {
| props.lexemType = 'identifier'
| } else if ('0123456789'.includes(symbol)) {
| props.lexemType = 'number'
| } else if (' \t'.includes(symbol)) {
| props.lexemType = 'whitespace'
| } else if ('()[]+-*/%<>=~;:.,'.includes(symbol)) {
| props.lexemType = 'operator'
| result.push(endLexemExecute(element, props, i + 1))
| } else {
| props.lexemType = 'error'
| result.push(endLexemExecute(element, props, i + 1))
| }
| } else if (props.lexemType == 'string' && symbol == '""') {
| if (symbolNext == '""') {
| i++
| } else {
| result.push(endLexemExecute(element, props, i + 1))
| }
| } else if (props.lexemType == 'date' && symbol == ""'"") {
| result.push(endLexemExecute(element, props, i + 1))
| } else if (props.lexemType == 'identifier'
| && ! 'абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz_0123456789'.includes(symbol.toLowerCase())) {
| result.push(endLexemExecute(element, props, i))
| continue
| } else if (props.lexemType == 'number') {
| if (symbol == '.') {
| props.lexemType = 'numberWithDot'
| } else if (! '0123456789'.includes(symbol)) {
| result.push(endLexemExecute(element, props, i))
| continue
| }
| } else if (props.lexemType == 'numberWithDot' && ! '0123456789'.includes(symbol)) {
| result.push(endLexemExecute(element, props, i))
| continue
| } else if (props.lexemType == 'whitespace' && !(' \t'.includes(symbol))) {
| result.push(endLexemExecute(element, props, i))
| continue
| }
| i++
| }
| result.push(endLexemExecute(element, props, i))
| result.push('\n')
| });
| return result.join('');
| }
|
| function render() {
| var v = ta.value;
| hl.innerHTML = highlightCode(v);
|
| var lineCount = v.split('\n').length;
| var html = '';
| for (var i = 1; i <= lineCount; i++) {
| html += '<div>' + i + '</div>';
| }
| lines.innerHTML = html;
| }
|
| function syncscroll() {
| hl.scrollTop = ta.scrollTop;
| hl.scrollLeft = ta.scrollLeft;
| lines.scrollTop = ta.scrollTop;
| }
| document.render = render
|
| ta.addEventListener('input', function () {
| render();
| syncscroll();
| });
|
| ta.addEventListener('scroll', syncscroll);
|
| ta.addEventListener('keydown', function (e) {
| // Tab
| if (e.keyCode === 9) {
| e.preventDefault();
| var start = ta.selectionStart;
| var end = ta.selectionEnd;
|
| // Если есть выделение
| if (start !== end) {
| var selectedText = ta.value.substring(start, end);
| var lines = selectedText.split('\n');
|
| if (e.shiftKey) {
| // Shift+Tab: убираем отступ
| lines = lines.map(function(line) {
| if (line.startsWith('\t')) {
| return line.substring(1);
| }
| // Также убираем пробельные отступы (4 пробела)
| var spacesToRemove = 0;
| for (var i = 0; i < line.length && i < 4; i++) {
| if (line[i] === ' ') {
| spacesToRemove++;
| } else {
| break;
| }
| }
| return line.substring(spacesToRemove);
| });
| } else {
| // Tab: добавляем отступ
| lines = lines.map(function(line) {
| return '\t' + line;
| });
| }
|
| var newSelectedText = lines.join('\n');
| ta.value = ta.value.substring(0, start) + newSelectedText + ta.value.substring(end);
|
| // Восстанавливаем выделение с учётом изменений
| var lengthDiff = newSelectedText.length - selectedText.length;
| ta.selectionStart = start;
| ta.selectionEnd = end + lengthDiff;
| } else {
| // Нет выделения: просто вставляем табуляцию
| document.execCommand('insertText', false, '\t')
| }
|
| render();
| }
|
| // Enter - автоотступ
| if (e.keyCode === 13) {
| e.preventDefault();
| var start = ta.selectionStart;
| var end = ta.selectionEnd;
|
| // Находим предыдущую строку
| var textBeforeCursor = ta.value.substring(0, start);
| var lastNewlineIndex = textBeforeCursor.lastIndexOf('\n');
| var previousLine = lastNewlineIndex >= 0
| ? textBeforeCursor.substring(lastNewlineIndex + 1, start)
| : textBeforeCursor;
|
| // Подсчитываем количество ведущих табуляций
| var indent = '';
| for (var i = 0; i < previousLine.length; i++) {
| if (previousLine[i] === '\t') {
| indent += '\t';
| } else {
| break;
| }
| }
|
| // Вставляем перенос строки и отступ через execCommand для сохранения истории
| document.execCommand('insertText', false, '\n' + indent);
|
| render();
| }
|
|
| });
|
| // Инициализация
| render();
| </script>
|</body>
|
|</html>"
КонецФункции
&НаКлиенте
Процедура УстановитьКодВРедактор(КодДляУстановки)
ОбластьРедактора().value = КодДляУстановки;
Документ().render();
КонецПроцедуры
&НаКлиенте
Процедура РедакторДокументСформирован(Элемент)
УстановитьКодВРедактор(ЭтотОбъект.НачальныйКод);
КонецПроцедуры
// Контекстная подсказка - необязательная часть
&НаКлиенте
Асинх Процедура Подсказка(Команда)
КлючевыеСлова = СтрРазделить("Если Тогда ИначеЕсли Иначе КонецЕсли Для Каждого Из По Пока Цикл КонецЦикла Ждать Процедура Функция КонецПроцедуры КонецФункции Перем Перейти Возврат Продолжить Прервать И Или Не Попытка Исключение ВызватьИсключение КонецПопытки Новый Выполнить Асинх Истина Ложь Null Неопределено", " ");
ТекущийТекст = РедакторТекст();
ПозицияКурсора = РедакторНачалоВыделения();
НачалоСтроки = ПозицияКурсора;
Буфер = "";
Пока ПозицияКурсора > 0 Цикл
Символ = НРег(Сред(ТекущийТекст, ПозицияКурсора, 1));
Если СтрНайти("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz_", Символ) = 0 Тогда
Прервать;
КонецЕсли;
Буфер = Символ + Буфер;
ПозицияКурсора = ПозицияКурсора - 1;
КонецЦикла;
Если СтрДлина(Буфер) = 0 Тогда
Возврат
КонецЕсли;
Варианты = Новый СписокЗначений;
Для каждого КлючевоеСлово Из КлючевыеСлова Цикл
Если СтрНачинаетсяС(НРег(КлючевоеСлово), Буфер) Тогда
Варианты.Добавить(КлючевоеСлово);
КонецЕсли;
КонецЦикла;
Если Не Варианты.Количество() Тогда
Возврат;
ИначеЕсли Варианты.Количество() = 1 Тогда
ВыбранныйВариант = Варианты[0];
Иначе
ВыбранныйВариант = Ждать ВыбратьИзСпискаАсинх(Варианты);
КонецЕсли;
Если ВыбранныйВариант = Неопределено Тогда
Возврат;
КонецЕсли;
Документ().execCommand("insertText", false, Сред(ВыбранныйВариант.Значение, СтрДлина(Буфер) + 1));
КонецПроцедуры
&НаКлиенте
Функция Документ()
Возврат Элементы.Редактор.Документ;
КонецФункции
&НаКлиенте
Функция ОбластьРедактора()
Возврат Документ().getElementById("code");
КонецФункции
&НаКлиенте
Функция РедакторТекст()
Возврат ОбластьРедактора().value;
КонецФункции
&НаКлиенте
Функция РедакторНачалоВыделения()
Возврат ОбластьРедактора().selectionStart;
КонецФункции
#КонецОбласти
#КонецОбласти
Проект редактора на базе JavaScript - https://github.com/fotov/bsl_html_code_editor
Дополнительно - код раскраски на базе 1С
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Код = РеквизитФормыВЗначение("Объект").ПолучитьМакет("Макет").ПолучитьТекст();
РаскраситьКод(Код);
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
&НаСервере
Процедура РаскраситьКод(Код)
Параграфы = ВыделитьЛексемы(Код);
Цвета = Новый Соответствие;
Цвета.Вставить("АННОТАЦИИ", Новый Цвет(150, 50, 0));
Цвета.Вставить("ПРЕПРОЦЕССОР", Новый Цвет(150, 50, 0));
Цвета.Вставить("КОММЕНТАРИЙ", WebЦвета.Зеленый);
//Цвета.Вставить("СТРОКА", WebЦвета.Черный);
//Цвета.Вставить("ДАТА", WebЦвета.Черный);
//Цвета.Вставить("ЧИСЛО", WebЦвета.Черный);
Цвета.Вставить("ОШИБКА", WebЦвета.Розовый);
Цвета.Вставить("ИДЕНТИФИКАТОР", WebЦвета.Синий);
Цвета.Вставить("КЛЮЧЕВОЕСЛОВО", WebЦвета.Красный);
Цвета.Вставить("ОПЕРАТОР", WebЦвета.Красный);
Для каждого Параграф Из Параграфы Цикл
Для каждого Лексема Из Параграф Цикл
Текст = ФД.Добавить(Лексема.Значение);
//Текст.Шрифт = Новый Шрифт("Consolas");
Если Цвета[Лексема.Тип] <> Неопределено Тогда
Текст.ЦветТекста = Цвета[Лексема.Тип];
КонецЕсли;
КонецЦикла;
ФД.Добавить(, Тип("ПереводСтрокиФорматированногоДокумента"));
КонецЦикла;
КонецПроцедуры
&НаКлиентеНаСервереБезКонтекста
Функция ВыделитьЛексемы(Знач Код)
Параграфы = Новый Массив;
Для каждого Строка Из СтрРазделить(Код, Символы.ПС) Цикл
Лексемы = Новый Массив;
Состояние = Неопределено;
ДлинаСтроки = СтрДлина(Строка);
н = 1;
НачалоЛексемы = 1;
Пока н <= ДлинаСтроки Цикл
Символ = Сред(Строка, н, 1);
СледующийСимвол = Сред(Строка, н + 1, 1);
Если Состояние = Неопределено Тогда
Если Символ = "/" И СледующийСимвол = "/" Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, ДлинаСтроки, "КОММЕНТАРИЙ");
Прервать;
ИначеЕсли Символ = "#" Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, ДлинаСтроки, "ПРЕПРОЦЕССОР");
Прервать;
ИначеЕсли Символ = "&" Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, ДлинаСтроки, "АННОТАЦИИ");
Прервать;
ИначеЕсли Символ = """" Или Символ = "|" Тогда
Состояние = "СТРОКА";
ИначеЕсли Символ = "'" Тогда
Состояние = "ДАТА";
ИначеЕсли СтрНайти("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz_", НРег(Символ)) > 0 Тогда
Состояние = "ИДЕНТИФИКАТОР";
ИначеЕсли СтрНайти("0123456789", Символ) > 0 Тогда
Состояние = "ЧИСЛО";
ИначеЕсли СтрНайти(" ", Символ) > 0 Тогда
Состояние = "ПРОБЕЛЬНЫЕСИМВОЛЫ";
ИначеЕсли СтрНайти("()[]+-*/%<>=~;:.,'", Символ) > 0 Тогда
Состояние = "ОПЕРАТОР";
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н, Состояние);
Иначе
Состояние = "ОШИБКА";
КонецЕсли;
ИначеЕсли Состояние = "СТРОКА" И Символ = """" Тогда
Если СледующийСимвол = """" Тогда
н = н + 1;
Иначе
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н, Состояние);
КонецЕсли;
ИначеЕсли Состояние = "ДАТА" И Символ = "'"Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н, Состояние);
ИначеЕсли Состояние = "ИДЕНТИФИКАТОР"
И СтрНайти("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz_0123456789", НРег(Символ)) = 0 Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н - 1, Состояние);
Продолжить;
ИначеЕсли Состояние = "ЧИСЛО" Тогда
Если Символ = "." Тогда
Состояние = "ЧИСЛОСДРОБНОЙЧАСТЬЮ";
ИначеЕсли СтрНайти("0123456789", Символ) = 0 Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н - 1, Состояние);
Продолжить;
КонецЕсли;
ИначеЕсли Состояние = "ЧИСЛОСДРОБНОЙЧАСТЬЮ" И СтрНайти("0123456789", Символ) = 0 Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н - 1, Состояние);
Продолжить;
ИначеЕсли Состояние = "ПРОБЕЛЬНЫЕСИМВОЛЫ" И СтрНайти(" ", Символ) = 0 Тогда
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н - 1, Состояние);
Продолжить;
КонецЕсли;
н = н + 1;
КонецЦикла;
ЗавершитьОбработкуЛексемы(Лексемы, Строка, НачалоЛексемы, н, Состояние);
Параграфы.Добавить(Лексемы);
КонецЦикла;
Возврат Параграфы;
КонецФункции
&НаКлиентеНаСервереБезКонтекста
Процедура ЗавершитьОбработкуЛексемы(Лексемы, Строка, Начало, Конец, Состояние)
Лексема = Сред(Строка, Начало, Конец - Начало + 1);
Если Состояние = "ИДЕНТИФИКАТОР"
И СтрНайти(" если if тогда then иначеесли elsif иначе else конецесли endif для for каждого each из in по to пока while цикл do конеццикла enddo ждать await процедура procedure функция function конецпроцедуры endprocedure конецфункции endfunction перем var перейти goto возврат return продолжить continue прервать break и and или or не not попытка try исключение except вызватьисключение raise конецпопытки endtry новый new выполнить execute асинх истина ложь null неопределено "
, СтрШаблон(" %1 ", НРег(Лексема))) > 0 Тогда
Тип = "КЛЮЧЕВОЕСЛОВО";
Иначе
Тип = Состояние;
КонецЕсли;
Если Лексема <> "" Тогда
Лексемы.Добавить(Новый Структура("Тип, Значение", Тип, Лексема));
КонецЕсли;
Состояние = Неопределено;
Начало = Конец + 1;
КонецПроцедуры
#КонецОбласти
upd - добавил программную установку кода
Проверено на следующих конфигурациях и релизах:
- 1С:Библиотека стандартных подсистем, редакция 3.1, релизы 3.1.11.366
Вступайте в нашу телеграмм-группу Инфостарт