Идея
Основная идея по сути в том, чтобы реализовать некое подобие Language Context Protocol (LCP), но с куда более широкими возможностями. Вы можете создавать свой автокомплит, свои GoTo, свои шаблоны кода и экшны прямо внутри IDE на том языке, на котором вы пишете свой проект.
Введение
Большинство скриптовых языков имеют собственные фреймворки/CMS (например Symfony, Drupal в PHP, Nest, Next в JS/TS). И существуют плагины для поддержки некоторых специфических возможностей фреймворков/CMS, например symfony plugin или drupal plugin. Основная проблема этих плагинов в том, что они не зависят от проекта, и вам нужно устанавливать множество плагинов для поддержки разных функций. А что если у вас есть собственные кастомные возможности в вашем проекте и вы хотите обрабатывать ссылки или автодополнение для них?
Как это работает?
Прежде всего, чтобы использовать плагин, вы должны включить его в настройках и указать путь к исполняемому скрипту, который будет выполнен для автодополнения или поиска ссылок для GoTo.
Каждый раз, когда вы пытаетесь дополнить выражение (например, ctrl + space), плагин создаёт очень простое представление текущего сфокусированного PSI элемента, называемого PSA Context, затем JSON-кодирует его, записывает во временный файл (из-за некоторых ограничений длины аргументов/переменных окружения) и передаёт его в указанный исполняемый файл.
Например, если вы попытаетесь автодополнить следующий PHP код:
<?php function myFunc() { $l = ''; // ^ курсор здесь }
то вы получите следующий JSON в пути к файлу, переданном из переменной окружения PSA_CONTEXT:
Развернуть
{ "elementType": "right single quote", "elementName": null, "elementFqn": null, "text": "'", "parent": { "elementType": "String", "elementName": null, "elementFqn": null, "text": "''", "parent": { "elementType": "Assignment expression", "elementName": null, "elementFqn": null, "text": "$l = ''", "parent": { "elementType": "Statement", "elementName": null, "elementFqn": null, "text": "$l = '';", "parent": { "elementType": "Group statement", "elementName": null, "elementFqn": null, "text": "{\n $l = '';\n}", "parent": { "elementType": "FUNCTION", "elementName": null, "elementFqn": null, "text": "function myFunc() {\n $l = '';\n}", "parent": { "elementType": "PsiElement(Non Lazy Group statement)", "elementName": null, "elementFqn": null, "text": "<?php\n\nfunction myFunc() {\n $l = '';\n}", "parent": { "elementType": "php.FILE", "elementName": null, "elementFqn": null, "text": "<?php\n\nfunction myFunc() {\n $l = '';\n}\n", "parent": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null }, "prev": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null }, "next": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null } }, "prev": null, "next": { "elementType": "PLAIN_TEXT_FILE", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null } }, "prev": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null }, "next": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null } }, "prev": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null }, "next": { "elementType": "<null>", "elementName": null, "elementFqn": null, "text": "", "parent": null, "prev": null, "next": null } }, "prev": null, "next": null }, "prev": { "elementType": "WHITE_SPACE", "elementName": null, "elementFqn": null, "text": "\n", "parent": null, "prev": null, "next": null }, "next": null }, "prev": null, "next": { "elementType": "WHITE_SPACE", "elementName": null, "elementFqn": null, "text": "\n\n", "parent": null, "prev": null, "next": null } }, "prev": null, "next": { "elementType": "WHITE_SPACE", "elementName": null, "elementFqn": null, "text": " ", "parent": null, "prev": null, "next": null } }, "prev": { "elementType": "WHITE_SPACE", "elementName": null, "elementFqn": null, "text": "\n", "parent": null, "prev": null, "next": null }, "next": { "elementType": "WHITE_SPACE", "elementName": null, "elementFqn": null, "text": "\n ", "parent": null, "prev": null, "next": null } }, "prev": { "elementType": "semicolon", "elementName": null, "elementFqn": null, "text": ";", "parent": null, "prev": null, "next": null }, "next": null }, "prev": null, "next": { "elementType": "left single quote", "elementName": null, "elementFqn": null, "text": "'", "parent": null, "prev": null, "next": null } }, "prev": null, "next": { "elementType": "left single quote", "elementName": null, "elementFqn": null, "text": "'", "parent": null, "prev": null, "next": null } }
В выводе выше параметры
optionsиtextRangeопущены для уменьшения размера.
Документация
Swagger
Вы можете проверить Swagger UI, сгенерированный моделями внутри классов плагина. Он может быть использован для лучшего понимания того, как работает плагин, а также для генерации DTO классов для PSA.
Смотрите документацию Swagger здесь.
Все методы, описанные в документации Swagger, являются “фейковыми” методами и перечислены только для описания структуры вызовов, которые будут выполнены для вашего PSA скрипта.
Также ознакомьтесь с документацией OpenAPI Generator для получения дополнительной информации о генерации DTO классов для PSA.
Автодополнение и GoTo
Как уже упоминалось во введении, плагин отправляет JSON-кодированное PSI дерево в исполняемый файл.
Вот полный список переменных окружения, передаваемых в исполняемый файл:
PSA_CONTEXT- путь к файлу, содержащему JSON-кодированный PSI контекст.PSA_TYPE- может быть либоCompletion, либоGoTo. Тип выполнения.PSA_LANGUAGE- язык, вызвавший автодополнение/разрешение ссылки (PHP,JS, …).PSA_DEBUG-1, если отладка включена в настройках плагина, и0в противном случае.PSA_OFFSET- показывает позицию курсора внутри текущего элемента в редакторе.
Таким образом, вы можете разобрать JSON и проанализировать его для своих нужд. Этот JSON имеет древовидную структуру, и каждый элемент будет иметь следующую структуру:
Развернуть
{ "elementType": "строка", "elementName": "строка | null", "elementFqn": "строка | null", "options": { "optionName": "optionValue" }, "text": "строка", "parent": "дополнительный элемент дерева", "prev": "дополнительный элемент дерева", "next": "дополнительный элемент дерева", "textRange": { "startOffset": "целое число, позиция начала PSI элемента в файле", "endOffset": "целое число, позиция конца PSI элемента в файле" } }
Анализируя элемент и его родителей + некоторые опции, вы можете найти, как проверить, что курсор находится на элементе, который может быть автодополнен.
В результате ваш скрипт должен вернуть:
Массив завершений в случае, если
PSA_TYPEравенCompletion.Массив завершений с одним элементом (этот элемент должен содержать ссылку) в случае, если
PSA_TYPEравенGoTo.Опционально, вы можете передать массив уведомлений, которые будут показаны IDEA. Полезно для отладки.
Опционально, вы можете вернуть массив типов элементов для фильтрации GoTo по причинам производительности. Для получения дополнительной информации прочитайте раздел производительность.
Полная структура результирующего JSON будет описана ниже:
Развернуть
{ "completions": [ { "text": "строка, текст завершения", "link": "строка, требуется только в случае `PSA_TYPE=GoTo`, абсолютная/относительная ссылка на файл в формате FileName.ext[:line_number][:position]", "bold": "boolean, должно ли завершение быть жирным.", "priority": "число, опционально. Используется для упорядочивания элементов в автодополнении. Если `bold` равен `true` и `priority` не указан, то значение по умолчанию будет 100.", "type": "строка, тип, который будет показан серым текстом справа от завершения." } ], "notifications": [ { "type": "строка, может быть либо `info`, `error` или `warning`.", "text": "строка, текст уведомления." } ] }
И полный рабочий пример:
Развернуть
{ "completions": [ { "text": "My Completion", "link": "/path/to/file.php:123:123", "bold": false, "priority": 123, "type": "MyType" } ], "notifications": [ { "type": "info", "text": "Hello from my custom autocomplete!" } ] }
В случае, если ваш исполняемый файл ответит JSON выше, результат автокомплита будет выглядеть так:

