mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 15:05:39 +01:00
Integrate JSON language service worker for full functionality
The standalone package now taps into Monaco's built-in JSON language
service via monaco.languages.json.getWorker() to provide:
- Schema-aware completions (doComplete)
- JSON hover information (doHover)
- Full validation with interpolation filtering (doValidation)
- Document formatting (format)
- Document symbols/outline (findDocumentSymbols)
- Folding ranges (getFoldingRanges)
- Selection ranges (getSelectionRanges)
- Color provider (findDocumentColors, getColorPresentations)
Inside ${...} interpolations, the custom variable provider takes over
for completions and hover. All JSON diagnostics that overlap with
interpolations are automatically filtered out.
This commit is contained in:
parent
6039262df7
commit
59eafca1f2
2 changed files with 614 additions and 133 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
/*---------------------------------------------------------------------------------------------
|
/*---------------------------------------------------------------------------------------------
|
||||||
* Monaco JSON Interpolation
|
* Monaco JSON Interpolation
|
||||||
* Standalone add-on for Monaco Editor providing JSON with ${...} variable interpolation
|
* Standalone add-on for Monaco Editor providing JSON with ${...} variable interpolation
|
||||||
|
* Integrates with Monaco's built-in JSON language service for full functionality
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
|
|
@ -94,7 +95,11 @@ class LanguageServiceDefaultsImpl implements LanguageServiceDefaults {
|
||||||
documentSymbols: true,
|
documentSymbols: true,
|
||||||
tokens: true,
|
tokens: true,
|
||||||
foldingRanges: true,
|
foldingRanges: true,
|
||||||
diagnostics: true
|
diagnostics: true,
|
||||||
|
documentFormattingEdits: true,
|
||||||
|
documentRangeFormattingEdits: true,
|
||||||
|
selectionRanges: true,
|
||||||
|
colors: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,174 +139,630 @@ class LanguageServiceDefaultsImpl implements LanguageServiceDefaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helper to get JSON worker ---
|
||||||
|
|
||||||
|
type JSONWorker = {
|
||||||
|
doComplete(uri: string, position: { lineNumber: number; column: number }): Promise<monaco.languages.CompletionList | null>;
|
||||||
|
doHover(uri: string, position: { line: number; character: number }): Promise<{ contents: { kind: string; value: string }[]; range?: any } | null>;
|
||||||
|
doValidation(uri: string): Promise<any[]>;
|
||||||
|
format(uri: string, range: any, options: any): Promise<any[]>;
|
||||||
|
findDocumentSymbols(uri: string): Promise<any[]>;
|
||||||
|
getFoldingRanges(uri: string): Promise<any[]>;
|
||||||
|
getSelectionRanges(uri: string, positions: any[]): Promise<any[]>;
|
||||||
|
findDocumentColors(uri: string): Promise<any[]>;
|
||||||
|
getColorPresentations(uri: string, color: any, range: any): Promise<any[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getJSONWorker(resource: monaco.Uri): Promise<JSONWorker | null> {
|
||||||
|
// Access Monaco's built-in JSON language service
|
||||||
|
const json = (monaco.languages as any).json;
|
||||||
|
if (!json || !json.getWorker) {
|
||||||
|
console.warn('monaco-json-interpolation: JSON language service not available');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const getWorker = await json.getWorker();
|
||||||
|
const worker = await getWorker(resource);
|
||||||
|
return worker;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('monaco-json-interpolation: Failed to get JSON worker', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Check if position is inside interpolation ---
|
||||||
|
|
||||||
|
function isInsideInterpolation(model: monaco.editor.ITextModel, position: monaco.Position): boolean {
|
||||||
|
const textUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastInterpolationStart = textUntilPosition.lastIndexOf('${');
|
||||||
|
if (lastInterpolationStart === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterInterpolationStart = textUntilPosition.substring(lastInterpolationStart);
|
||||||
|
return !afterInterpolationStart.includes('}');
|
||||||
|
}
|
||||||
|
|
||||||
// --- Providers ---
|
// --- Providers ---
|
||||||
|
|
||||||
function registerProviders(defaults: LanguageServiceDefaultsImpl): void {
|
function registerProviders(defaults: LanguageServiceDefaultsImpl): void {
|
||||||
// Variable completion provider
|
// Completion provider - combines JSON completions with variable completions
|
||||||
monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
|
monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
|
||||||
triggerCharacters: ['$', '{'],
|
triggerCharacters: ['"', ':', ' ', '$', '{'],
|
||||||
|
|
||||||
async provideCompletionItems(
|
async provideCompletionItems(
|
||||||
model: monaco.editor.ITextModel,
|
model: monaco.editor.ITextModel,
|
||||||
position: monaco.Position
|
position: monaco.Position,
|
||||||
|
context: monaco.languages.CompletionContext
|
||||||
): Promise<monaco.languages.CompletionList | null> {
|
): Promise<monaco.languages.CompletionList | null> {
|
||||||
const variableContext = defaults.variableContext;
|
// Check if we're inside an interpolation
|
||||||
if (!variableContext) {
|
if (isInsideInterpolation(model, position)) {
|
||||||
|
// Provide variable completions
|
||||||
|
const variableContext = defaults.variableContext;
|
||||||
|
if (variableContext) {
|
||||||
|
const variables = await variableContext.getVariables();
|
||||||
|
const wordInfo = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: wordInfo.startColumn,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: wordInfo.endColumn
|
||||||
|
};
|
||||||
|
|
||||||
|
const suggestions: monaco.languages.CompletionItem[] = variables.map((variable) => ({
|
||||||
|
label: variable.name,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Variable,
|
||||||
|
detail: variable.detail || variable.type,
|
||||||
|
documentation: formatDocumentation(variable),
|
||||||
|
insertText: variable.name,
|
||||||
|
range: range
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're inside an interpolation ${...}
|
// Get JSON completions from the worker
|
||||||
const textUntilPosition = model.getValueInRange({
|
const worker = await getJSONWorker(model.uri);
|
||||||
startLineNumber: 1,
|
if (!worker) {
|
||||||
startColumn: 1,
|
|
||||||
endLineNumber: position.lineNumber,
|
|
||||||
endColumn: position.column
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastInterpolationStart = textUntilPosition.lastIndexOf('${');
|
|
||||||
if (lastInterpolationStart === -1) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const afterInterpolationStart = textUntilPosition.substring(lastInterpolationStart);
|
try {
|
||||||
if (afterInterpolationStart.includes('}')) {
|
const info = await worker.doComplete(model.uri.toString(), {
|
||||||
|
lineNumber: position.lineNumber,
|
||||||
|
column: position.column
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert LSP completions to Monaco completions
|
||||||
|
const suggestions: monaco.languages.CompletionItem[] = (info.items || []).map((item: any) => ({
|
||||||
|
label: item.label,
|
||||||
|
kind: convertCompletionItemKind(item.kind),
|
||||||
|
detail: item.detail,
|
||||||
|
documentation: item.documentation,
|
||||||
|
insertText: item.insertText || item.label,
|
||||||
|
insertTextRules: item.insertTextFormat === 2
|
||||||
|
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
: undefined,
|
||||||
|
range: item.textEdit?.range ? convertRange(item.textEdit.range) : undefined,
|
||||||
|
sortText: item.sortText,
|
||||||
|
filterText: item.filterText
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { suggestions, incomplete: info.isIncomplete || false };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON completion error:', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variables = await variableContext.getVariables();
|
|
||||||
const wordInfo = model.getWordUntilPosition(position);
|
|
||||||
const range = {
|
|
||||||
startLineNumber: position.lineNumber,
|
|
||||||
startColumn: wordInfo.startColumn,
|
|
||||||
endLineNumber: position.lineNumber,
|
|
||||||
endColumn: wordInfo.endColumn
|
|
||||||
};
|
|
||||||
|
|
||||||
const suggestions: monaco.languages.CompletionItem[] = variables.map((variable) => ({
|
|
||||||
label: variable.name,
|
|
||||||
kind: monaco.languages.CompletionItemKind.Variable,
|
|
||||||
detail: variable.detail || variable.type,
|
|
||||||
documentation: formatDocumentation(variable),
|
|
||||||
insertText: variable.name,
|
|
||||||
range: range
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { suggestions };
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Variable hover provider
|
// Hover provider - combines JSON hover with variable hover
|
||||||
monaco.languages.registerHoverProvider(LANGUAGE_ID, {
|
monaco.languages.registerHoverProvider(LANGUAGE_ID, {
|
||||||
async provideHover(
|
async provideHover(
|
||||||
model: monaco.editor.ITextModel,
|
model: monaco.editor.ITextModel,
|
||||||
position: monaco.Position
|
position: monaco.Position
|
||||||
): Promise<monaco.languages.Hover | null> {
|
): Promise<monaco.languages.Hover | null> {
|
||||||
const variableContext = defaults.variableContext;
|
// Check if we're inside an interpolation
|
||||||
if (!variableContext) {
|
if (isInsideInterpolation(model, position)) {
|
||||||
return null;
|
const variableContext = defaults.variableContext;
|
||||||
}
|
if (variableContext) {
|
||||||
|
const wordInfo = model.getWordAtPosition(position);
|
||||||
|
if (wordInfo) {
|
||||||
|
const variables = await variableContext.getVariables();
|
||||||
|
const variable = variables.find((v) => v.name === wordInfo.word);
|
||||||
|
|
||||||
const line = model.getLineContent(position.lineNumber);
|
if (variable) {
|
||||||
const offset = position.column - 1;
|
const contents: monaco.IMarkdownString[] = [];
|
||||||
|
|
||||||
// Find interpolation boundaries
|
if (variable.type) {
|
||||||
let inInterpolation = false;
|
contents.push({
|
||||||
let interpStart = -1;
|
value: `\`\`\`typescript\n(variable) ${variable.name}: ${variable.type}\n\`\`\``
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
contents.push({
|
||||||
|
value: `\`\`\`typescript\n(variable) ${variable.name}\n\`\`\``
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < line.length - 1; i++) {
|
if (variable.description) {
|
||||||
if (line[i] === '$' && line[i + 1] === '{') {
|
contents.push({ value: variable.description });
|
||||||
if (i < offset) {
|
}
|
||||||
inInterpolation = true;
|
|
||||||
interpStart = i + 2;
|
|
||||||
}
|
|
||||||
} else if (line[i] === '}' && inInterpolation) {
|
|
||||||
if (i >= offset) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
inInterpolation = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inInterpolation || interpStart === -1) {
|
if (variable.value !== undefined) {
|
||||||
return null;
|
contents.push({
|
||||||
}
|
value: `**Current value:**\n\`\`\`json\n${JSON.stringify(variable.value, null, 2)}\n\`\`\``
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const wordInfo = model.getWordAtPosition(position);
|
return {
|
||||||
if (!wordInfo) {
|
contents,
|
||||||
return null;
|
range: {
|
||||||
}
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: wordInfo.startColumn,
|
||||||
const variableName = wordInfo.word;
|
endLineNumber: position.lineNumber,
|
||||||
const variables = await variableContext.getVariables();
|
endColumn: wordInfo.endColumn
|
||||||
const variable = variables.find((v) => v.name === variableName);
|
}
|
||||||
|
};
|
||||||
if (!variable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contents: monaco.IMarkdownString[] = [];
|
|
||||||
|
|
||||||
if (variable.type) {
|
|
||||||
contents.push({
|
|
||||||
value: `\`\`\`typescript\n(variable) ${variable.name}: ${variable.type}\n\`\`\``
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
contents.push({
|
|
||||||
value: `\`\`\`typescript\n(variable) ${variable.name}\n\`\`\``
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.description) {
|
|
||||||
contents.push({ value: variable.description });
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Folding range provider (basic JSON-like folding)
|
|
||||||
monaco.languages.registerFoldingRangeProvider(LANGUAGE_ID, {
|
|
||||||
provideFoldingRanges(
|
|
||||||
model: monaco.editor.ITextModel
|
|
||||||
): monaco.languages.FoldingRange[] {
|
|
||||||
const ranges: monaco.languages.FoldingRange[] = [];
|
|
||||||
const stack: { char: string; line: number }[] = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= model.getLineCount(); i++) {
|
|
||||||
const line = model.getLineContent(i);
|
|
||||||
for (const char of line) {
|
|
||||||
if (char === '{' || char === '[') {
|
|
||||||
stack.push({ char, line: i });
|
|
||||||
} else if (char === '}' || char === ']') {
|
|
||||||
const open = stack.pop();
|
|
||||||
if (open && open.line < i) {
|
|
||||||
ranges.push({
|
|
||||||
start: open.line,
|
|
||||||
end: i,
|
|
||||||
kind: monaco.languages.FoldingRangeKind.Region
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ranges;
|
// Get JSON hover from the worker
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await worker.doHover(model.uri.toString(), {
|
||||||
|
line: position.lineNumber - 1,
|
||||||
|
character: position.column - 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents: monaco.IMarkdownString[] = info.contents.map((c: any) => {
|
||||||
|
if (typeof c === 'string') {
|
||||||
|
return { value: c };
|
||||||
|
}
|
||||||
|
return { value: c.value || '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents,
|
||||||
|
range: info.range ? convertRange(info.range) : undefined
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON hover error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Diagnostics adapter
|
||||||
|
setupDiagnostics(defaults);
|
||||||
|
|
||||||
|
// Document formatting provider
|
||||||
|
monaco.languages.registerDocumentFormattingEditProvider(LANGUAGE_ID, {
|
||||||
|
async provideDocumentFormattingEdits(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
options: monaco.languages.FormattingOptions
|
||||||
|
): Promise<monaco.languages.TextEdit[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const edits = await worker.format(model.uri.toString(), null, {
|
||||||
|
tabSize: options.tabSize,
|
||||||
|
insertSpaces: options.insertSpaces
|
||||||
|
});
|
||||||
|
|
||||||
|
return edits.map((edit: any) => ({
|
||||||
|
range: convertRange(edit.range),
|
||||||
|
text: edit.newText
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON formatting error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Document range formatting provider
|
||||||
|
monaco.languages.registerDocumentRangeFormattingEditProvider(LANGUAGE_ID, {
|
||||||
|
async provideDocumentRangeFormattingEdits(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
range: monaco.Range,
|
||||||
|
options: monaco.languages.FormattingOptions
|
||||||
|
): Promise<monaco.languages.TextEdit[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const edits = await worker.format(
|
||||||
|
model.uri.toString(),
|
||||||
|
{
|
||||||
|
start: { line: range.startLineNumber - 1, character: range.startColumn - 1 },
|
||||||
|
end: { line: range.endLineNumber - 1, character: range.endColumn - 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tabSize: options.tabSize,
|
||||||
|
insertSpaces: options.insertSpaces
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return edits.map((edit: any) => ({
|
||||||
|
range: convertRange(edit.range),
|
||||||
|
text: edit.newText
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON range formatting error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Document symbols provider
|
||||||
|
monaco.languages.registerDocumentSymbolProvider(LANGUAGE_ID, {
|
||||||
|
async provideDocumentSymbols(
|
||||||
|
model: monaco.editor.ITextModel
|
||||||
|
): Promise<monaco.languages.DocumentSymbol[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const symbols = await worker.findDocumentSymbols(model.uri.toString());
|
||||||
|
return symbols.map(convertSymbol);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON symbols error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folding range provider
|
||||||
|
monaco.languages.registerFoldingRangeProvider(LANGUAGE_ID, {
|
||||||
|
async provideFoldingRanges(
|
||||||
|
model: monaco.editor.ITextModel
|
||||||
|
): Promise<monaco.languages.FoldingRange[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ranges = await worker.getFoldingRanges(model.uri.toString());
|
||||||
|
return ranges.map((range: any) => ({
|
||||||
|
start: range.startLine + 1,
|
||||||
|
end: range.endLine + 1,
|
||||||
|
kind: range.kind
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON folding error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Selection range provider
|
||||||
|
monaco.languages.registerSelectionRangeProvider(LANGUAGE_ID, {
|
||||||
|
async provideSelectionRanges(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
positions: monaco.Position[]
|
||||||
|
): Promise<monaco.languages.SelectionRange[][] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lspPositions = positions.map((p) => ({
|
||||||
|
line: p.lineNumber - 1,
|
||||||
|
character: p.column - 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ranges = await worker.getSelectionRanges(model.uri.toString(), lspPositions);
|
||||||
|
return ranges.map((rangeList: any) => {
|
||||||
|
const result: monaco.languages.SelectionRange[] = [];
|
||||||
|
let current = rangeList;
|
||||||
|
while (current) {
|
||||||
|
result.push({
|
||||||
|
range: convertRange(current.range)
|
||||||
|
});
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON selection range error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Color provider
|
||||||
|
monaco.languages.registerColorProvider(LANGUAGE_ID, {
|
||||||
|
async provideDocumentColors(
|
||||||
|
model: monaco.editor.ITextModel
|
||||||
|
): Promise<monaco.languages.IColorInformation[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const colors = await worker.findDocumentColors(model.uri.toString());
|
||||||
|
return colors.map((info: any) => ({
|
||||||
|
range: convertRange(info.range),
|
||||||
|
color: info.color
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON colors error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async provideColorPresentations(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
colorInfo: monaco.languages.IColorInformation
|
||||||
|
): Promise<monaco.languages.IColorPresentation[] | null> {
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const presentations = await worker.getColorPresentations(
|
||||||
|
model.uri.toString(),
|
||||||
|
colorInfo.color,
|
||||||
|
{
|
||||||
|
start: {
|
||||||
|
line: colorInfo.range.startLineNumber - 1,
|
||||||
|
character: colorInfo.range.startColumn - 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line: colorInfo.range.endLineNumber - 1,
|
||||||
|
character: colorInfo.range.endColumn - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return presentations.map((p: any) => ({
|
||||||
|
label: p.label,
|
||||||
|
textEdit: p.textEdit
|
||||||
|
? {
|
||||||
|
range: convertRange(p.textEdit.range),
|
||||||
|
text: p.textEdit.newText
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON color presentations error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Diagnostics ---
|
||||||
|
|
||||||
|
function setupDiagnostics(defaults: LanguageServiceDefaultsImpl): void {
|
||||||
|
const listeners: { [uri: string]: monaco.IDisposable } = {};
|
||||||
|
|
||||||
|
const doValidate = async (model: monaco.editor.ITextModel) => {
|
||||||
|
if (model.getLanguageId() !== LANGUAGE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await getJSONWorker(model.uri);
|
||||||
|
if (!worker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const diagnostics = await worker.doValidation(model.uri.toString());
|
||||||
|
|
||||||
|
// Filter out diagnostics that overlap with interpolations
|
||||||
|
const text = model.getValue();
|
||||||
|
const filteredDiagnostics = diagnostics.filter((diag: any) => {
|
||||||
|
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 for overlap with interpolations
|
||||||
|
const interpolationRegex = /\$\{[^}]*\}/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = interpolationRegex.exec(text)) !== null) {
|
||||||
|
const interpStart = match.index;
|
||||||
|
const interpEnd = interpStart + match[0].length;
|
||||||
|
|
||||||
|
if (startOffset < interpEnd && endOffset > interpStart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to Monaco markers
|
||||||
|
const markers: monaco.editor.IMarkerData[] = filteredDiagnostics.map((diag: any) => ({
|
||||||
|
severity: 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
|
||||||
|
}));
|
||||||
|
|
||||||
|
monaco.editor.setModelMarkers(model, LANGUAGE_ID, markers);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON validation error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModelAdd = (model: monaco.editor.ITextModel) => {
|
||||||
|
if (model.getLanguageId() !== LANGUAGE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle: ReturnType<typeof setTimeout>;
|
||||||
|
listeners[model.uri.toString()] = model.onDidChangeContent(() => {
|
||||||
|
clearTimeout(handle);
|
||||||
|
handle = setTimeout(() => doValidate(model), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
doValidate(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModelRemoved = (model: monaco.editor.ITextModel) => {
|
||||||
|
monaco.editor.setModelMarkers(model, LANGUAGE_ID, []);
|
||||||
|
const listener = listeners[model.uri.toString()];
|
||||||
|
if (listener) {
|
||||||
|
listener.dispose();
|
||||||
|
delete listeners[model.uri.toString()];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.onDidCreateModel(onModelAdd);
|
||||||
|
monaco.editor.onWillDisposeModel(onModelRemoved);
|
||||||
|
monaco.editor.onDidChangeModelLanguage((event) => {
|
||||||
|
onModelRemoved(event.model);
|
||||||
|
onModelAdd(event.model);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate existing models
|
||||||
|
monaco.editor.getModels().forEach(onModelAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Conversion utilities ---
|
||||||
|
|
||||||
|
function convertRange(range: any): monaco.IRange {
|
||||||
|
return {
|
||||||
|
startLineNumber: range.start.line + 1,
|
||||||
|
startColumn: range.start.character + 1,
|
||||||
|
endLineNumber: range.end.line + 1,
|
||||||
|
endColumn: range.end.character + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSeverity(severity: number): monaco.MarkerSeverity {
|
||||||
|
switch (severity) {
|
||||||
|
case 1:
|
||||||
|
return monaco.MarkerSeverity.Error;
|
||||||
|
case 2:
|
||||||
|
return monaco.MarkerSeverity.Warning;
|
||||||
|
case 3:
|
||||||
|
return monaco.MarkerSeverity.Info;
|
||||||
|
case 4:
|
||||||
|
return monaco.MarkerSeverity.Hint;
|
||||||
|
default:
|
||||||
|
return monaco.MarkerSeverity.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertCompletionItemKind(kind: number): monaco.languages.CompletionItemKind {
|
||||||
|
const kindMap: { [key: number]: monaco.languages.CompletionItemKind } = {
|
||||||
|
1: monaco.languages.CompletionItemKind.Text,
|
||||||
|
2: monaco.languages.CompletionItemKind.Method,
|
||||||
|
3: monaco.languages.CompletionItemKind.Function,
|
||||||
|
4: monaco.languages.CompletionItemKind.Constructor,
|
||||||
|
5: monaco.languages.CompletionItemKind.Field,
|
||||||
|
6: monaco.languages.CompletionItemKind.Variable,
|
||||||
|
7: monaco.languages.CompletionItemKind.Class,
|
||||||
|
8: monaco.languages.CompletionItemKind.Interface,
|
||||||
|
9: monaco.languages.CompletionItemKind.Module,
|
||||||
|
10: monaco.languages.CompletionItemKind.Property,
|
||||||
|
11: monaco.languages.CompletionItemKind.Unit,
|
||||||
|
12: monaco.languages.CompletionItemKind.Value,
|
||||||
|
13: monaco.languages.CompletionItemKind.Enum,
|
||||||
|
14: monaco.languages.CompletionItemKind.Keyword,
|
||||||
|
15: monaco.languages.CompletionItemKind.Snippet,
|
||||||
|
16: monaco.languages.CompletionItemKind.Color,
|
||||||
|
17: monaco.languages.CompletionItemKind.File,
|
||||||
|
18: monaco.languages.CompletionItemKind.Reference,
|
||||||
|
19: monaco.languages.CompletionItemKind.Folder,
|
||||||
|
20: monaco.languages.CompletionItemKind.EnumMember,
|
||||||
|
21: monaco.languages.CompletionItemKind.Constant,
|
||||||
|
22: monaco.languages.CompletionItemKind.Struct,
|
||||||
|
23: monaco.languages.CompletionItemKind.Event,
|
||||||
|
24: monaco.languages.CompletionItemKind.Operator,
|
||||||
|
25: monaco.languages.CompletionItemKind.TypeParameter
|
||||||
|
};
|
||||||
|
return kindMap[kind] || monaco.languages.CompletionItemKind.Property;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSymbolKind(kind: number): monaco.languages.SymbolKind {
|
||||||
|
const kindMap: { [key: number]: monaco.languages.SymbolKind } = {
|
||||||
|
1: monaco.languages.SymbolKind.File,
|
||||||
|
2: monaco.languages.SymbolKind.Module,
|
||||||
|
3: monaco.languages.SymbolKind.Namespace,
|
||||||
|
4: monaco.languages.SymbolKind.Package,
|
||||||
|
5: monaco.languages.SymbolKind.Class,
|
||||||
|
6: monaco.languages.SymbolKind.Method,
|
||||||
|
7: monaco.languages.SymbolKind.Property,
|
||||||
|
8: monaco.languages.SymbolKind.Field,
|
||||||
|
9: monaco.languages.SymbolKind.Constructor,
|
||||||
|
10: monaco.languages.SymbolKind.Enum,
|
||||||
|
11: monaco.languages.SymbolKind.Interface,
|
||||||
|
12: monaco.languages.SymbolKind.Function,
|
||||||
|
13: monaco.languages.SymbolKind.Variable,
|
||||||
|
14: monaco.languages.SymbolKind.Constant,
|
||||||
|
15: monaco.languages.SymbolKind.String,
|
||||||
|
16: monaco.languages.SymbolKind.Number,
|
||||||
|
17: monaco.languages.SymbolKind.Boolean,
|
||||||
|
18: monaco.languages.SymbolKind.Array,
|
||||||
|
19: monaco.languages.SymbolKind.Object,
|
||||||
|
20: monaco.languages.SymbolKind.Key,
|
||||||
|
21: monaco.languages.SymbolKind.Null,
|
||||||
|
22: monaco.languages.SymbolKind.EnumMember,
|
||||||
|
23: monaco.languages.SymbolKind.Struct,
|
||||||
|
24: monaco.languages.SymbolKind.Event,
|
||||||
|
25: monaco.languages.SymbolKind.Operator,
|
||||||
|
26: monaco.languages.SymbolKind.TypeParameter
|
||||||
|
};
|
||||||
|
return kindMap[kind] || monaco.languages.SymbolKind.Property;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSymbol(symbol: any): monaco.languages.DocumentSymbol {
|
||||||
|
return {
|
||||||
|
name: symbol.name,
|
||||||
|
detail: symbol.detail || '',
|
||||||
|
kind: convertSymbolKind(symbol.kind),
|
||||||
|
range: convertRange(symbol.range),
|
||||||
|
selectionRange: convertRange(symbol.selectionRange),
|
||||||
|
children: symbol.children ? symbol.children.map(convertSymbol) : undefined,
|
||||||
|
tags: symbol.tags || []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDocumentation(
|
function formatDocumentation(
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,26 @@ export interface ModeConfiguration {
|
||||||
* Defines whether the built-in diagnostic provider is enabled
|
* Defines whether the built-in diagnostic provider is enabled
|
||||||
*/
|
*/
|
||||||
readonly diagnostics?: boolean;
|
readonly diagnostics?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 selectionRange provider is enabled
|
||||||
|
*/
|
||||||
|
readonly selectionRanges?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether the built-in color provider is enabled
|
||||||
|
*/
|
||||||
|
readonly colors?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue