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