xstate-tsserver
Version:
XState tsserver plugin for easier navigation in state machines
360 lines • 14.3 kB
JavaScript
"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