И будет показано следующее уведомление:

Для рабочих примеров на разных языках ознакомьтесь с папкой examples.
В случае, если
PSA_TYPEравенGoTo, вы должны вернуть только одно завершение со ссылкой на ссылку.
Шаблоны кода
Большинство языков предоставляют некоторые общие шаблоны файлов, такие как PHP Class в PHP или TypeScript File в TypeScript. Плагин позволяет вам создавать пользовательские шаблоны файлов, которые будут иметь переменные, переданные из формы. Для поддержки шаблонов файлов вы должны указать все поддерживаемые шаблоны в вашем исполняемом скрипте в разделе templates. Ознакомьтесь с разделом информация об автодополнении для получения дополнительной информации.
Шаблон одного файла
В случае, если вам нужно создать шаблон одного файла, в запросе info ваш JSON должен содержать шаблон со следующими полями:
type- строка, обязательно. Поддерживаютсяsingle_fileилиmultiple_file. Для шаблона одного файла передайтеsingle_fileв качестве значения.name- строка, обязательно. Имя шаблона для ссылки. Будет передано вPSA_CONTEXTво время генерации шаблона.title- строка, обязательно. Заголовок шаблона. Этот текст будет показан в IDE.path_regex- строка, опционально. Регулярное выражение для фильтрации путей, где будет доступно действие создания шаблона.fields- массив объектов со следующей структурой:name- строка, обязательно. Имя поля формы. Будет передано вPSA_CONTEXTво время генерации шаблона.title- строка, обязательно. Заголовок поля, который будет отображаться в форме.type- строка, обязательно. Допустимые значения:Text,Checkbox,Select,Collection,RichText. Тип поля формы.focused- boolean, опционально. Установите в true для поля, которое вы хотите сфокусировать при открытии диалога создания шаблона.options- массив строк.Требуется, если
typeравенSelect. Массив опций выбора.Требуется, если
typeравенRichText. Массив завершений.
Например, для некоторого простого PHP класса вы можете использовать следующую структуру:
Развернуть
{ "templates": [ { "type": "single_file", "name": "my_awesome_template", "title": "My Awesome Template", "path_regex": "^\/src\/[^\/]\/$", "fields": [ { "name": "className", "title": "Class Name", "type": "Text", "options": [] }, { "name": "abstract", "title": "Is Abstract", "type": "Checkbox", "options": [] }, { "name": "comment", "title": "Comment", "type": "Select", "options": ["Option A", "Option B", "Option C"] }, { "name": "richText", "title": "Rich Text with Completion", "type": "RichText", "options": ["Completion A", "Completion B", "Completion C"] }, { "name": "collection", "title": "Collection of text fields", "type": "Collection", "options": [] } ] } ] }
И в случае, если ваш скрипт автодополнения вернёт шаблон, как выше, у вас будет следующая опция меню для генерации нового файла из шаблона в любом пути в структуре проекта (путь может быть отфильтрован опцией path_regex):
Развернуть

