/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rpc from "vscode-jsonrpc/node"; import {Recipe, ScanningRecipe} from "../../recipe"; import {Cursor, rootCursor, SourceFile, Tree} from "../../tree"; import {TreeVisitor} from "../../visitor"; import {ExecutionContext} from "../../execution"; import {DATA_TABLE_STORE, DataTableStore} from "../../data-table"; import {withMetrics, extractSourcePath} from "./metrics"; import {lookupVisitor} from "./visitor-registry"; export interface VisitResponse { modified: boolean } // Tracks the last phase (scan or edit) for each recipe to detect cycle transitions type RecipePhase = 'scan' | 'edit'; const recipePhases: WeakMap = new WeakMap(); export class Visit { constructor(readonly visitor: string, readonly sourceFileType: string, readonly visitorOptions: Map | undefined, readonly treeId: string, readonly p: string, readonly cursor: string[] | undefined) { } static handle(connection: rpc.MessageConnection, localObjects: Map, preparedRecipes: Map, recipeCursors: WeakMap, getObject: (id: string, sourceFileType?: string) => any, captureRefCheckpoint: (treeId: string) => void, getCursor: (cursorIds: string[] | undefined, sourceFileType?: string) => Promise, dataTableStore: () => DataTableStore | undefined, metricsCsv?: string): void { connection.onRequest( new rpc.RequestType("Visit"), withMetrics( "Visit", metricsCsv, (context) => async (request) => { const p = await getObject(request.p, undefined); const store = dataTableStore(); if (store && p instanceof ExecutionContext) { p.messages[DATA_TABLE_STORE] = store; } captureRefCheckpoint(request.treeId); const before: Tree = await getObject(request.treeId, request.sourceFileType); const cursor = await getCursor(request.cursor, request.sourceFileType); context.target = extractSourcePath(before, cursor); localObjects.set(before.id.toString(), before); const visitor = await Visit.instantiateVisitor(request, preparedRecipes, recipeCursors, p); const after = await visitor.visit(before, p, cursor); if (!after) { localObjects.delete(before.id.toString()); } else if (after !== before) { localObjects.set(after.id.toString(), after); } return {modified: before !== after}; } ) ); } static async instantiateVisitor(request: {visitor: string, visitorOptions?: Map}, preparedRecipes: Map, recipeCursors: WeakMap, p: any): Promise> { const visitorName = request.visitor; if (visitorName.startsWith("scan:")) { const recipeKey = visitorName.substring("scan:".length); const recipe = preparedRecipes.get(recipeKey) as ScanningRecipe; if (!recipe) { throw new Error(`No scanning recipe found for key: ${recipeKey}`); } // If we're transitioning from edit back to scan, this is a new cycle. // Clear the cursor so a fresh accumulator is created. if (recipePhases.get(recipe) === 'edit') { recipeCursors.delete(recipe); } recipePhases.set(recipe, 'scan'); let cursor = recipeCursors.get(recipe); if (!cursor) { cursor = rootCursor(); recipeCursors.set(recipe, cursor); } const acc = recipe.accumulator(cursor, p); return new class extends TreeVisitor { // Delegate isAcceptable to the scanner visitor // This ensures we only process source files the scanner can handle async isAcceptable(sourceFile: SourceFile, ctx: ExecutionContext): Promise { return (await recipe.scanner(acc)).isAcceptable(sourceFile, ctx); } protected async preVisit(tree: any, ctx: ExecutionContext): Promise { await (await recipe.scanner(acc)).visit(tree, ctx); this.stopAfterPreVisit(); return tree; } } } else if (visitorName.startsWith("edit:")) { const recipeKey = visitorName.substring("edit:".length); const recipe = preparedRecipes.get(recipeKey) as Recipe; if (!recipe) { throw new Error(`No editing recipe found for key: ${recipeKey}`); } recipePhases.set(recipe, 'edit'); // For ScanningRecipe, we need to use the same cursor that was used during scanning // to retrieve the accumulator that was stored there if (recipe instanceof ScanningRecipe) { let cursor = recipeCursors.get(recipe); if (!cursor) { cursor = rootCursor(); recipeCursors.set(recipe, cursor); } const acc = recipe.accumulator(cursor, p); return recipe.editorWithData(acc); } return await recipe.editor(); } else { const ctor = lookupVisitor(visitorName) ?? (globalThis as any)[visitorName]; if (!ctor) { throw new Error(`Unknown visitor: ${visitorName}`); } return Reflect.construct( ctor, request.visitorOptions ? Array.from(request.visitorOptions.values()) : []) } } }