Add JSON with interpolation language support

Introduce a new 'json-interpolation' language that extends JSON with
${...} variable interpolation syntax. Features include:

- Monarch tokenizer with nextEmbedded for JavaScript inside ${...}
- Variable context API for custom completion and hover providers
- Diagnostics filtering to ignore errors inside interpolations
- Full JSON language service integration (formatting, symbols, etc.)
- Support for JSONC-style comments and trailing commas

Usage:
```typescript
monaco.languages.jsonInterpolation.jsonInterpolationDefaults.setVariableContext({
  getVariables: () => [
    { name: 'env', type: 'string', value: 'production' }
  ]
});
```
This commit is contained in:
Claude 2025-12-09 19:46:12 +00:00
parent c619ef9a3d
commit 4540e05e5a
No known key found for this signature in database
6 changed files with 991 additions and 1 deletions

View file

@ -37,6 +37,10 @@ export default defineConfig(async (args) => {
__dirname,
'../../src/language/json/monaco.contribution.ts'
),
'language/json-interpolation/monaco.contribution': resolve(
__dirname,
'../../src/language/json-interpolation/monaco.contribution.ts'
),
'language/typescript/monaco.contribution': resolve(
__dirname,
'../../src/language/typescript/monaco.contribution.ts'

View file

@ -12,3 +12,4 @@ monacoApi.languages.css = monaco.css;
monacoApi.languages.html = monaco.html;
monacoApi.languages.typescript = monaco.typescript;
monacoApi.languages.json = monaco.json;
monacoApi.languages.jsonInterpolation = monaco.jsonInterpolation;

View file

@ -1,10 +1,11 @@
import * as css from '../../language/css/monaco.contribution';
import * as html from '../../language/html/monaco.contribution';
import * as json from '../../language/json/monaco.contribution';
import * as jsonInterpolation from '../../language/json-interpolation/monaco.contribution';
import * as typescript from '../../language/typescript/monaco.contribution';
import '../../basic-languages/monaco.contribution';
import * as lsp from '@vscode/monaco-lsp-client';
export * from 'monaco-editor-core';
export { createWebWorker, type IWebWorkerOptions } from '../../common/workers';
export { css, html, json, typescript, lsp };
export { css, html, json, jsonInterpolation, typescript, lsp };

View file

@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* JSON with Interpolation - Monarch Language Definition
* Supports ${...} interpolation with embedded JavaScript
*--------------------------------------------------------------------------------------------*/
import { languages } from 'monaco-editor-core';
export const conf: languages.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\[\{\]\}\:\"\,\s]+)/g,
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['${', '}']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '${', close: '}' }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '"', close: '"' }
],
folding: {
markers: {
start: /^\s*\/\/\s*#?region\b/,
end: /^\s*\/\/\s*#?endregion\b/
}
}
};
export const language = <languages.IMonarchLanguage>{
defaultToken: '',
tokenPostfix: '.json-interpolation',
// Escape sequences
escapes: /\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4})/,
// The main tokenizer
tokenizer: {
root: [
// Whitespace
[/\s+/, ''],
// Comments (JSONC style)
[/\/\/.*$/, 'comment'],
[/\/\*/, 'comment', '@comment'],
// Start of object or array
[/[{]/, 'delimiter.bracket', '@object'],
[/\[/, 'delimiter.array', '@array']
],
// Inside an object
object: [
// Whitespace
[/\s+/, ''],
// Comments
[/\/\/.*$/, 'comment'],
[/\/\*/, 'comment', '@comment'],
// Property name (key)
[/"/, 'string.key', '@propertyName'],
// Colon
[/:/, 'delimiter.colon'],
// Comma
[/,/, 'delimiter.comma'],
// Values
{ include: '@value' },
// End of object
[/\}/, 'delimiter.bracket', '@pop']
],
// Property name inside double quotes
propertyName: [
[/[^"\\]+/, 'string.key'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string.key', '@pop']
],
// Inside an array
array: [
// Whitespace
[/\s+/, ''],
// Comments
[/\/\/.*$/, 'comment'],
[/\/\*/, 'comment', '@comment'],
// Comma
[/,/, 'delimiter.comma'],
// Values
{ include: '@value' },
// End of array
[/\]/, 'delimiter.array', '@pop']
],
// JSON values
value: [
// String with interpolation support
[/"/, 'string.value', '@string'],
// Numbers
[/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/, 'number'],
// Keywords
[/true|false/, 'keyword'],
[/null/, 'keyword'],
// Nested object
[/\{/, 'delimiter.bracket', '@object'],
// Nested array
[/\[/, 'delimiter.array', '@array']
],
// String value with interpolation
string: [
// Interpolation start - switch to JavaScript
[
/\$\{/,
{
token: 'delimiter.bracket.interpolation',
next: '@interpolation',
nextEmbedded: 'javascript'
}
],
// Regular string content
[/[^"\\$]+/, 'string.value'],
// Escape sequences
[/@escapes/, 'string.escape'],
// Invalid escape
[/\\./, 'string.escape.invalid'],
// Dollar sign not followed by brace
[/\$(?!\{)/, 'string.value'],
// End of string
[/"/, 'string.value', '@pop']
],
// Inside ${...} interpolation - JavaScript is embedded here
interpolation: [
// End of interpolation - return to string
[
/\}/,
{
token: 'delimiter.bracket.interpolation',
next: '@pop',
nextEmbedded: '@pop'
}
]
],
// Block comment
comment: [
[/[^/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[/*]/, 'comment']
]
}
};

View file

@ -0,0 +1,502 @@
/*---------------------------------------------------------------------------------------------
* JSON with Interpolation - Mode Setup
* Registers all language providers including variable completion/hover
*--------------------------------------------------------------------------------------------*/
import { WorkerManager } from '../json/workerManager';
import type { JSONWorker } from '../json/jsonWorker';
import { LanguageServiceDefaults, VariableDefinition } from './monaco.contribution';
import * as languageFeatures from '../common/lspLanguageFeatures';
import { conf, language } from './jsonInterpolation';
import { Uri, IDisposable, languages, editor, Position, CancellationToken } from 'monaco-editor-core';
let worker: languageFeatures.WorkerAccessor<JSONWorker>;
export function getWorker(): Promise<(...uris: Uri[]) => Promise<JSONWorker>> {
return new Promise((resolve, reject) => {
if (!worker) {
return reject('JSON Interpolation not registered!');
}
resolve(worker);
});
}
// --- Custom Diagnostics Adapter that filters out interpolation syntax errors ---
class JSONInterpolationDiagnosticsAdapter implements IDisposable {
private readonly _disposables: IDisposable[] = [];
private readonly _listener: { [uri: string]: IDisposable } = Object.create(null);
constructor(
private readonly _languageId: string,
private readonly _worker: languageFeatures.WorkerAccessor<JSONWorker>,
defaults: LanguageServiceDefaults
) {
const onModelAdd = (model: editor.ITextModel): void => {
const modeId = model.getLanguageId();
if (modeId !== this._languageId) {
return;
}
let handle: number;
this._listener[model.uri.toString()] = model.onDidChangeContent(() => {
window.clearTimeout(handle);
handle = window.setTimeout(() => this._doValidate(model, modeId), 500);
});
this._doValidate(model, modeId);
};
const onModelRemoved = (model: editor.ITextModel): void => {
editor.setModelMarkers(model, this._languageId, []);
const uriStr = model.uri.toString();
const listener = this._listener[uriStr];
if (listener) {
listener.dispose();
delete this._listener[uriStr];
}
};
this._disposables.push(editor.onDidCreateModel(onModelAdd));
this._disposables.push(editor.onWillDisposeModel(onModelRemoved));
this._disposables.push(
editor.onDidChangeModelLanguage((event) => {
onModelRemoved(event.model);
onModelAdd(event.model);
})
);
this._disposables.push(
defaults.onDidChange((_) => {
editor.getModels().forEach((model) => {
if (model.getLanguageId() === this._languageId) {
onModelRemoved(model);
onModelAdd(model);
}
});
})
);
this._disposables.push(
editor.onWillDisposeModel((model) => {
this._resetSchema(model.uri);
})
);
this._disposables.push(
editor.onDidChangeModelLanguage((event) => {
this._resetSchema(event.model.uri);
})
);
this._disposables.push({
dispose: () => {
editor.getModels().forEach(onModelRemoved);
for (const key in this._listener) {
this._listener[key].dispose();
}
}
});
editor.getModels().forEach(onModelAdd);
}
public dispose(): void {
this._disposables.forEach((d) => d && d.dispose());
this._disposables.length = 0;
}
private _resetSchema(resource: Uri): void {
this._worker(resource).then((worker) => {
worker.resetSchema(resource.toString());
});
}
private _doValidate(model: editor.ITextModel, languageId: string): void {
this._worker(model.uri)
.then((worker) => worker.doValidation(model.uri.toString()))
.then((diagnostics) => {
// Filter out diagnostics that are inside or related to ${...} interpolations
const text = model.getValue();
const filteredDiagnostics = diagnostics.filter((diag) => {
const startOffset = model.getOffsetAt({
lineNumber: diag.range.start.line + 1,
column: diag.range.start.character + 1
});
const endOffset = model.getOffsetAt({
lineNumber: diag.range.end.line + 1,
column: diag.range.end.character + 1
});
// Check if this diagnostic overlaps with any interpolation
const interpolationRegex = /\$\{[^}]*\}/g;
let match;
while ((match = interpolationRegex.exec(text)) !== null) {
const interpStart = match.index;
const interpEnd = interpStart + match[0].length;
// If the diagnostic overlaps with interpolation, filter it out
if (startOffset < interpEnd && endOffset > interpStart) {
return false;
}
}
return true;
});
// Convert to Monaco markers
const markers = filteredDiagnostics.map((diag) => ({
severity: this._convertSeverity(diag.severity),
startLineNumber: diag.range.start.line + 1,
startColumn: diag.range.start.character + 1,
endLineNumber: diag.range.end.line + 1,
endColumn: diag.range.end.character + 1,
message: diag.message,
code: typeof diag.code === 'number' ? String(diag.code) : diag.code
}));
if (model.getLanguageId() === languageId) {
editor.setModelMarkers(model, languageId, markers);
}
})
.catch((err) => {
console.error(err);
});
}
private _convertSeverity(severity: number | undefined): editor.MarkerSeverity {
switch (severity) {
case 1:
return editor.MarkerSeverity.Error;
case 2:
return editor.MarkerSeverity.Warning;
case 3:
return editor.MarkerSeverity.Info;
case 4:
return editor.MarkerSeverity.Hint;
default:
return editor.MarkerSeverity.Error;
}
}
}
// --- Variable Completion Provider ---
class VariableCompletionProvider implements languages.CompletionItemProvider {
triggerCharacters = ['$', '{'];
constructor(private _defaults: LanguageServiceDefaults) {}
async provideCompletionItems(
model: editor.ITextModel,
position: Position,
_context: languages.CompletionContext,
_token: CancellationToken
): Promise<languages.CompletionList | null> {
const variableContext = this._defaults.variableContext;
if (!variableContext) {
return null;
}
// Check if we're inside an interpolation ${...}
const textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
// Find if we're inside an unclosed ${
const lastInterpolationStart = textUntilPosition.lastIndexOf('${');
if (lastInterpolationStart === -1) {
return null;
}
const afterInterpolationStart = textUntilPosition.substring(lastInterpolationStart);
if (afterInterpolationStart.includes('}')) {
// The interpolation is closed, we're not inside it
return null;
}
// We're inside an interpolation! Get variables
const variables = await variableContext.getVariables();
// Get the word being typed after ${
const wordInfo = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
startColumn: wordInfo.startColumn,
endLineNumber: position.lineNumber,
endColumn: wordInfo.endColumn
};
const suggestions: languages.CompletionItem[] = variables.map((variable) => ({
label: variable.name,
kind: languages.CompletionItemKind.Variable,
detail: variable.detail || variable.type,
documentation: this._formatDocumentation(variable),
insertText: variable.name,
range: range
}));
return { suggestions };
}
private _formatDocumentation(variable: VariableDefinition): string | { value: string } {
let doc = '';
if (variable.description) {
doc += variable.description;
}
if (variable.value !== undefined) {
if (doc) {
doc += '\n\n';
}
doc += `**Current value:** \`${JSON.stringify(variable.value)}\``;
}
return doc ? { value: doc } : '';
}
}
// --- Variable Hover Provider ---
class VariableHoverProvider implements languages.HoverProvider {
constructor(private _defaults: LanguageServiceDefaults) {}
async provideHover(
model: editor.ITextModel,
position: Position,
_token: CancellationToken
): Promise<languages.Hover | null> {
const variableContext = this._defaults.variableContext;
if (!variableContext) {
return null;
}
// Check if we're hovering over something inside ${...}
const line = model.getLineContent(position.lineNumber);
const offset = position.column - 1;
// Find interpolation boundaries on this line
let inInterpolation = false;
let interpStart = -1;
let interpEnd = -1;
for (let i = 0; i < line.length - 1; i++) {
if (line[i] === '$' && line[i + 1] === '{') {
if (i < offset) {
inInterpolation = true;
interpStart = i + 2; // After ${
}
} else if (line[i] === '}' && inInterpolation) {
if (i >= offset) {
interpEnd = i;
break;
}
inInterpolation = false;
}
}
if (!inInterpolation || interpStart === -1) {
return null;
}
if (interpEnd === -1) {
interpEnd = line.length;
}
// Get the word at position
const wordInfo = model.getWordAtPosition(position);
if (!wordInfo) {
return null;
}
const variableName = wordInfo.word;
const variables = await variableContext.getVariables();
const variable = variables.find((v) => v.name === variableName);
if (!variable) {
return null;
}
const contents: languages.IMarkdownString[] = [];
// Type signature
if (variable.type) {
contents.push({
value: `\`\`\`typescript\n(variable) ${variable.name}: ${variable.type}\n\`\`\``
});
} else {
contents.push({
value: `\`\`\`typescript\n(variable) ${variable.name}\n\`\`\``
});
}
// Description
if (variable.description) {
contents.push({ value: variable.description });
}
// Current value
if (variable.value !== undefined) {
contents.push({
value: `**Current value:**\n\`\`\`json\n${JSON.stringify(variable.value, null, 2)}\n\`\`\``
});
}
return {
contents,
range: {
startLineNumber: position.lineNumber,
startColumn: wordInfo.startColumn,
endLineNumber: position.lineNumber,
endColumn: wordInfo.endColumn
}
};
}
}
// --- Mode Setup ---
export function setupMode(defaults: LanguageServiceDefaults): IDisposable {
const disposables: IDisposable[] = [];
const providers: IDisposable[] = [];
const client = new WorkerManager(defaults as any); // Reuse JSON worker manager
disposables.push(client);
worker = (...uris: Uri[]): Promise<JSONWorker> => {
return client.getLanguageServiceWorker(...uris);
};
function registerProviders(): void {
const { languageId, modeConfiguration } = defaults;
disposeAll(providers);
// Register Monarch tokenizer
if (modeConfiguration.tokens) {
providers.push(languages.setMonarchTokensProvider(languageId, language));
}
// Register language configuration
providers.push(languages.setLanguageConfiguration(languageId, conf));
// JSON language service providers (reusing from json worker)
if (modeConfiguration.documentFormattingEdits) {
providers.push(
languages.registerDocumentFormattingEditProvider(
languageId,
new languageFeatures.DocumentFormattingEditProvider(worker)
)
);
}
if (modeConfiguration.documentRangeFormattingEdits) {
providers.push(
languages.registerDocumentRangeFormattingEditProvider(
languageId,
new languageFeatures.DocumentRangeFormattingEditProvider(worker)
)
);
}
if (modeConfiguration.completionItems) {
// Standard JSON completions
providers.push(
languages.registerCompletionItemProvider(
languageId,
new languageFeatures.CompletionAdapter(worker, [' ', ':', '"'])
)
);
// Variable completions inside ${...}
providers.push(
languages.registerCompletionItemProvider(
languageId,
new VariableCompletionProvider(defaults)
)
);
}
if (modeConfiguration.hovers) {
// Standard JSON hover
providers.push(
languages.registerHoverProvider(languageId, new languageFeatures.HoverAdapter(worker))
);
// Variable hover inside ${...}
providers.push(
languages.registerHoverProvider(languageId, new VariableHoverProvider(defaults))
);
}
if (modeConfiguration.documentSymbols) {
providers.push(
languages.registerDocumentSymbolProvider(
languageId,
new languageFeatures.DocumentSymbolAdapter(worker)
)
);
}
if (modeConfiguration.colors) {
providers.push(
languages.registerColorProvider(
languageId,
new languageFeatures.DocumentColorAdapter(worker)
)
);
}
if (modeConfiguration.foldingRanges) {
providers.push(
languages.registerFoldingRangeProvider(
languageId,
new languageFeatures.FoldingRangeAdapter(worker)
)
);
}
if (modeConfiguration.diagnostics) {
providers.push(new JSONInterpolationDiagnosticsAdapter(languageId, worker, defaults));
}
if (modeConfiguration.selectionRanges) {
providers.push(
languages.registerSelectionRangeProvider(
languageId,
new languageFeatures.SelectionRangeAdapter(worker)
)
);
}
}
registerProviders();
let modeConfiguration = defaults.modeConfiguration;
defaults.onDidChange((newDefaults) => {
if (newDefaults.modeConfiguration !== modeConfiguration) {
modeConfiguration = newDefaults.modeConfiguration;
registerProviders();
}
});
disposables.push(asDisposable(providers));
return asDisposable(disposables);
}
function asDisposable(disposables: IDisposable[]): IDisposable {
return { dispose: () => disposeAll(disposables) };
}
function disposeAll(disposables: IDisposable[]) {
while (disposables.length) {
disposables.pop()!.dispose();
}
}

View file

@ -0,0 +1,298 @@
/*---------------------------------------------------------------------------------------------
* JSON with Interpolation - Monaco Contribution
* Registers the language and provides configuration APIs
*--------------------------------------------------------------------------------------------*/
import { Emitter, IEvent, languages } from 'monaco-editor-core';
// Re-export types from json language service that are still useful
export type {
ASTNode,
JSONDocument,
JSONSchema,
JSONSchemaRef,
MatchingSchema
} from '../json/monaco.contribution';
// --- Variable Context Types ---
/**
* Represents a variable that can be used in interpolation
*/
export interface VariableDefinition {
/**
* The name of the variable (without $ prefix)
*/
readonly name: string;
/**
* The type of the variable for display purposes
*/
readonly type?: string;
/**
* Description shown in hover and completion
*/
readonly description?: string;
/**
* The current value of the variable (for hover preview)
*/
readonly value?: unknown;
/**
* Optional detail text shown in completion item
*/
readonly detail?: string;
}
/**
* Context provider for interpolation variables
*/
export interface VariableContextProvider {
/**
* Get all available variables
*/
getVariables(): VariableDefinition[] | Promise<VariableDefinition[]>;
/**
* Resolve a variable by name (optional, for nested property access)
*/
resolveVariable?(name: string): unknown | Promise<unknown>;
}
// --- Configuration Options ---
export interface DiagnosticsOptions {
/**
* If set, the validator will be enabled and perform syntax and schema based validation
*/
readonly validate?: boolean;
/**
* If set, comments are tolerated
*/
readonly allowComments?: boolean;
/**
* If set, trailing commas are tolerated
*/
readonly allowTrailingCommas?: boolean;
/**
* A list of known schemas and/or associations of schemas to file names
*/
readonly schemas?: {
readonly uri: string;
readonly fileMatch?: string[];
readonly schema?: unknown;
}[];
/**
* If set, the schema service would load schema content on-demand
*/
readonly enableSchemaRequest?: boolean;
/**
* The severity of problems from schema validation
*/
readonly schemaValidation?: SeverityLevel;
/**
* The severity of problems from schema request failures
*/
readonly schemaRequest?: SeverityLevel;
/**
* The severity of trailing commas
*/
readonly trailingCommas?: SeverityLevel;
/**
* The severity of comments
*/
readonly comments?: SeverityLevel;
}
export type SeverityLevel = 'error' | 'warning' | 'ignore';
export interface ModeConfiguration {
/**
* Defines whether the built-in documentFormattingEdit provider is enabled
*/
readonly documentFormattingEdits?: boolean;
/**
* Defines whether the built-in documentRangeFormattingEdit provider is enabled
*/
readonly documentRangeFormattingEdits?: boolean;
/**
* Defines whether the built-in completionItemProvider is enabled
*/
readonly completionItems?: boolean;
/**
* Defines whether the built-in hoverProvider is enabled
*/
readonly hovers?: boolean;
/**
* Defines whether the built-in documentSymbolProvider is enabled
*/
readonly documentSymbols?: boolean;
/**
* Defines whether the built-in tokens provider is enabled
*/
readonly tokens?: boolean;
/**
* Defines whether the built-in color provider is enabled
*/
readonly colors?: boolean;
/**
* Defines whether the built-in foldingRange provider is enabled
*/
readonly foldingRanges?: boolean;
/**
* Defines whether the built-in diagnostic provider is enabled
*/
readonly diagnostics?: boolean;
/**
* Defines whether the built-in selection range provider is enabled
*/
readonly selectionRanges?: boolean;
}
// --- Language Service Defaults ---
export interface LanguageServiceDefaults {
readonly languageId: string;
readonly onDidChange: IEvent<LanguageServiceDefaults>;
readonly diagnosticsOptions: DiagnosticsOptions;
readonly modeConfiguration: ModeConfiguration;
readonly variableContext: VariableContextProvider | null;
setDiagnosticsOptions(options: DiagnosticsOptions): void;
setModeConfiguration(modeConfiguration: ModeConfiguration): void;
/**
* Set the variable context provider for interpolation completions and hover
*/
setVariableContext(provider: VariableContextProvider | null): void;
}
class LanguageServiceDefaultsImpl implements LanguageServiceDefaults {
private _onDidChange = new Emitter<LanguageServiceDefaults>();
private _diagnosticsOptions!: DiagnosticsOptions;
private _modeConfiguration!: ModeConfiguration;
private _variableContext: VariableContextProvider | null = null;
private _languageId: string;
constructor(
languageId: string,
diagnosticsOptions: DiagnosticsOptions,
modeConfiguration: ModeConfiguration
) {
this._languageId = languageId;
this.setDiagnosticsOptions(diagnosticsOptions);
this.setModeConfiguration(modeConfiguration);
}
get onDidChange(): IEvent<LanguageServiceDefaults> {
return this._onDidChange.event;
}
get languageId(): string {
return this._languageId;
}
get modeConfiguration(): ModeConfiguration {
return this._modeConfiguration;
}
get diagnosticsOptions(): DiagnosticsOptions {
return this._diagnosticsOptions;
}
get variableContext(): VariableContextProvider | null {
return this._variableContext;
}
setDiagnosticsOptions(options: DiagnosticsOptions): void {
this._diagnosticsOptions = options || Object.create(null);
this._onDidChange.fire(this);
}
setModeConfiguration(modeConfiguration: ModeConfiguration): void {
this._modeConfiguration = modeConfiguration || Object.create(null);
this._onDidChange.fire(this);
}
setVariableContext(provider: VariableContextProvider | null): void {
this._variableContext = provider;
this._onDidChange.fire(this);
}
}
// --- Defaults ---
const diagnosticDefault: Required<DiagnosticsOptions> = {
validate: true,
allowComments: true,
allowTrailingCommas: true,
schemas: [],
enableSchemaRequest: false,
schemaRequest: 'warning',
schemaValidation: 'warning',
comments: 'ignore', // Allow comments by default for this variant
trailingCommas: 'ignore' // Allow trailing commas by default
};
const modeConfigurationDefault: Required<ModeConfiguration> = {
documentFormattingEdits: true,
documentRangeFormattingEdits: true,
completionItems: true,
hovers: true,
documentSymbols: true,
tokens: true,
colors: true,
foldingRanges: true,
diagnostics: true,
selectionRanges: true
};
// --- Exports ---
export const jsonInterpolationDefaults: LanguageServiceDefaults = new LanguageServiceDefaultsImpl(
'json-interpolation',
diagnosticDefault,
modeConfigurationDefault
);
// --- Language Registration ---
let modePromise: Promise<typeof import('./jsonInterpolationMode')> | null = null;
function getMode(): Promise<typeof import('./jsonInterpolationMode')> {
if (!modePromise) {
modePromise = import('./jsonInterpolationMode');
}
return modePromise;
}
languages.register({
id: 'json-interpolation',
extensions: ['.jsonc', '.json5'],
aliases: ['JSON with Interpolation', 'json-interpolation'],
mimetypes: ['application/json-interpolation']
});
languages.onLanguage('json-interpolation', async () => {
const mode = await getMode();
mode.setupMode(jsonInterpolationDefaults);
});