Когда вы нажмёте на действие, вы увидите следующую форму:
Развернуть

В этой форме вы можете изменить любую из ваших переменных, описанных выше. Предварительный просмотр обновляется автоматически после изменения значения любой переменной.
После нажатия кнопки OK файл будет сгенерирован в папке, где вы инициировали действие.
После открытия формы, после изменения любой переменной и нажатия OK, плагин отправит запрос на генерацию кода вашему скрипту автодополнения со следующими переменными:
PSA_TYPE- всегда будетGenerateFileFromTemplatePSA_CONTEXT- как и при завершении, это путь к файлу с JSON следующей структуры:{ "templateName": "строка, имя шаблона для генерации.", "actionPath": "строка, относительный путь от корня проекта, где было инициировано действие.", "formFields": { "name": "value" }, "originatorFieldName": "строка, опционально. Если регенерация шаблона была вызвана изменением какого-либо поля, эта опция будет содержать имя этого поля." }
formFields- будет JSON объектом, где каждый ключ - имя поля, а значение - значение поля формы.
В результате ваш скрипт должен вернуть простой JSON объект со следующими полями:
{ "file_name": "строка, обязательно. Имя файла вновь сгенерированного файла.", "content": "строка, обязательно. Содержимое файла.", "form_fields": { "{field_name}": { "options": "Массив строк, опционально. Здесь вы можете переопределить массив завершений `RichText`.", "value": "Строка, опционально. Здесь вы можете переопределить текущее значение любого поля формы, если необходимо." } } }
form_fields- опциональное поле. Каждое внутреннее значениеform_fieldsтакже опционально.
Некоторые примеры для PHP, JavaScript, TypeScript показаны в папке examples/README.md.
Действия редактора (Actions)
Иногда вам не нужно генерировать целый файл из шаблона, а скорее - обработать некоторую часть кода и заменить её прямо в редакторе. Для этих целей были созданы действия редактора.
Для поддержки действий редактора ваш ответ Info должен содержать поле editor_actions. Это должен быть массив со следующей структурой:
Развернуть
{ "editor_actions": [ { "name": "строка, обязательно. Имя действия, будет передано обратно в ваш скрипт при вызове", "title": "строка, обязательно. Заголовок действия для отображения в действиях IDE", "source": "строка, обязательно. Может быть либо 'editor', либо 'clipboard'. Источник данных. Либо выделенный текст, либо буфер обмена", "target": "строка, обязательно. Может быть либо 'editor', либо 'clipboard'. Цель данных. Либо заменить выделенный текст, либо скопировать результат в буфер обмена", "group_name": "строка, опционально. Добавляет подменю в меню PSA Actions", "path_regex": "строка, опционально. Регулярное выражение пути для фильтрации, где будет показано это действие" } ] }
Например, вы можете создать действие, которое будет преобразовывать JSON в PHP массив:
Развернуть
<?php $type = getenv('PSA_TYPE'); if ('Info' === $type) { echo json_encode([ 'supported_languages' => ['PHP'], 'editor_actions' => [ [ 'name' => 'jsonToPhpArray', 'title' => 'Convert JSON -> PHP array (Copy to Clipboard)', 'source' => 'editor', 'target' => 'clipboard', 'group_name' => 'JSON', ], ], ]); exit(0); } $context = json_decode(file_get_contents(getenv('PSA_CONTEXT')), true); if ('PerformEditorAction' === $type && $context['action_name'] === 'jsonToPhpArray') { $data = json_decode($context['text'], true); echo var_export($data); }
И в случае, если ваш PSA скрипт вернёт значение, вы сможете вставить его в любое место вашего кода. Например, это полезно с действием Generate Pattern Model, так что вы запускаете это действие, копируете результат в некоторый временный JSON файл, удаляете всё ненужное, затем запускаете своё собственное действие для преобразования этого в PHP массив и вуаля:

