FILES: Path rework & typesafety (#479)

* Added new types for various file paths, all in the Paths folder.
* TypeSafety and other helper functions related to these types
* Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands
* Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way
* Server.textFiles is now a map
* TextFile no longer uses a fn property, now it is filename
* Added a shared ContentFile interface for shared functionality between TextFile and Script.
* related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa.
* File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root.
* Singularized the MessageFilename and LiteratureName enums
* Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath).
* Fix several issues with tab completion, which included pretty much a complete rewrite
* Changed the autocomplete display options so there's less chance it clips outside the display area.
* Turned CompletedProgramName into an enum.
* Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine.
* For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
This commit is contained in:
Snarling
2023-04-24 10:26:57 -04:00
committed by GitHub
parent 6f56f35943
commit e0272ad4af
93 changed files with 3293 additions and 4297 deletions
+25 -117
View File
@@ -1,13 +1,13 @@
import { Terminal } from "../../../Terminal";
import { removeLeadingSlash, removeTrailingSlash } from "../../DirectoryHelpers";
import { ScriptEditorRouteOptions } from "../../../ui/Router";
import { Router } from "../../../ui/GameRoot";
import { BaseServer } from "../../../Server/BaseServer";
import { isScriptFilename } from "../../../Script/isScriptFilename";
import { CursorPositions } from "../../../ScriptEditor/CursorPositions";
import { Script } from "../../../Script/Script";
import { isEmpty } from "lodash";
import { ScriptFilename } from "src/Types/strings";
import { ScriptFilePath, hasScriptExtension } from "../../../Paths/ScriptFilePath";
import { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath";
import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles";
// 2.3: Globbing implementation was removed from this file. Globbing will be reintroduced as broader functionality and integrated here.
interface EditorParameters {
args: (string | number | boolean)[];
@@ -23,126 +23,34 @@ export async function main(ns) {
}`;
interface ISimpleScriptGlob {
glob: string;
preGlob: string;
postGlob: string;
globError: string;
globMatches: string[];
globAgainst: Map<ScriptFilename, Script>;
}
function containsSimpleGlob(filename: string): boolean {
return filename.includes("*");
}
function detectSimpleScriptGlob({ args, server }: EditorParameters): ISimpleScriptGlob | null {
if (args.length == 1 && containsSimpleGlob(`${args[0]}`)) {
const filename = `${args[0]}`;
const scripts = server.scripts;
const parsedGlob = parseSimpleScriptGlob(filename, scripts);
return parsedGlob;
}
return null;
}
function parseSimpleScriptGlob(globString: string, globDatabase: Map<ScriptFilename, Script>): ISimpleScriptGlob {
const parsedGlob: ISimpleScriptGlob = {
glob: globString,
preGlob: "",
postGlob: "",
globError: "",
globMatches: [],
globAgainst: globDatabase,
};
// Ensure deep globs are minified to simple globs, which act as deep globs in this impl
globString = globString.replace("**", "*");
// Ensure only a single glob is present
if (globString.split("").filter((c) => c == "*").length !== 1) {
parsedGlob.globError = "Only a single glob is supported per command.\nexample: `nano my-dir/*.js`";
return parsedGlob;
}
// Split arg around glob, normalize preGlob path
[parsedGlob.preGlob, parsedGlob.postGlob] = globString.split("*");
parsedGlob.preGlob = removeLeadingSlash(parsedGlob.preGlob);
// Add CWD to preGlob path
const cwd = removeTrailingSlash(Terminal.cwd());
parsedGlob.preGlob = `${cwd}/${parsedGlob.preGlob}`;
// For every script on the current server, filter matched scripts per glob values & persist
globDatabase.forEach((script) => {
const filename = script.filename.startsWith("/") ? script.filename : `/${script.filename}`;
if (filename.startsWith(parsedGlob.preGlob) && filename.endsWith(parsedGlob.postGlob)) {
parsedGlob.globMatches.push(filename);
}
});
// Rebuild glob for potential error reporting
parsedGlob.glob = `${parsedGlob.preGlob}*${parsedGlob.postGlob}`;
return parsedGlob;
}
export function commonEditor(
command: string,
{ args, server }: EditorParameters,
scriptEditorRouteOptions?: ScriptEditorRouteOptions,
): void {
if (args.length < 1) {
Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
return;
}
if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
const filesToOpen: Map<ScriptFilePath | TextFilePath, string> = new Map();
for (const arg of args) {
const pattern = String(arg);
let filesToLoadOrCreate = args;
try {
const globSearch = detectSimpleScriptGlob({ args, server });
if (globSearch) {
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError);
filesToLoadOrCreate = globSearch.globMatches;
// Glob of existing files
if (pattern.includes("*") || pattern.includes("?")) {
for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) {
filesToOpen.set(path, file.content);
}
continue;
}
const files = filesToLoadOrCreate.map((arg) => {
const filename = `${arg}`;
if (isScriptFilename(filename)) {
const filepath = Terminal.getFilepath(filename);
if (!filepath) throw `Invalid filename: ${filename}`;
const script = Terminal.getScript(filename);
const fileIsNs2 = isNs2(filename);
const code = script !== null ? script.code : fileIsNs2 ? newNs2Template : "";
if (code === newNs2Template) {
CursorPositions.saveCursor(filename, {
row: 3,
column: 5,
});
}
return [filepath, code];
}
if (filename.endsWith(".txt")) {
const filepath = Terminal.getFilepath(filename);
if (!filepath) throw `Invalid filename: ${filename}`;
const txt = Terminal.getTextFile(filename);
return [filepath, txt === null ? "" : txt.text];
}
throw new Error(
`Invalid file. Only scripts (.script or .js), or text files (.txt) can be edited with ${command}`,
);
});
if (globSearch && files.length === 0) {
throw new Error(`Could not find any valid files to open with ${command} using glob: \`${globSearch.glob}\``);
// Non-glob, files do not need to already exist
const path = Terminal.getFilepath(pattern);
if (!path) return Terminal.error(`Invalid file path ${arg}`);
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`);
}
Router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions);
} catch (e) {
Terminal.error(`${e}`);
const file = server.getContentFile(path);
const content = file ? file.content : isNs2(path) ? newNs2Template : "";
filesToOpen.set(path, content);
if (content === newNs2Template) CursorPositions.saveCursor(path, { row: 3, column: 5 });
}
Router.toScriptEditor(filesToOpen, scriptEditorRouteOptions);
}