feat: write & test command parser + tweak tsconfig for @/ imports

Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
Chapman Pendery
2023-10-06 09:59:41 -07:00
parent 6f869932e0
commit d815ded501
9 changed files with 8835 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+8381
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -6,7 +6,8 @@
"type": "module",
"scripts": {
"build": "tsc",
"start": "node ./build/index.js"
"start": "node ./build/index.js",
"test": "jest"
},
"repository": {
"type": "git",
@@ -30,8 +31,11 @@
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/ink": "^2.0.3",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@withfig/autocomplete-types": "^1.28.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
}
}
}
+3 -4
View File
@@ -1,6 +1,5 @@
import {render} from "../ui/ui.js"
import { render } from "@/ui/ui.js";
export const action = () => {
render()
}
render();
};
+74
View File
@@ -0,0 +1,74 @@
type CommandToken = {
token: string;
complete: boolean;
isOption: boolean;
};
const cmdDelim = /(\|\|)|(&&)|(;)/;
const spaceRegex = /\s/;
export const parseCommand = (command: string): CommandToken[] => {
const lastCommand = command.split(cmdDelim).at(-1)?.trimStart();
return lastCommand ? parse(lastCommand) : [];
};
const parse = (command: string): CommandToken[] => {
const tokens: CommandToken[] = [];
let [readingQuotedString, readingFlag, readingCmd] = [false, false, false];
let readingIdx = 0;
let readingQuoteChar = "";
[...command].forEach((char, idx) => {
const reading = readingQuotedString || readingFlag || readingCmd;
if (!reading && (char === `'` || char === `"`)) {
[readingQuotedString, readingIdx, readingQuoteChar] = [true, idx, char];
return;
} else if (!reading && char === `-`) {
[readingFlag, readingIdx] = [true, idx];
return;
} else if (!reading && !spaceRegex.test(char)) {
[readingCmd, readingIdx] = [true, idx];
return;
}
if (
readingQuotedString &&
char === readingQuoteChar &&
command.at(idx - 1) !== "\\"
) {
readingQuotedString = false;
const complete =
idx + 1 < command.length && spaceRegex.test(command[idx + 1]);
tokens.push({
token: command.slice(readingIdx, idx + 1),
complete,
isOption: false,
});
} else if ((readingFlag && spaceRegex.test(char)) || char === "=") {
readingFlag = false;
tokens.push({
token: command.slice(readingIdx, idx),
complete: true,
isOption: true,
});
} else if (readingCmd && spaceRegex.test(char)) {
readingCmd = false;
tokens.push({
token: command.slice(readingIdx, idx),
complete: true,
isOption: false,
});
}
});
const reading = readingQuotedString || readingFlag || readingCmd;
if (reading) {
tokens.push({
token: command.slice(readingIdx),
complete: false,
isOption: false,
});
}
return tokens;
};
@@ -0,0 +1,321 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseCommand cmd 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
]
`;
exports[`parseCommand cmd "value' 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": false,
"isOption": false,
"token": ""value' ",
},
]
`;
exports[`parseCommand cmd "value'\\"\\"" 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": false,
"token": ""value'\\"\\""",
},
]
`;
exports[`parseCommand cmd 'value' 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": false,
"token": "'value'",
},
]
`;
exports[`parseCommand cmd --flag value 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "--flag",
},
{
"complete": false,
"isOption": false,
"token": "value",
},
]
`;
exports[`parseCommand cmd --flag="value" 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "--flag",
},
{
"complete": true,
"isOption": false,
"token": ""value"",
},
]
`;
exports[`parseCommand cmd --flag='value' 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "--flag",
},
{
"complete": true,
"isOption": false,
"token": "'value'",
},
]
`;
exports[`parseCommand cmd --flag=value 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "--flag",
},
{
"complete": false,
"isOption": false,
"token": "value",
},
]
`;
exports[`parseCommand cmd -f 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
]
`;
exports[`parseCommand cmd -f 'value' 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
{
"complete": true,
"isOption": false,
"token": "'value'",
},
]
`;
exports[`parseCommand cmd -f 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": false,
"isOption": false,
"token": "-f",
},
]
`;
exports[`parseCommand cmd -f 2`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": false,
"isOption": false,
"token": "-f",
},
]
`;
exports[`parseCommand cmd -f value 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
{
"complete": true,
"isOption": false,
"token": "value",
},
]
`;
exports[`parseCommand cmd -f= 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
]
`;
exports[`parseCommand cmd -f="value" 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
{
"complete": true,
"isOption": false,
"token": ""value"",
},
]
`;
exports[`parseCommand cmd -f='val 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
{
"complete": false,
"isOption": false,
"token": "'val",
},
]
`;
exports[`parseCommand cmd -f=value 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": true,
"token": "-f",
},
{
"complete": true,
"isOption": false,
"token": "value",
},
]
`;
exports[`parseCommand cmd 1`] = `
[
{
"complete": false,
"isOption": false,
"token": "cmd",
},
]
`;
exports[`parseCommand cmd value 1`] = `
[
{
"complete": true,
"isOption": false,
"token": "cmd",
},
{
"complete": true,
"isOption": false,
"token": "value",
},
]
`;
+31
View File
@@ -0,0 +1,31 @@
import { parseCommand } from "../../runtime/parser";
const testData = [
{ command: `cmd --flag value` },
{ command: `cmd --flag=value` },
{ command: `cmd --flag='value' ` },
{ command: `cmd --flag="value" ` },
{ command: `cmd 'value' ` },
{ command: `cmd value ` },
{ command: `cmd -f` },
{ command: `cmd -f=value ` },
{ command: `cmd -f value ` },
{ command: `cmd -f 'value' ` },
{ command: `cmd -f="value" ` },
{ command: `cmd -f='val` },
{ command: `cmd -f` },
{ command: `cmd -f=` },
{ command: `cmd -f ` },
{ command: `cmd` },
{ command: `cmd ` },
{ command: `cmd "value' ` },
{ command: `cmd "value'\\"\\"" ` },
];
describe(`parseCommand`, () => {
testData.forEach(({ command }) => {
test(command, () => {
expect(parseCommand(command)).toMatchSnapshot();
});
});
});
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import {
Text,
Box,
@@ -7,10 +7,10 @@ import {
measureElement,
DOMElement,
} from "ink";
import { getSuggestions } from "../runtime/runtime.js";
import wrapAnsi from "wrap-ansi";
import { getSuggestions } from "@/runtime/runtime.js";
import Cursor from "./cursor.js";
import Suggestions from "./suggestions.js";
import wrapAnsi from "wrap-ansi";
const Prompt = "> ";
+11 -7
View File
@@ -1,9 +1,13 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": ["src/**/*"],
"compilerOptions": {
"jsx": "react",
"outDir": "./build",
"types": ["@withfig/autocomplete-types"]
"extends": "@tsconfig/node18/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "src/**/*.test.ts", "src/**/*.spec.ts"],
"compilerOptions": {
"jsx": "react",
"outDir": "./build",
"types": ["@withfig/autocomplete-types", "jest"],
"paths": {
"@/*": ["./src/*"]
}
}
}
}