UNPKG

xstate-tsserver

Version:

XState tsserver plugin for easier navigation in state machines

226 lines 13 kB
"use strict"; const tsserverlibrary_1 = require("typescript/lib/tsserverlibrary"); const utils_1 = require("./utils"); const treesitter_1 = require("./treesitter"); const xstate_1 = require("./xstate"); function init(modules) { // TODO: Figure out how bad it is to use the global typescript module even // though it might be different const ts = modules.typescript; function create(info) { // Diagnostic logging function log(message) { info.project.projectService.logger.info(message); } log("xstate tsserver loaded"); // Set up decorator object const proxy = Object.create(null); for (let k of Object.keys(info.languageService)) { const x = info.languageService[k]; // @ts-expect-error - JS runtime trickery which is tricky to type tersely proxy[k] = (...args) => x.apply(info.languageService, args); } function getMachineAtPosition(fileName, position) { const program = info.languageService.getProgram(); const sourceFile = program?.getSourceFile(fileName); if (!sourceFile) return null; const rootNode = (0, treesitter_1.getFileRootNode)(sourceFile); const machineConfigNodes = (0, xstate_1.getMachineConfigNodes)(rootNode, position); if (!machineConfigNodes) return null; return machineConfigNodes; } proxy.getQuickInfoAtPosition = (fileName, position) => { const prior = info.languageService.getQuickInfoAtPosition(fileName, position); // TODO: Get the hover info for a thing inside a machine config and // display it's implementation return prior; }; // If used on a setup implementable, find the references within the machine // to it // If used on a machine implementable, find the reference within setup // function. If the setup implementable is a shorthand_property_identifier, // then merge in the references at that position proxy.findReferences = (fileName, position) => { const prior = info.languageService.findReferences(fileName, position); const machineConfigNodes = getMachineAtPosition(fileName, position); if (!machineConfigNodes) return prior; const { machineConfig, setupConfig, location } = machineConfigNodes; if (location === "setupConfig") { const { type, node, text } = (0, xstate_1.getImplementableInSetupInPosition)(setupConfig, position); if (type === "unknown") return prior; log(`✅ Found ${type} implementation for ${text}`); const implementations = (0, xstate_1.findAllImplementablesInMachine)(machineConfig, type, text); if (implementations.length === 0) return prior; return [ ...(prior ?? []), { definition: (0, utils_1.createNodeDefinitionWithDisplayParts)(fileName, node), references: implementations.map((implementation) => (0, utils_1.createReferenceDefinition)(fileName, implementation)), }, ]; } else if (location === "machineConfig") { const { type, text, node } = (0, xstate_1.getImplementableInMachine)(machineConfig, position); if (type === "unknown") return prior; const setupNode = (0, xstate_1.findImplementableInSetup)(setupConfig, type, text); if (setupNode) { log(`✅ Found ${type} definition for ${text} in setup`); return [ ...(prior ?? []), ...(setupNode.type === "shorthand_property_identifier" ? (info.languageService.findReferences(fileName, setupNode.startIndex) ?? []) : []), { definition: (0, utils_1.createNodeDefinitionWithDisplayParts)(fileName, node), references: [(0, utils_1.createReferenceDefinition)(fileName, node)], }, ]; } } return prior; }; // If used on a implementable within the machine config, find the definition // within the setup config // If the defintion is a shorthand_property_identifier, then use it's // definition for the implementable definition // If used on a transition target node, go to the state which it targets // If used on an initial state identifier, go to the state which is targets proxy.getDefinitionAndBoundSpan = (fileName, position) => { const prior = info.languageService.getDefinitionAndBoundSpan(fileName, position); const machineConfigNodes = getMachineAtPosition(fileName, position); if (!machineConfigNodes) return prior; const { machineConfig, setupConfig, location } = machineConfigNodes; if (location !== "machineConfig") return prior; const { type, text, node } = (0, xstate_1.getImplementableInMachine)(machineConfig, position); if (type !== "unknown") { const setupNode = (0, xstate_1.findImplementableInSetup)(setupConfig, type, text); if (setupNode) { log(`✅ Found ${type} definition for ${text} in setup`); if (setupNode.type === "shorthand_property_identifier") return info.languageService.getDefinitionAndBoundSpan(fileName, setupNode.startIndex); return (0, utils_1.createNodeDefinitionWithTextSpan)(fileName, setupNode, node); } } const transitionObject = (0, xstate_1.getTransitionObjectAtPosition)(machineConfig, position); if (transitionObject) { const { node: transitionNode, text: transitionText, target: transitionTarget, type: transitionType, } = transitionObject; log(`✅ Found transition ${transitionText} at ${position}`); const machineStates = (0, xstate_1.getAllDescendantStateObjects)(machineConfig); if (transitionType === "absolute") { const [idTarget, ...rest] = transitionTarget.split("."); const relativeTarget = rest.join("."); const stateWithId = machineStates.find((state) => state.id === idTarget); if (stateWithId) { // If just a pure id, then go to the state if (relativeTarget === "") { log(`✅ Found state target ${stateWithId.name || "(machine)"} at ${position}`); return (0, utils_1.createNodeDefinitionWithTextSpan)(fileName, stateWithId.node, transitionNode); } else { // If more after . then run the state children search const stateTarget = machineStates.find((state) => state.path === `${stateWithId.path}.${relativeTarget}`); if (stateTarget) { log(`✅ Found state target ${stateTarget.path} at ${position}`); return (0, utils_1.createNodeDefinitionWithTextSpan)(fileName, stateTarget.node, transitionNode); } } } } else { const currentStateObjects = (0, xstate_1.getStateObjectsAtPosition)(machineStates, position); const baseStateObject = currentStateObjects.at(transitionType === "relativeChildren" ? -1 : -2); if (!baseStateObject) return prior; const stateObjectTarget = machineStates.find((state) => state.path === `${baseStateObject.path}.${transitionTarget}`); if (stateObjectTarget) { log(`✅ Found state target ${stateObjectTarget.path} at ${position}`); return (0, utils_1.createNodeDefinitionWithTextSpan)(fileName, stateObjectTarget.node, transitionNode); } } } const initialStateObject = (0, xstate_1.getInitialStateObjectAtPosition)(machineConfig, position); if (initialStateObject) { log(`✅ Found initial state ${initialStateObject.text} at ${position}`); const machineStates = (0, xstate_1.getAllDescendantStateObjects)(machineConfig); const currentStateObjects = (0, xstate_1.getStateObjectsAtPosition)(machineStates, position); const currentStatePath = currentStateObjects.at(-1)?.path; const targetPath = `${currentStatePath}.${initialStateObject.text}`; const targetStateObject = machineStates.find((state) => state.path === targetPath); if (targetStateObject) { log(`✅ Found initial state target ${targetPath} at ${position}`); return (0, utils_1.createNodeDefinitionWithTextSpan)(fileName, targetStateObject.node, initialStateObject.node); } } return prior; }; // If used on a transition target node, list all the possible transitions proxy.getCompletionsAtPosition = (fileName, position, options, formatSettings) => { const prior = info.languageService.getCompletionsAtPosition(fileName, position, options, formatSettings); const machineConfigNodes = getMachineAtPosition(fileName, position); if (!machineConfigNodes) return prior; const { machineConfig, location } = machineConfigNodes; if (location !== "machineConfig") return prior; const transitionObject = (0, xstate_1.getTransitionObjectAtPosition)(machineConfig, position); if (transitionObject) { const { node: transitionNode } = transitionObject; log(`✅ Found transition at ${position}`); const machineStates = (0, xstate_1.getAllDescendantStateObjects)(machineConfig); // Get all state nodes const currentStatePath = (0, xstate_1.getStateObjectsAtPosition)(machineStates, position).at(-1)?.path ?? ""; const stateTargets = (0, xstate_1.getAllStateTargets)(currentStatePath, machineStates); return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: stateTargets.map((target) => ({ replacementSpan: { start: transitionNode.startIndex + 1, length: transitionNode.endIndex - transitionNode.startIndex - 2, }, name: target.transitionName, kind: tsserverlibrary_1.ScriptElementKind.string, sortText: target.sortText, })), }; } const initialStateObject = (0, xstate_1.getInitialStateObjectAtPosition)(machineConfig, position); if (initialStateObject) { const { node: initialStateNode } = initialStateObject; log(`✅ Found initial state at ${position}`); const machineStates = (0, xstate_1.getAllDescendantStateObjects)(machineConfig); // Get all state nodes const currentStatePath = (0, xstate_1.getStateObjectsAtPosition)(machineStates, position).at(-1)?.path ?? ""; const stateTargets = (0, xstate_1.getAllDirectChildStateTargets)(currentStatePath, machineStates); return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: stateTargets.map((target) => ({ replacementSpan: { start: initialStateNode.startIndex + 1, length: initialStateNode.endIndex - initialStateNode.startIndex - 2, }, name: target.transitionName, kind: tsserverlibrary_1.ScriptElementKind.string, sortText: target.sortText, })), }; } return prior; }; return proxy; } return { create }; } module.exports = init; //# sourceMappingURL=index.js.map