UNPKG

xstate-tsserver

Version:

XState tsserver plugin for easier navigation in state machines

360 lines 14.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getMachineConfigNodes = getMachineConfigNodes; exports.getImplementableInMachine = getImplementableInMachine; exports.findImplementableInSetup = findImplementableInSetup; exports.getImplementableInSetupInPosition = getImplementableInSetupInPosition; exports.findAllImplementablesInMachine = findAllImplementablesInMachine; exports.getTransitionObjectAtPosition = getTransitionObjectAtPosition; exports.getInitialStateObjectAtPosition = getInitialStateObjectAtPosition; exports.getAllDescendantStateObjects = getAllDescendantStateObjects; exports.getStateObjectsAtPosition = getStateObjectsAtPosition; exports.getAllStateTargets = getAllStateTargets; exports.getAllDirectChildStateTargets = getAllDirectChildStateTargets; const tree_sitter_1 = __importDefault(require("tree-sitter")); const queries_1 = require("./queries"); const treesitter_1 = require("./treesitter"); const utils_1 = require("./utils"); /** * Finds the machine at the given position and returns all capture groups within * the match */ function getMachineNodes(rootNode, position) { const parser = (0, treesitter_1.createParser)(); const queryMatches = new tree_sitter_1.default.Query(parser.getLanguage(), queries_1.machineWithSetupQuery); const matches = queryMatches.matches(rootNode); const results = {}; for (const match of matches) { const machineNode = match.captures.find((cap) => cap.name === "xstate.machine")?.node; if (machineNode && machineNode.startIndex <= position && position < machineNode.endIndex) { for (const capture of match.captures) { results[capture.name] = capture.node; } continue; } } return results; } /** * Find the machine at the given position */ function getMachineConfigNodes(rootNode, position) { const { "xstate.machine": machine, "xstate.root.config": machineConfig, "xstate.setup.config": setupConfig, } = getMachineNodes(rootNode, position); if (!machine || !machineConfig || !setupConfig) return null; let location = null; if (machineConfig.startIndex <= position && position < machineConfig.endIndex) { location = "machineConfig"; } else if (setupConfig.startIndex <= position && position < setupConfig.endIndex) { location = "setupConfig"; } if (!location) return null; return { machine, machineConfig, setupConfig, location }; } const setupQueryByImplementationType = { action: queries_1.setupActionsQuery, actor: queries_1.setupActorsQuery, guard: queries_1.setupGuardsQuery, delay: queries_1.setupDelaysQuery, }; const machineQueryByImplementationType = { action: queries_1.machineActionsQuery, actor: queries_1.machineActorsQuery, guard: queries_1.machineGuardsQuery, delay: queries_1.machineDelaysQuery, }; /** * Find the xstate implementation type at the given position * To be used with machine configuration */ function getImplementableInMachine(machineNode, position) { const actionNode = (0, treesitter_1.findMatchingNode)(machineNode, position, queries_1.machineActionsQuery, "xstate.action", "xstate.action.name"); if (actionNode) return { type: "action", node: actionNode, text: actionNode.text, }; const actorNode = (0, treesitter_1.findMatchingNode)(machineNode, position, queries_1.machineActorsQuery, "xstate.actor", "xstate.actor.name"); if (actorNode) return { type: "actor", node: actorNode, text: actorNode.text, }; const guardNode = (0, treesitter_1.findMatchingNode)(machineNode, position, queries_1.machineGuardsQuery, "xstate.guard", "xstate.guard.name"); if (guardNode) return { type: "guard", node: guardNode, text: guardNode.text, }; const delayNode = (0, treesitter_1.findMatchingNode)(machineNode, position, queries_1.machineDelaysQuery, "xstate.delay", "xstate.delay.name"); if (delayNode) return { type: "delay", node: delayNode, text: delayNode.text, }; return { type: "unknown", node: null, text: "" }; } /** * Find the implementation definition node for the given implementation type * and name * To be used within the setup configuration */ function findImplementableInSetup(setupConfig, type, implementationName) { const setupNode = (0, treesitter_1.findCaptureNodeWithText)(setupConfig, setupQueryByImplementationType[type], `${type}.name`, implementationName); return setupNode; } const setupKeyToImplementableType = { actions: "action", actors: "actor", guards: "guard", delays: "delay", }; /** * Find the setup node at the given position and also return it's implementable type */ function getImplementableInSetupInPosition(setupConfig, position) { const parser = (0, treesitter_1.createParser)(); const queryMatches = new tree_sitter_1.default.Query(parser.getLanguage(), queries_1.setupImplementableQuery); const matches = queryMatches.matches(setupConfig); for (const match of matches) { const keyNode = match.captures.find((cap) => cap.name === "implementation.name")?.node; if (keyNode) { if (position >= keyNode.startIndex && position < keyNode.endIndex) { const keyType = match.captures.find((cap) => cap.name === "setup.key") ?.node.text; const type = setupKeyToImplementableType[keyType]; if (type) { return { type, node: keyNode, text: keyNode.text, }; } } } } return { type: "unknown", node: null, text: "" }; } /** * Find all implementations of the given implementable type and name in the given machine */ function findAllImplementablesInMachine(machineConfig, implementationType, implementationName) { const parser = (0, treesitter_1.createParser)(); const queryMatches = new tree_sitter_1.default.Query(parser.getLanguage(), machineQueryByImplementationType[implementationType]); const matches = queryMatches.matches(machineConfig); const implementations = []; for (const match of matches) { const keyNode = match.captures.find((cap) => cap.name === `xstate.${implementationType}.name`)?.node; if (keyNode && keyNode.text === implementationName) { implementations.push(keyNode); } } return implementations; } /** * Returns the transition (target) node at the given position */ function getTransitionObjectAtPosition(rootNode, position) { const node = (0, treesitter_1.findMatchingNode)(rootNode, position, queries_1.transitionQuery, "transition.target"); if (!node) return null; const text = node.firstNamedChild?.text; const { type, target } = (0, utils_1.getTransitionType)(text); return { node, type, target, text, }; } function getInitialStateObjectAtPosition(rootNode, position) { const node = (0, treesitter_1.findMatchingNode)(rootNode, position, queries_1.initialStateQuery, "initial.state"); if (!node) return null; const text = node.firstNamedChild?.text ?? ""; return { node, text, }; } /** * Finds all the descendant state nodes visible from the root node including itself * State paths will be nested with . separators relative to the root node */ function getAllDescendantStateObjects(rootNode) { const parser = (0, treesitter_1.createParser)(); const queryMatches = new tree_sitter_1.default.Query(parser.getLanguage(), queries_1.stateQuery); const matches = queryMatches.matches(rootNode); const stateObjects = [ { node: rootNode, name: "", id: getStateId(rootNode), path: "", initialState: getInitialState(rootNode), isInitialState: true, }, ]; for (const match of matches) { const { ["xstate.state.name"]: stateNameNode, ["xstate.state.config"]: stateConfigNode, ["xstate.state"]: fullStateNode, } = (0, treesitter_1.getAllCapturesOfMatch)(match); if (!stateNameNode || !stateConfigNode || !fullStateNode) continue; const stateName = stateNameNode.text; const parentState = stateObjects.findLast((state) => state.node.startIndex <= fullStateNode.startIndex && fullStateNode.endIndex <= state.node.endIndex); const statePath = `${parentState?.path ?? ""}.${stateName}`; const stateId = getStateId(stateConfigNode); const initialState = getInitialState(stateConfigNode); stateObjects.push({ node: fullStateNode, name: stateName, path: statePath, id: stateId, initialState, isInitialState: stateName === parentState?.initialState, }); } return stateObjects; } /** * Filters the states objects to only include those with the current position. * The last one will be the current state */ function getStateObjectsAtPosition(stateObject, position) { return stateObject.filter((state) => state.node.startIndex <= position && position < state.node.endIndex); } /** * From a state config node, find the id by iterating through the object * properties */ function getStateId(stateConfigNode) { if (stateConfigNode.type !== "object") return ""; for (const pair of stateConfigNode.namedChildren) { const [key, value] = pair.namedChildren; if (!key || !value) continue; if (key.type === "property_identifier" && key.text === "id") { if (value.type === "string") { return value.namedChildren[0].text; } } } return ""; } function getInitialState(stateConfigNode) { if (stateConfigNode.type !== "object") return ""; for (const pair of stateConfigNode.namedChildren) { const [key, value] = pair.namedChildren; if (!key || !value) continue; if (key.type === "property_identifier" && key.text === "initial") { if (value.type === "string") { return value.namedChildren[0]?.text ?? ""; } } } return ""; } function getAllStateTargets(currentStatePath, machineStateObjects) { const stateTargets = []; for (const state of machineStateObjects) { const isCurrentState = state.path === currentStatePath; // First figure out the ids if (state.id) { stateTargets.push({ type: "absolute", sortText: getStateSortText("absolute", state.id, isCurrentState), transitionName: `#${state.id}`, node: state.node, }); for (const childState of machineStateObjects) { if (childState.path.startsWith(state.path) && childState.path !== state.path) { const childStateAbsolutePath = `${state.id}${childState.path.slice(state.path.length)}`; stateTargets.push({ type: "absolute", sortText: getStateSortText("absolute", childStateAbsolutePath, childState.path === currentStatePath), transitionName: `#${childStateAbsolutePath}`, node: childState.node, }); } } } // The root state can only be reached with an id if (state.path === "") continue; // Handle child states if (state.path.startsWith(currentStatePath) && !isCurrentState) { const transitionName = state.path.slice(currentStatePath.length + 1); stateTargets.push({ type: "relativeChildren", sortText: getStateSortText("relativeChildren", transitionName), transitionName: `.${transitionName}`, node: state.node, }); } // Handle sibling states // The root state node has no siblings if (currentStatePath === "") continue; const parentStatePath = getParentStatePath(currentStatePath); if (state.path.startsWith(parentStatePath)) { const transitionName = state.path.slice(parentStatePath.length + 1); stateTargets.push({ type: "relative", sortText: getStateSortText("relative", transitionName, isCurrentState), transitionName, node: state.node, }); } } return stateTargets; } function getAllDirectChildStateTargets(currentStatePath, machineStateObjects) { const stateTargets = []; for (const state of machineStateObjects) { const currentStateDepth = getStateDepth(currentStatePath); const stateDepth = getStateDepth(state.path); if (state.path.startsWith(currentStatePath) && stateDepth === currentStateDepth + 1) { const transitionName = state.path.slice(currentStatePath.length + 1); stateTargets.push({ type: "relativeChildren", sortText: state.node.startIndex.toString(), transitionName, node: state.node, }); } } return stateTargets; } function getStateDepth(statePath) { return statePath.split(".").length; } const stateSortTextMap = { relativeChildren: 1, absolute: 2, relative: 0, }; function getStateSortText(transitionType, stateName, isCurrentState) { return `${isCurrentState ? "z" : ""}${stateName.split(".").length - 1}${stateSortTextMap[transitionType]}`; } function getParentStatePath(statePath) { return statePath.split(".").slice(0, -1).join("."); } //# sourceMappingURL=xstate.js.map