mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 11:35:40 +01:00
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:
parent
c619ef9a3d
commit
4540e05e5a
6 changed files with 991 additions and 1 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
184
src/language/json-interpolation/jsonInterpolation.ts
Normal file
184
src/language/json-interpolation/jsonInterpolation.ts
Normal 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']
|
||||
]
|
||||
}
|
||||
};
|
||||
502
src/language/json-interpolation/jsonInterpolationMode.ts
Normal file
502
src/language/json-interpolation/jsonInterpolationMode.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
298
src/language/json-interpolation/monaco.contribution.ts
Normal file
298
src/language/json-interpolation/monaco.contribution.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue