Сайт

Выражения

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.