monaco-yaml
Version:
YAML plugin for the Monaco Editor
684 lines (599 loc) • 17.5 kB
text/typescript
import type { editor, IDisposable, MonacoEditor } from 'monaco-types'
import type { CompletionItemKind, Diagnostic } from 'vscode-languageserver-types'
import type { YAMLWorker } from './yaml.worker.js'
import {
fromCodeActionTriggerType,
fromFormattingOptions,
fromPosition,
fromRange,
toCodeAction,
toCodeLens,
toCompletionList,
toDocumentSymbol,
toFoldingRange,
toHover,
toLink,
toLocationLink,
toMarkerData,
toRange,
toSelectionRanges,
toTextEdit,
toWorkspaceEdit
} from 'monaco-languageserver-types'
import { registerMarkerDataProvider } from 'monaco-marker-data-provider'
import { createWorkerManager } from 'monaco-worker-manager'
export interface JSONSchema {
id?: string
$id?: string
$schema?: string
url?: string
type?: string | string[]
title?: string
closestTitle?: string
versions?: Record<string, string>
default?: unknown
definitions?: Record<string, JSONSchema>
description?: string
properties?: Record<string, boolean | JSONSchema>
patternProperties?: Record<string, boolean | JSONSchema>
additionalProperties?: boolean | JSONSchema
minProperties?: number
maxProperties?: number
dependencies?: Record<string, boolean | JSONSchema | string[]>
items?: (boolean | JSONSchema)[] | boolean | JSONSchema
minItems?: number
maxItems?: number
uniqueItems?: boolean
additionalItems?: boolean | JSONSchema
pattern?: string
minLength?: number
maxLength?: number
minimum?: number
maximum?: number
exclusiveMinimum?: boolean | number
exclusiveMaximum?: boolean | number
multipleOf?: number
required?: string[]
$ref?: string
anyOf?: (boolean | JSONSchema)[]
allOf?: (boolean | JSONSchema)[]
oneOf?: (boolean | JSONSchema)[]
not?: boolean | JSONSchema
enum?: unknown[]
format?: string
const?: unknown
contains?: boolean | JSONSchema
propertyNames?: boolean | JSONSchema
examples?: unknown[]
$comment?: string
if?: boolean | JSONSchema
then?: boolean | JSONSchema
else?: boolean | JSONSchema
defaultSnippets?: {
label?: string
description?: string
markdownDescription?: string
type?: string
suggestionKind?: CompletionItemKind
sortText?: string
body?: unknown
bodyText?: string
}[]
errorMessage?: string
patternErrorMessage?: string
deprecationMessage?: string
enumDescriptions?: string[]
markdownEnumDescriptions?: string[]
markdownDescription?: string
doNotSuggest?: boolean
allowComments?: boolean
schemaSequence?: JSONSchema[]
filePatternAssociation?: string
}
export interface SchemasSettings {
/**
* A `Uri` file match which will trigger the schema validation. This may be a glob or an exact
* path.
*
* @example '.gitlab-ci.yml'
* @example 'file://**\/.github/actions/*.yaml'
*/
fileMatch: string[]
/**
* The JSON schema which will be used for validation. If not specified, it will be downloaded from
* `uri`.
*/
schema?: JSONSchema
/**
* The source URI of the JSON schema. The JSON schema will be downloaded from here if no schema
* was supplied. It will also be displayed as the source in hover tooltips.
*/
uri: string
}
export interface FormatterOptions {
/**
* Print spaces between brackets in objects.
*
* @default true
*/
readonly bracketSpacing?: boolean
/**
* Enable/disable default YAML formatter.
*
* @default true
*/
readonly enable?: boolean
/**
* Specify the line length that the printer will wrap on.
*
* @default 80
*/
readonly printWidth?: number
/**
* - `always`: wrap prose if it exceeds the print width.
* - `never`: never wrap the prose,
* - `preserve`: wrap prose as-is.
*
* @default `'preserve'`
*/
readonly proseWrap?: 'always' | 'never' | 'preserve'
/**
* @default false
*/
readonly singleQuote?: boolean
/**
* Specify if trailing commas should be used in JSON-like segments of the YAML.
*
* @default true
*/
readonly trailingComma?: boolean
}
export interface MonacoYamlOptions {
/**
* If set, enable code lens.
*
* @default false
*/
readonly codeLens?: boolean
/**
* If set, enable schema based autocompletion.
*
* @default true
*/
readonly completion?: boolean
/**
* A list of custom tags.
*
* @default []
*/
readonly customTags?: string[]
/**
* Globally set `additionalProperties` to false if `additionalProperties` is not set and if
* `schema.type` is `object`. So if is true, no extra properties are allowed inside yaml.
*
* @default false
*/
readonly disableAdditionalProperties?: boolean
/**
* Disable adding not required properties with default values into completion text.
*
* @default false
*/
readonly disableDefaultProperties?: boolean
/**
* If set, the schema service will load schema content on-demand.
*
* @default false
*/
readonly enableSchemaRequest?: boolean
/**
* Control the use of flow mappings.
*
* @default 'allow'
*/
readonly flowMapping?: 'allow' | 'forbid'
/**
* Control the use of flow sequences.
*
* @default 'allow'
*/
readonly flowSequence?: 'allow' | 'forbid'
/**
* Formatting options.
*/
readonly format?: FormatterOptions
/**
* If set, enable hover tips based the JSON schema.
*
* @default true
*/
readonly hover?: boolean
/**
* Enable/disable hover feature for anchors
*
* @default true
*/
readonly hoverAnchor?: boolean
/**
* Enable/disable showing the schema source in hover tooltips.
*
* @default true
*/
readonly hoverSchemaSource?: boolean
/**
* Default indentation size
*
* @default ' '
*/
readonly indentation?: string
/**
* If true, a different diffing algorithm is used to generate error messages.
*
* @default false
*/
readonly isKubernetes?: boolean
/**
* If set enforce alphabetical ordering of keys in mappings.
*
* @default false
*/
readonly keyOrdering?: boolean
/**
* If true, the user must select some parent skeleton first before autocompletion starts to
* suggest the rest of the properties. When yaml object is not empty, autocompletion ignores
* this setting and returns all properties and skeletons.
*
* @default false
*/
readonly parentSkeletonSelectedFirst?: boolean
/**
* A list of known schemas and/or associations of schemas to file names.
*
* @default []
*/
readonly schemas?: SchemasSettings[]
/**
* If set, the validator will be enabled and perform syntax validation as well as schema
* based validation.
*
* @default true
*/
readonly validate?: boolean
/**
* Default yaml lang version.
*
* @default '1.2'
*/
readonly yamlVersion?: '1.1' | '1.2'
}
export interface MonacoYaml extends IDisposable {
/**
* Recondigure `monaco-yaml`.
*/
update: (options: MonacoYamlOptions) => Promise<undefined>
/**
* Get the current configuration of `monaco-yaml`.
*/
getOptions: () => MonacoYamlOptions
}
/**
* Merge two options objects.
*
* @param newOptions
* The new options to merge into with old options.
* @param oldOptions
* The old options to merge the new options with.
* @returns
* The options merged with the previous options.
*/
function mergeOptions(
newOptions: MonacoYamlOptions,
oldOptions: MonacoYamlOptions
): MonacoYamlOptions {
return {
...oldOptions,
...newOptions,
format: {
...oldOptions.format,
...newOptions.format
}
}
}
interface Togglable<T> {
get: () => T | undefined
toggle: (enabled: boolean | undefined) => undefined
}
function togglable<T extends IDisposable>(create: () => T): Togglable<T> {
let disposable: T | undefined
return {
get(): T | undefined {
return disposable
},
toggle(enabled) {
if (enabled) {
disposable ??= create()
} else {
disposable?.dispose()
disposable = undefined
}
}
}
}
/**
* Configure `monaco-yaml`.
*
* > **Note**: There may only be one configured instance of `monaco-yaml` at a time.
*
* @param monaco
* The Monaco editor module. Typically you get this by importing `monaco-editor`. Third party
* integrations often expose it as the global `monaco` variable instead.
* @param options
* Options to configure `monaco-yaml`
* @returns
* A disposable object that can be used to update `monaco-yaml`
*/
export function configureMonacoYaml(
monaco: MonacoEditor,
options: MonacoYamlOptions = {}
): MonacoYaml {
let createData = mergeOptions(options, {
codeLens: false,
completion: true,
customTags: [],
disableAdditionalProperties: false,
disableDefaultProperties: false,
enableSchemaRequest: false,
flowMapping: 'allow',
flowSequence: 'allow',
format: {
bracketSpacing: true,
enable: true,
printWidth: 80,
proseWrap: 'preserve',
singleQuote: false,
trailingComma: true
},
hover: true,
hoverAnchor: true,
indentation: ' ',
isKubernetes: false,
keyOrdering: false,
parentSkeletonSelectedFirst: false,
schemas: [],
validate: true,
yamlVersion: '1.2'
})
monaco.languages.register({
id: 'yaml',
extensions: ['.yaml', '.yml'],
aliases: ['YAML', 'yaml', 'YML', 'yml'],
mimetypes: ['application/x-yaml']
})
const workerManager = createWorkerManager<YAMLWorker, MonacoYamlOptions>(monaco, {
label: 'yaml',
moduleId: 'monaco-yaml/yaml.worker',
createData
})
const diagnosticMap = new WeakMap<editor.ITextModel, Diagnostic[] | undefined>()
const codeLensProvider = togglable(() =>
monaco.languages.registerCodeLensProvider('yaml', {
async provideCodeLenses(model) {
const worker = await workerManager.getWorker(model.uri)
const lenses = await worker.getCodeLens(String(model.uri))
if (lenses) {
return {
lenses: lenses.map(toCodeLens),
dispose() {
// This is required by the TypeScript interface, but it’s not implemented.
}
}
}
}
})
)
const completionProvider = togglable(() =>
monaco.languages.registerCompletionItemProvider('yaml', {
triggerCharacters: [' ', ':'],
async provideCompletionItems(model, position) {
const wordInfo = model.getWordUntilPosition(position)
const worker = await workerManager.getWorker(model.uri)
const info = await worker.doComplete(String(model.uri), fromPosition(position))
if (info) {
return toCompletionList(info, {
range: {
startLineNumber: position.lineNumber,
startColumn: wordInfo.startColumn,
endLineNumber: position.lineNumber,
endColumn: wordInfo.endColumn
}
})
}
}
})
)
const formatProvider = togglable(() =>
monaco.languages.registerDocumentFormattingEditProvider('yaml', {
displayName: 'yaml',
async provideDocumentFormattingEdits(model) {
const worker = await workerManager.getWorker(model.uri)
const edits = await worker.format(String(model.uri))
return edits?.map(toTextEdit)
}
})
)
const hoverProvider = togglable(() =>
monaco.languages.registerHoverProvider('yaml', {
async provideHover(model, position) {
const worker = await workerManager.getWorker(model.uri)
const info = await worker.doHover(String(model.uri), fromPosition(position))
if (info) {
return toHover(info)
}
}
})
)
const validationProvider = togglable(() =>
registerMarkerDataProvider(monaco, 'yaml', {
owner: 'yaml',
async provideMarkerData(model) {
const worker = await workerManager.getWorker(model.uri)
const diagnostics = await worker.doValidation(String(model.uri))
diagnosticMap.set(model, diagnostics)
return diagnostics?.map(toMarkerData)
},
async doReset(model) {
const worker = await workerManager.getWorker(model.uri)
await worker.resetSchema(String(model.uri))
}
})
)
function toggleAll(): undefined {
codeLensProvider.toggle(createData.codeLens)
completionProvider.toggle(createData.completion)
formatProvider.toggle(createData.format?.enable)
hoverProvider.toggle(createData.hover)
validationProvider.toggle(createData.validate)
}
toggleAll()
const disposables = [
workerManager,
monaco.languages.registerDefinitionProvider('yaml', {
async provideDefinition(model, position) {
const worker = await workerManager.getWorker(model.uri)
const locationLinks = await worker.doDefinition(String(model.uri), fromPosition(position))
return locationLinks?.map(toLocationLink)
}
}),
monaco.languages.registerDocumentSymbolProvider('yaml', {
displayName: 'yaml',
async provideDocumentSymbols(model) {
const worker = await workerManager.getWorker(model.uri)
const items = await worker.findDocumentSymbols(String(model.uri))
return items?.map(toDocumentSymbol)
}
}),
monaco.languages.registerLinkProvider('yaml', {
async provideLinks(model) {
const worker = await workerManager.getWorker(model.uri)
const links = await worker.findLinks(String(model.uri))
if (links) {
return {
links: links.map(toLink)
}
}
}
}),
monaco.languages.registerCodeActionProvider('yaml', {
async provideCodeActions(model, range, context) {
const worker = await workerManager.getWorker(model.uri)
const codeActions = await worker.getCodeAction(String(model.uri), fromRange(range), {
diagnostics:
diagnosticMap
.get(model)
?.filter((diagnostic) => range.intersectRanges(toRange(diagnostic.range))) || [],
only: context.only ? [context.only] : undefined,
triggerKind: fromCodeActionTriggerType(context.trigger)
})
if (codeActions) {
return {
actions: codeActions.map(toCodeAction),
dispose() {
// This is required by the TypeScript interface, but it’s not implemented.
}
}
}
}
}),
monaco.languages.registerFoldingRangeProvider('yaml', {
async provideFoldingRanges(model) {
const worker = await workerManager.getWorker(model.uri)
const foldingRanges = await worker.getFoldingRanges(String(model.uri))
return foldingRanges?.map(toFoldingRange)
}
}),
monaco.languages.setLanguageConfiguration('yaml', {
comments: {
lineComment: '#'
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
]
}),
monaco.languages.registerOnTypeFormattingEditProvider('yaml', {
autoFormatTriggerCharacters: ['\n'],
async provideOnTypeFormattingEdits(model, position, ch, formattingOptions) {
const worker = await workerManager.getWorker(model.uri)
const edits = await worker.doDocumentOnTypeFormatting(
String(model.uri),
fromPosition(position),
ch,
fromFormattingOptions(formattingOptions)
)
return edits?.map(toTextEdit)
}
}),
monaco.languages.registerRenameProvider('yaml', {
async provideRenameEdits(model, position, newName) {
const worker = await workerManager.getWorker(model.uri)
const edit = await worker.doRename(String(model.uri), fromPosition(position), newName)
if (edit) {
return toWorkspaceEdit(edit)
}
},
async resolveRenameLocation(model, position) {
const worker = await workerManager.getWorker(model.uri)
const range = await worker.prepareRename(String(model.uri), fromPosition(position))
if (range) {
return { range: toRange(range), text: '' }
}
}
}),
monaco.languages.registerSelectionRangeProvider('yaml', {
async provideSelectionRanges(model, positions) {
const worker = await workerManager.getWorker(model.uri)
const selectionRanges = await worker.getSelectionRanges(
String(model.uri),
positions.map(fromPosition)
)
return selectionRanges?.map(toSelectionRanges)
}
})
]
return {
dispose() {
codeLensProvider.get()?.dispose()
completionProvider.get()?.dispose()
formatProvider.get()?.dispose()
hoverProvider.get()?.dispose()
validationProvider.get()?.dispose()
for (const disposable of disposables) {
disposable.dispose()
}
},
async update(newOptions) {
createData = mergeOptions(newOptions, createData)
workerManager.updateCreateData(createData)
toggleAll()
await validationProvider.get()?.revalidate()
},
getOptions() {
return createData
}
}
}