mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 19:42:56 +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,
|
__dirname,
|
||||||
'../../src/language/json/monaco.contribution.ts'
|
'../../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(
|
'language/typescript/monaco.contribution': resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../src/language/typescript/monaco.contribution.ts'
|
'../../src/language/typescript/monaco.contribution.ts'
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ monacoApi.languages.css = monaco.css;
|
||||||
monacoApi.languages.html = monaco.html;
|
monacoApi.languages.html = monaco.html;
|
||||||
monacoApi.languages.typescript = monaco.typescript;
|
monacoApi.languages.typescript = monaco.typescript;
|
||||||
monacoApi.languages.json = monaco.json;
|
monacoApi.languages.json = monaco.json;
|
||||||
|
monacoApi.languages.jsonInterpolation = monaco.jsonInterpolation;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import * as css from '../../language/css/monaco.contribution';
|
import * as css from '../../language/css/monaco.contribution';
|
||||||
import * as html from '../../language/html/monaco.contribution';
|
import * as html from '../../language/html/monaco.contribution';
|
||||||
import * as json from '../../language/json/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 * as typescript from '../../language/typescript/monaco.contribution';
|
||||||
import '../../basic-languages/monaco.contribution';
|
import '../../basic-languages/monaco.contribution';
|
||||||
import * as lsp from '@vscode/monaco-lsp-client';
|
import * as lsp from '@vscode/monaco-lsp-client';
|
||||||
|
|
||||||
export * from 'monaco-editor-core';
|
export * from 'monaco-editor-core';
|
||||||
export { createWebWorker, type IWebWorkerOptions } from '../../common/workers';
|
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