и затем где-то в коде вашего PSA скрипта (перед вставкой):

и после вставки:

Бонус для PHP разработчиков и людей, использующий PHPStorm
Плагин добавляет опциональную поддержку рендеринга __toString при дебаге
Форматирование значений Xdebug (переопределение отображения __toString) При отладке с Xdebug, PSA может заменить компактное значение по умолчанию, показываемое для не-скалярных значений, используя ваш to_string_value_formatter из Info.
Как это работает:
PSA оборачивает отладочные значения PHP и для не-скалярных типов вычисляет небольшой PHP-сниппет в сессии отладки для получения короткого текстового представления.
Сниппет проходит от корневой переменной к текущему выбранному свойству/элементу массива с использованием reflection для приватных/защищённых свойств, затем вызывает ваш код как:
(function ($value) { YOUR_CODE_HERE })($current)YOUR_CODE_HERE— это в точности строка, которую вы возвращаете вto_string_value_formatter. Она должна использовать$valueи возвращать строку.
Пример кода форматтера:
return match (true) { is_array($value) => 'array(' . count($value) . ')', $value instanceof DateTimeInterface => $value->format(DATE_ATOM), is_object($value) => get_class($value), default => (string)$value, };
Безопасность и примечания:
Ошибки внутри форматтера подавляются, чтобы не сломать UI отладки; если включены отладочные уведомления PSA, ошибки вычисления будут показаны.
Форматтер запускается только если включено PHP-расширение и предоставлен
to_string_value_formatter.Плагин добавляет поддержку поиска вызовов функции с конкретным аргументом (если есть больше одно аргумента по умолчанию) Примерно так это выглядит

find usages by method parameter invocation Плагин добавляет поиск методов по всему дереву наследования (включая методы трейтов)
P.S. Это частичный перевод README.md файла. За дополнительными подробностями смотрите сам файл.
P.P.S. Этот текст был переведён с использованием ИИ, оригинал - всё тот же README.md файл (при написании оригинала ИИ не использовались).
