Как подумать о том что хорошо бы реализовать idea для php и вляпаться в разработку

от автора

Это первая статья о том как я начал изучать как работают инструменты разработки для языка программирования PHP. С какими я трудностями столкнулся, и как я это решал. В этой первой статье я опишу о том почему не подошли готовые решения для редактора кода, основное готовое решение для редактора кода был выбран CodeArea от ReachTextFX


Стэк технологий был выбран такой:

  1. Java — как платформа разработки

  2. Gradle — как менеджер пакетов для java

  3. JavaFX — для frontend приложения

  4. SqlLite — для хранение глобальных конфигураций и конфигураций проектов

Архитектура проекта изначально строилось так что бы логику отображения оставить в одном слое это как вы понимаете JavaFX со своим потоком, логика связанная с обработкой кода она тоже выделена в поток, логика работы с сокетами в основном это xdebug.

Что бы потоки и логика была мало зависимы друг от друга, с помощью DTO (Data Transfer Object) было описано состояние приложение, которое создается в сервисах бизнес-логики, и уже поток отображения читает это состояние и в зависимости от него строит интерфейс. Простой пример можно привести к примеру когда мы получили массив переменных из сокета xdebug и отображаем его в дереве переменных, если это массив значит срабатывает построение дерева, если текстовая переменная просто отображается текст.

Есть еще один ключевой момент архитектуры, я сразу заложил функционал диспетчера, и многие внутренние процессы такие как открытие файла, обработка сообщений от xdebug, обработка кода и так далее сделаны на том что в момент какого либо действия, отсылается сообщение что это действие произошло с объектом описывающим данное действие, и уже все остальные сервисы подписывают обработчики на данное сообщение. Это сделано на будущее для функционала плагинов, плагин можно полностью написать на Observers (Наблюдателей за тем или иным действием).

Остановимся на плюсах и минусах второго редактора более подробно

Основные плюсы CodeArea следующие:

  1. Подсветка через regexp внутри есть StyledDocument который разбивает строку на токенны соответствующие регулярному выражению.

  2. Работа с параграфами (строками)

  3. Редактор кода сам по себе довольно быстро работает

  4. Простые фабрики для Gatera

  5. Мульти-каретка — несколько кареток в одном редакторе

  6. Управление позиционированием

Но не смотря на реализацию и гибкость есть и довольно сильные минусы:

  1. Gater — реализован как элементы в начале строки, то есть из этого вытекает много проблем несвязанности, вычисление размеров

  2. Нельзя каким либо нормальным способом добавить свой элемент в сам редактор (к примеру волнистую линию для ошибочных строк) приходиться добавлять в gutter элемент и его откреплять от layout и в ручную вычислять размеры, позицию, видимость и устанавливать их элементу, что кажется что это кастыль а если кажется то оно так и есть.

  3. Реализация просто линии для отображения справа границы 80го символа так же через gater что тоже является скотчем

  4. Выделение строки через gater

  5. Выделение строки если сработала точка остановки xdebug такая же проблема невозможно добавить элемент подчеркивания строки

  6. Из более сложного автозаполнение кода, реализация когда то была но сейчас вроде вынесена в другой компонент но я его для так и не смог найти, реализация своего тоже почти не реально потому что кастомный элемент можно добавить только в строку

  7. Есть способ обвернуть CodeArea и добавить свой overlay для него и там вычисляя границы строк и токенов отображать элементы по верх CodeArea, но это тоже почти невозможно потому что у ReachTextFX своя реализация скролинга и она принимает только CodeArea так что возможности в нее передать элемент контейнера такие как StackPane невозможно, при использование просто ScrollPane все размеры ломаются

  8. Координаты Bounds, практически невозможно получить актуальные координаты потому что Bounds параграфа считается с 0 с лева и не учитывают Bounds гатера так как гатер является параграфом, вычисление ширины того же гатера то же не дает достаточно точных цифр его ширины, даже в момент полной отрисовки сцены на экране компьютера.

  9. Нет API что бы получить координаты каретки, хотя можно получить Bounds самой каретки, пробовал получить Bounds гатера, получить отступы в css, получить Bounds самой строки. И так далее, в какой то момент удалось в целом получить более менее приемлемый результат но оказалось при рендере параграфа есть нюансы со склейкой Span в StyledDocuments, и тоже появляется какое то неизвестное мне значение или артифакт, который имеет очень маленькие значения но после 40 пробелов достигает 1-2 пикселей. Что критично было даже для подчеркивания строки.

Я реализовал довольно много задач и интеграций с этим CodeArea, и даже сделал overlay котором решил просто отрисовывать параграфы положение каретки и так далее… Но тут подумал что не стоит продолжать дальше писать «свои» реализации потому что CodeArea перестает отвечать главной критерии выбора сократить время.

