Simplify JS worker integration using tokenization detection

Remove virtual model approach and use Monaco's built-in tokenization
to detect when cursor is in embedded JavaScript region. With
nextEmbedded, Monaco automatically syncs model content to the JS
worker, so we can call the worker directly on the model's URI
instead of creating a separate virtual model.
This commit is contained in:
Claude 2025-12-09 20:48:51 +00:00
parent a96c488593
commit 8ca12ee61a
No known key found for this signature in database

View file

@ -174,6 +174,7 @@ async function getJSONWorker(resource: monaco.Uri): Promise<JSONWorker | null> {
type JSWorker = { type JSWorker = {
getCompletionsAtPosition(uri: string, offset: number): Promise<any>; getCompletionsAtPosition(uri: string, offset: number): Promise<any>;
getCompletionEntryDetails(uri: string, offset: number, name: string): Promise<any>;
getQuickInfoAtPosition(uri: string, offset: number): Promise<any>; getQuickInfoAtPosition(uri: string, offset: number): Promise<any>;
getSignatureHelpItems(uri: string, offset: number): Promise<any>; getSignatureHelpItems(uri: string, offset: number): Promise<any>;
}; };
@ -195,82 +196,37 @@ async function getJavaScriptWorker(resource: monaco.Uri): Promise<JSWorker | nul
} }
} }
// --- Interpolation context helpers --- // --- Check if position is inside interpolation using tokenization ---
interface InterpolationContext { function isInsideInterpolation(model: monaco.editor.ITextModel, offset: number): boolean {
/** The full expression inside ${...} */ // Tokenize the content and check if we're in an embedded JS region
expression: string; const source = model.getValue();
/** Offset of ${ in the document */ const tokenLines = monaco.editor.tokenize(source, LANGUAGE_ID);
interpolationStart: number;
/** Offset within the expression where cursor is */
offsetInExpression: number;
/** Start line/column of the interpolation */
startPosition: { lineNumber: number; column: number };
}
function getInterpolationContext( // Flatten tokens and find which one contains our offset
model: monaco.editor.ITextModel, let currentOffset = 0;
position: monaco.Position for (let lineIdx = 0; lineIdx < tokenLines.length; lineIdx++) {
): InterpolationContext | null { const lineTokens = tokenLines[lineIdx];
const text = model.getValue(); const lineContent = model.getLineContent(lineIdx + 1);
const offset = model.getOffsetAt(position);
// Find the ${...} that contains the current position for (let tokenIdx = 0; tokenIdx < lineTokens.length; tokenIdx++) {
const beforeCursor = text.substring(0, offset); const token = lineTokens[tokenIdx];
const lastStart = beforeCursor.lastIndexOf('${'); const nextToken = lineTokens[tokenIdx + 1];
const tokenStart = currentOffset + token.offset;
const tokenEnd = nextToken
? currentOffset + nextToken.offset
: currentOffset + lineContent.length;
if (lastStart === -1) { if (offset >= tokenStart && offset <= tokenEnd) {
return null; // Check if this token is in an embedded JavaScript region
} // The embedded JS tokens won't have our language's postfix
return !token.type.endsWith('.json-interpolation') && token.type !== '';
// Check if there's a closing } between ${ and cursor
const betweenStartAndCursor = text.substring(lastStart + 2, offset);
if (betweenStartAndCursor.includes('}')) {
return null;
}
// Find the closing }
let depth = 1;
let endOffset = offset;
for (let i = offset; i < text.length; i++) {
if (text[i] === '{') depth++;
else if (text[i] === '}') {
depth--;
if (depth === 0) {
endOffset = i;
break;
} }
} }
currentOffset += lineContent.length + 1; // +1 for newline
} }
// Extract the expression (content between ${ and }) return false;
const expressionStart = lastStart + 2;
const expression = text.substring(expressionStart, endOffset);
const offsetInExpression = offset - expressionStart;
const startPos = model.getPositionAt(lastStart);
return {
expression,
interpolationStart: lastStart,
offsetInExpression,
startPosition: {
lineNumber: startPos.lineNumber,
column: startPos.column
}
};
}
// --- Virtual model for JavaScript worker ---
const VIRTUAL_JS_URI = monaco.Uri.parse('file:///interpolation-context.js');
let virtualJsModel: monaco.editor.ITextModel | null = null;
function getOrCreateVirtualJsModel(): monaco.editor.ITextModel {
if (!virtualJsModel || virtualJsModel.isDisposed()) {
virtualJsModel = monaco.editor.createModel('', 'javascript', VIRTUAL_JS_URI);
}
return virtualJsModel;
} }
// --- Providers --- // --- Providers ---
@ -285,25 +241,22 @@ function registerProviders(defaults: LanguageServiceDefaultsImpl): void {
position: monaco.Position, position: monaco.Position,
context: monaco.languages.CompletionContext context: monaco.languages.CompletionContext
): Promise<monaco.languages.CompletionList | null> { ): Promise<monaco.languages.CompletionList | null> {
// Check if we're inside an interpolation const offset = model.getOffsetAt(position);
const interpContext = getInterpolationContext(model, position);
if (interpContext) { // Check if we're inside an interpolation using tokenization
if (isInsideInterpolation(model, offset)) {
// Use JavaScript worker for completions inside interpolation // Use JavaScript worker for completions inside interpolation
const jsWorker = await getJavaScriptWorker(VIRTUAL_JS_URI); // Monaco syncs the model content automatically with nextEmbedded
const jsWorker = await getJavaScriptWorker(model.uri);
if (!jsWorker) { if (!jsWorker) {
return null; return null;
} }
try { try {
// Update the virtual model with the expression // Get completions from JavaScript worker directly on the model's URI
const virtualModel = getOrCreateVirtualJsModel();
virtualModel.setValue(interpContext.expression);
// Get completions from JavaScript worker
const info = await jsWorker.getCompletionsAtPosition( const info = await jsWorker.getCompletionsAtPosition(
VIRTUAL_JS_URI.toString(), model.uri.toString(),
interpContext.offsetInExpression offset
); );
if (!info || !info.entries) { if (!info || !info.entries) {
@ -378,25 +331,21 @@ function registerProviders(defaults: LanguageServiceDefaultsImpl): void {
model: monaco.editor.ITextModel, model: monaco.editor.ITextModel,
position: monaco.Position position: monaco.Position
): Promise<monaco.languages.Hover | null> { ): Promise<monaco.languages.Hover | null> {
// Check if we're inside an interpolation const offset = model.getOffsetAt(position);
const interpContext = getInterpolationContext(model, position);
if (interpContext) { // Check if we're inside an interpolation using tokenization
if (isInsideInterpolation(model, offset)) {
// Use JavaScript worker for hover inside interpolation // Use JavaScript worker for hover inside interpolation
const jsWorker = await getJavaScriptWorker(VIRTUAL_JS_URI); const jsWorker = await getJavaScriptWorker(model.uri);
if (!jsWorker) { if (!jsWorker) {
return null; return null;
} }
try { try {
// Update the virtual model with the expression // Get quick info from JavaScript worker directly on the model's URI
const virtualModel = getOrCreateVirtualJsModel();
virtualModel.setValue(interpContext.expression);
// Get quick info from JavaScript worker
const info = await jsWorker.getQuickInfoAtPosition( const info = await jsWorker.getQuickInfoAtPosition(
VIRTUAL_JS_URI.toString(), model.uri.toString(),
interpContext.offsetInExpression offset
); );
if (!info) { if (!info) {
@ -483,23 +432,22 @@ function registerProviders(defaults: LanguageServiceDefaultsImpl): void {
model: monaco.editor.ITextModel, model: monaco.editor.ITextModel,
position: monaco.Position position: monaco.Position
): Promise<monaco.languages.SignatureHelpResult | null> { ): Promise<monaco.languages.SignatureHelpResult | null> {
const interpContext = getInterpolationContext(model, position); const offset = model.getOffsetAt(position);
if (!interpContext) {
// Only provide signature help inside interpolations
if (!isInsideInterpolation(model, offset)) {
return null; return null;
} }
const jsWorker = await getJavaScriptWorker(VIRTUAL_JS_URI); const jsWorker = await getJavaScriptWorker(model.uri);
if (!jsWorker) { if (!jsWorker) {
return null; return null;
} }
try { try {
const virtualModel = getOrCreateVirtualJsModel();
virtualModel.setValue(interpContext.expression);
const info = await jsWorker.getSignatureHelpItems( const info = await jsWorker.getSignatureHelpItems(
VIRTUAL_JS_URI.toString(), model.uri.toString(),
interpContext.offsetInExpression offset
); );
if (!info || !info.items || info.items.length === 0) { if (!info || !info.items || info.items.length === 0) {
@ -837,7 +785,7 @@ function setupDiagnostics(defaults: LanguageServiceDefaultsImpl): void {
monaco.editor.onDidCreateModel(onModelAdd); monaco.editor.onDidCreateModel(onModelAdd);
monaco.editor.onWillDisposeModel(onModelRemoved); monaco.editor.onWillDisposeModel(onModelRemoved);
monaco.editor.onDidChangeModelLanguage((event) => { monaco.editor.onDidChangeModelLanguage((event: { model: monaco.editor.ITextModel }) => {
onModelRemoved(event.model); onModelRemoved(event.model);
onModelAdd(event.model); onModelAdd(event.model);
}); });