Выражения
sdk.expressions — то, чем расширение обучает редактор своему языку выражений. Граница жёсткая: расширение отвечает на вопросы о тексте, платформа рисует всё до пикселя — выпадающее меню, группы, клавиатуру, проверку под полем. Ни один компонент не пересекает границу, только сериализуемые данные.
Объявите провайдер в contributes.expressionProviders, затем приложите реализацию при активации:
registerProvider(id: string, provider: ISdkExpressionProvider): IDisposable
registerOptionsProvider(key: string, provider: SdkOptionsProvider): IDisposable
id должен совпадать с id из манифеста.
Контракт провайдера
interface ISdkExpressionProvider {
suggest?(request: ExpressionSuggestRequest): Promise<ExpressionCandidate[]> | ExpressionCandidate[]
diagnose?(request: ExpressionSuggestRequest): Promise<ExpressionDiagnostic[]> | ExpressionDiagnostic[]
resolveRef?(text: string, index: ExpressionRequestIndex): Promise<ExpressionRef | undefined> | ExpressionRef | undefined
}
Все методы необязательны: платформа объявляет только реализованные вами методы, а остальные пропускает.
Запрос
interface ExpressionSuggestRequest {
readonly text: string // что сейчас в поле
readonly caret: number // где каретка
readonly mode: 'value' | 'expression'
readonly index: ExpressionRequestIndex
readonly nodeId?: string
readonly parameterKey?: string
readonly portKey?: string
}
Граф перемещается внутри запроса — провайдер обратно за данными не ходит:
interface ExpressionRequestIndex {
readonly nodes: readonly ExpressionNodeIndexEntry[]
readonly secrets: readonly string[] // только имена, значений здесь нет
readonly selfNodeId?: string
}
interface ExpressionNodeIndexEntry {
readonly id: string
readonly name: string
readonly type?: string
readonly icon?: { name: string; color?: string }
readonly outputs: readonly {
readonly key: string
readonly dataType: string
readonly description?: string
readonly schema?: unknown // вложенная структура, если node её объявила
}[]
readonly upstream: boolean // от неё в текущую node уже идёт edge
readonly parentName?: string
}
Платформа собирает и кэширует индекс, обновляет его по событиям графа и выкидывает из него группы.
Кандидаты
interface ExpressionCandidate {
readonly kind: 'node' | 'key' | 'namespace' | 'secret'
readonly label: string
readonly detail?: string // показывается справа в строке
readonly dataType?: string // красит строку по палитре портов
readonly insertText: string
readonly replace: readonly [number, number] // диапазон в `text`, который заменяем
readonly docs?: string
readonly examples?: readonly string[]
readonly sort?: number
}
replace и определяет поведение каретки: верните точный диапазон, который должен заменить ваш insertText. Чтобы дополнить, а не заменить уже введённое, верните пустой диапазон в позиции каретки — так после ввода полного ключа предлагаются .поле и [0].
Два соглашения, на которые опирается платформа: строки kind: 'node' группируются по upstream, а kind: 'secret' | 'namespace' уходят в группу секретов.
Диагностика
interface ExpressionDiagnostic {
readonly severity: 'hint' | 'warning' | 'error'
readonly message: string
readonly detail?: string
readonly range?: readonly [number, number]
readonly replacement?: string // «возможно, вы имели в виду…»
}
Диагностика рисуется под полем и живёт до следующего ввода — в реестр проблем она не попадает.
Разрешение ссылки
interface ExpressionRef {
readonly nodeId: string
readonly portKey: string
readonly tail?: string // остаток пути: "[0].customer.email"
}
Понять, на что указывает текст, может только грамматика — поэтому за resolveRef отвечает расширение. За что редактор сделает с ответом (нарисует зависимость ребром) отвечает платформа.
Вызовами управляет платформа
suggest и diagnose вызываются на каждое движение каретки, поэтому платформа требует, чтобы они были лёгкими и безопасными:
- вызовы выполняются с задержкой, а устаревшие отбрасываются — запоздавший ответ никогда не перезаписывает более свежий;
- внутрь передаётся
abortSignal, а вызов, не ответивший за 1500 мс, снимается по таймауту; - упавший провайдер попадает в лог, а не в интерфейс: поле просто останется без подсказок.
Старайтесь, чтобы работа внутри провайдера оставалась «чистой» и синхронной — например, при разборе текста или обходе индекса. Это критически важный участок кода, задействуемый непосредственно в процессе ввода текста пользователем.
Провайдеры вариантов
Параметр может заполнять свой выпадающий список не статикой, а ответом расширения. Нода объявляет ключ провайдера в своей схеме данных, расширение отдаёт варианты:
sdk.expressions.registerOptionsProvider('list-tables', async ({ values }) => {
const database = typeof values.database === 'string' ? values.database : undefined
return (await tablesOf(database)).map((name) => ({ value: name, label: name }))
})
В values лежат текущие значения параметров ноды, поэтому список может зависеть от соседнего поля, например, если выбрать базу, список таблиц перестроится.
Пример
export async function activate(context: ExtensionContext) {
context.subscriptions.push(
sdk.expressions.registerProvider('example', {
suggest: (request) => {
const context = contextAt(request.text, request.caret, request.mode)
if (context.at === 'head') {
return request.index.nodes
.filter((node) => node.id !== request.index.selfNodeId)
.map((node) => ({
kind: 'node' as const,
label: node.name,
insertText: `${node.name}.`,
replace: context.replace,
sort: node.upstream ? 0 : 1,
}))
}
return keysOf(request.index, context)
},
}),
)
}
Полный рабочий провайдер — грамматика, подсказки, диагностика, разрешение ссылок — лежит в extensions/example-workflow.
Смотрите также: выражения в интерфейсе, setNodeInputsResolved.