Тут как бы тяжело не было я принял решение поискать новый пакет удовлетворяющий моим требованиям, как минимум отрисовка точек остановки и возможности их редактирования. Забегая вперед точка остановки может быть установлена с условиям этой остановки это описано в протоколе DBGp (https://xdebug.org/docs/dbgp), возможность подсветить строку где остановился xdebug отрисовать значение переменных после окончания строки. Так же реализовать переход по дереву индекса кода по нажатию на используемый метод, функцию, класс и так далее. То есть редактор должен предоставлять возможность токинизации и самое главное предоставлять API для расширения возможности отображения токенов.

По этому пришлось принять очень страшное решение для меня разрабатывать свой компонент для JavaFX как говориться с девочками и кофем. В общем полчаса работы с гуглом и наметился план расширяем свой класс от javafx.scene.control.Control определяем метод createDefaultSkin(). Дальше создаем класс обложки (это наше представление) наследуем его от класса SkinBase<T> где T это класс нашего нового контрола. В общем были определены минимальные поля нужные для компонента их всего 2-ва lines и text. Один хранит оригинальный текст, другая линии (по сути параграфы). lines это ObservableList выбрал его так как можно отслеживать изменения в списке и по этим изменениям уже синхронизировать с нашей обложкой. Параметр text был выбран StringProperty по этой же причине и реализованы соответствующие обработчики изменения данных параметров.

А далее началось самое интересное, так как это просто Control, то что бы нам добавлять туда текст с клавиатуры необходимо:

  1. Обработка нажатия клавиш

  2. Каретка от ее рендера до положение в строке и между буквами

  3. Перемещение каретки с помощью стрелок на клавиатуре (влево, вправо, вниз, вверх)

  4. Установка каретки с помощью клика мышки

  5. Вставка текста в позицию каретки

Так вот, если каретку перемещать влево и в право это понятно соответствует caretPosition +- 1 влево отнимаем а в право прибавляем соответственно. А вот перемещение каретки в верх и вниз это уже математика, хотя с первого взгляда казалось бы формула очень простая определяем текущую строку где находиться каретка, очень просто вычисляем строку в которой находиться каретка, если надо в верх отнимаем длину предыдущей строке и там уже определяем есть ли вообще символ на этой строке если нет то каретку к последнему символу устанавливаем. В низ подобная логика просто прибавляем к каретки длину следующей строки и она устанавливается соответственно на место где она находиться на текущем месте.

Вычисление установки каретки с погрешностью в 5 пикселей при клике на строку. Хотел с начала повесить клик на сам Pane и Text внутри рендера строки но не получилось, потому как Control перехватывал клик мыши, по этому пришлось обрабатывать выше и вычислять собственно довольно просто строку с помощью mouseEvent.getY()/18 где 18 средняя ширина строки, почему то пустые строки получились после рендера 16 остальные 18 пикселей, по этому у меня есть параметр кэшь которые вычисляет один раз высоту строк при рендере и записывает округленное среднее значение (пока не особо правильно, надо бы вычислять для каждой строки но в целом работает быстро и погрешность приемлемая). Погрешность в несколько пикселей а точнее примерно 30%+- от высоты строки просто вычислена по остатку от деления если он больше 0.8 то берем следующую строку. А вот вычислить позицию где именно отрисовать каретку после какого символа? Уже сложнее, по этому там магия вычисления относительно Bounds символов перед кареткой, проблем особо нету, берем ширину Gater’a вычитаем его из mouseEvent.getX(), получаем 0-ю позицию в строке, далее что бы было быстрее просто берем полностью длину строки взяв Bounds всех элементов в HBox (Тут рендерятся все токены (да пришлось разбивать код на токены что бы отрисовать и сделать подсветку сразу) в виде элементов Text()). И если ширина всех элементов меньше клика мыши то просто ставим каретку в положение последнего символа. Но если это не так, тогда я копирую объект Text оригинальной строки туда устанавливается такой же шрифт, размер и так далее, потом каждый символ передается в этот объект от него уже берем Bounds для символа и продолжаем складывать пока значение аккумулятора не станет меньше чем X указателя мыши…

На самом деле для меня как для начинающего в Java, layout (слои) в JavaFX довольно сложны так как не всегда отображение слоев и элементов проходит в один шаг, обычно окно от маленького к большому отображается потом начинают отрисовываться объекты слоев на сцене чем меняют размер родительских элементов. Так что приходиться идти на ухищрения искать некое самое большое значение размера к примеру, если надо открепить от layout, поставив layoutManaged(false) то вычисление всех размеров ложатся на твою программу и по этому приходиться вычислять размеры дочерних элементов и потом уже делать resize() родительского элемента…

В целом подводя итоги я уже так отвык от математики работая с PHP что с Java я чувствую невероятное удовольствие с расчетами и математикой… Сейчас у меня следующая крупная задача, разработка валидатора кода, хотя бы минимального, а после уже можно интегрировать и PHPStan используя настройки редактора. В следующих статьях буду более подробно описывать свой экспириенс.

ссылка на оригинал статьи https://habr.com/ru/articles/1041924/