forked from tools/josh
parent
1399db3950
commit
5b28416861
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fwdekker.com",
|
||||
"version": "0.26.3",
|
||||
"version": "0.27.0",
|
||||
"description": "The source code of [my personal website](https://fwdekker.com/).",
|
||||
"author": "Felix W. Dekker",
|
||||
"repository": {
|
||||
|
|
|
@ -45,7 +45,7 @@ addOnLoad(() => {
|
|||
});
|
||||
|
||||
/**
|
||||
* Exist the application if the server is "shut down".
|
||||
* Exits the application if the server is "shut down".
|
||||
*/
|
||||
addOnLoad(() => {
|
||||
if (Persistence.getPoweroff()) {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import "./Extensions";
|
||||
|
||||
|
||||
/**
|
||||
* The fancy ASCII header that is displayed at the start of a terminal session.
|
||||
*/
|
||||
|
@ -50,6 +53,29 @@ export function escapeHtml(string: string): string {
|
|||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input string, finds the word that ends at the indicated offset, and returns the string before the word, the
|
||||
* word itself, and the string after the word.
|
||||
*
|
||||
* The word is preceded by a whitespace or forward slash, and ends at the indicated offset. Whitespace and forward
|
||||
* slashes at the end of the word are ignored.
|
||||
*
|
||||
* @param input the input string to find the word in
|
||||
* @param offset the right-most position of the word
|
||||
* @param delimiters the delimiters to consider
|
||||
* @return the string before the word, the word itself, and the string after the word
|
||||
*/
|
||||
export function extractWordBefore(input: string, offset: number, delimiters: string = " /"): [string, string, string] {
|
||||
const right = input.slice(offset);
|
||||
|
||||
const leftPlusWord = input.slice(0, offset);
|
||||
const trimmedLeftPlusWord =
|
||||
delimiters.split("").reduce((acc, delimiter) => acc.trimRightChar(delimiter), leftPlusWord);
|
||||
|
||||
const wordStart = Math.max.apply(null, delimiters.split("").map((it) => trimmedLeftPlusWord.lastIndexOf(it)));
|
||||
return [leftPlusWord.slice(0, wordStart + 1), leftPlusWord.slice(wordStart + 1, leftPlusWord.length), right];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension of the given filename, or `""` if it doesn't have one.
|
||||
*
|
||||
|
@ -70,10 +96,13 @@ export function isStandalone(): boolean {
|
|||
/**
|
||||
* Moves the caret to the given position in the given node.
|
||||
*
|
||||
* @param node the node to move the caret in
|
||||
* @param node the node to move the caret in; if `null`, nothing happens
|
||||
* @param position the position from the left to place the caret at
|
||||
*/
|
||||
export function moveCaretTo(node: Node, position: number): void {
|
||||
export function moveCaretTo(node: Node | null, position: number): void {
|
||||
if (node === null)
|
||||
return;
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(node, position);
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import {Commands} from "./Commands";
|
|||
import {Environment} from "./Environment";
|
||||
import {Directory, FileSystem, Path} from "./FileSystem";
|
||||
import {InputHistory} from "./InputHistory";
|
||||
import {InputParser} from "./InputParser";
|
||||
import {Globber, InputParser} from "./InputParser";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {asciiHeaderHtml, IllegalStateError, isStandalone} from "./Shared";
|
||||
import {asciiHeaderHtml, extractWordBefore, IllegalStateError, isStandalone} from "./Shared";
|
||||
import {EscapeCharacters} from "./Terminal";
|
||||
import {UserList} from "./UserList";
|
||||
import {OutputStream, StreamSet} from "./Stream";
|
||||
|
@ -182,6 +182,28 @@ export class Shell {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to auto-complete the given input string at the indicated offset.
|
||||
*
|
||||
* @param input the input to auto-complete in
|
||||
* @param offset the offset of the caret in the given string at which auto-completion is invoked
|
||||
* @return the new input string and caret position
|
||||
*/
|
||||
autoComplete(input: string, offset: number): [string, number] {
|
||||
const cwd = this.environment.get("cwd");
|
||||
const globber = new Globber(this.fileSystem, cwd);
|
||||
|
||||
const [left, word, right] = extractWordBefore(input, offset, " ");
|
||||
const options = globber.glob(word + InputParser.EscapeChar + "*");
|
||||
if (options.length !== 1)
|
||||
return [input, offset];
|
||||
|
||||
let replacement = options[0];
|
||||
if (this.fileSystem.get(Path.interpret(cwd, replacement)) instanceof Directory)
|
||||
replacement += "/";
|
||||
return [left + replacement + right, (left + replacement).length];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Persists the shell's state.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {InputHistory} from "./InputHistory";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {escapeHtml, isStandalone, moveCaretTo, moveCaretToEndOf, parseCssPixels} from "./Shared";
|
||||
import {escapeHtml, extractWordBefore, isStandalone, moveCaretTo, moveCaretToEndOf, parseCssPixels} from "./Shared";
|
||||
import {Shell} from "./Shell";
|
||||
import {Buffer, StreamSet} from "./Stream";
|
||||
|
||||
|
@ -297,8 +297,10 @@ export class Terminal {
|
|||
case "meta":
|
||||
case "os":
|
||||
case "shift":
|
||||
// Do nothing
|
||||
return; // Return without scrolling to 0
|
||||
case "arrowup": {
|
||||
// Display previous entry from history
|
||||
this.inputText = this.inputHistory.previous();
|
||||
|
||||
const inputChild = this.input.firstChild;
|
||||
|
@ -307,6 +309,7 @@ export class Terminal {
|
|||
break;
|
||||
}
|
||||
case "arrowdown": {
|
||||
// Display next entry in history
|
||||
this.inputText = this.inputHistory.next();
|
||||
|
||||
const inputChild = this.input.firstChild;
|
||||
|
@ -314,9 +317,19 @@ export class Terminal {
|
|||
setTimeout(() => moveCaretToEndOf(inputChild), 0);
|
||||
break;
|
||||
}
|
||||
case "tab":
|
||||
case "tab": {
|
||||
// Auto complete
|
||||
let offset = this.inputText.length;
|
||||
if (this.input === document.activeElement)
|
||||
offset = document.getSelection()?.anchorOffset ?? offset;
|
||||
|
||||
const [newInput, newPosition] = this.shell.autoComplete(this.inputText, offset);
|
||||
this.inputText = newInput;
|
||||
setTimeout(() => moveCaretTo(this.input.firstChild, newPosition), 0);
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "c":
|
||||
// Only if focused on the input as to not prevent copying of selected text
|
||||
if (event.ctrlKey) {
|
||||
|
@ -328,6 +341,7 @@ export class Terminal {
|
|||
}
|
||||
break;
|
||||
case "l":
|
||||
// Clear screen
|
||||
if (event.ctrlKey) {
|
||||
this.outputText = "";
|
||||
|
||||
|
@ -337,28 +351,15 @@ export class Terminal {
|
|||
break;
|
||||
case "w":
|
||||
case "backspace":
|
||||
// Remove word before caret
|
||||
if (event.ctrlKey) {
|
||||
let offset = this.inputText.length;
|
||||
if (this.input === document.activeElement)
|
||||
offset = document.getSelection()?.anchorOffset ?? offset;
|
||||
|
||||
const left = this.inputText.slice(0, offset);
|
||||
const right = this.inputText.slice(offset);
|
||||
|
||||
const delimiterIndex = Math.max(
|
||||
left.trimRightChar(" ").lastIndexOf(" "),
|
||||
left.trimRightChar("/").lastIndexOf("/")
|
||||
);
|
||||
const newLeft = delimiterIndex >= 0
|
||||
? left.slice(0, delimiterIndex + 1)
|
||||
: "";
|
||||
const newOffset = offset - (left.length - newLeft.length);
|
||||
|
||||
const [newLeft, word, right] = extractWordBefore(this.inputText, offset);
|
||||
this.inputText = newLeft + right;
|
||||
|
||||
const element = this.input.firstChild;
|
||||
if (element !== null)
|
||||
window.setTimeout(() => moveCaretTo(element, newOffset), 0);
|
||||
window.setTimeout(() => moveCaretTo(this.input.firstChild, offset - word.length), 0);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "mocha";
|
||||
import {expect} from "chai";
|
||||
|
||||
import "../main/js/Extensions"
|
||||
import "../main/js/Extensions";
|
||||
import {Directory, File} from "../main/js/FileSystem";
|
||||
|
||||
|
||||
|
@ -130,11 +130,11 @@ describe("directory", () => {
|
|||
});
|
||||
const copy = directory.copy();
|
||||
|
||||
(<File>directory.get("file")).open("write").write("changed");
|
||||
expect((<File>copy.get("file")).open("read").read()).to.equal("contents");
|
||||
(<File> directory.get("file")).open("write").write("changed");
|
||||
expect((<File> copy.get("file")).open("read").read()).to.equal("contents");
|
||||
|
||||
(<Directory>directory.get("dir")).add("file2", new File());
|
||||
expect((<Directory>copy.get("dir")).nodeCount).to.equal(0);
|
||||
(<Directory> directory.get("dir")).add("file2", new File());
|
||||
expect((<Directory> copy.get("dir")).nodeCount).to.equal(0);
|
||||
|
||||
directory.remove("file");
|
||||
expect(copy.nodeCount).to.equal(2);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "mocha";
|
||||
import {expect} from "chai";
|
||||
|
||||
import "../main/js/Extensions"
|
||||
import "../main/js/Extensions";
|
||||
import {Directory, File, FileSystem, Path} from "../main/js/FileSystem";
|
||||
|
||||
|
||||
|
@ -127,7 +127,7 @@ describe("file system", () => {
|
|||
fileSystem.copy(new Path("/src"), new Path("/dst"), false);
|
||||
file.open("write").write("new");
|
||||
|
||||
expect((<File>fileSystem.get(new Path("/dst"))).open("read").read()).to.equal("old");
|
||||
expect((<File> fileSystem.get(new Path("/dst"))).open("read").read()).to.equal("old");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -196,7 +196,7 @@ describe("file system", () => {
|
|||
expect(fileSystem.has(new Path("/dir"))).to.be.true;
|
||||
});
|
||||
|
||||
it("returns true if a directory exists at the given file path", () =>{
|
||||
it("returns true if a directory exists at the given file path", () => {
|
||||
fileSystem.add(new Path("/dir"), new Directory(), false);
|
||||
|
||||
expect(fileSystem.has(new Path("/dir"))).to.be.true;
|
||||
|
@ -263,7 +263,7 @@ describe("file system", () => {
|
|||
fileSystem.move(new Path("/src"), new Path("/dst"));
|
||||
file.open("write").write("new");
|
||||
|
||||
expect((<File>fileSystem.get(new Path("/dst"))).open("read").read()).to.equal("new");
|
||||
expect((<File> fileSystem.get(new Path("/dst"))).open("read").read()).to.equal("new");
|
||||
});
|
||||
|
||||
it("throws an error if the destination already exists", () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "mocha";
|
||||
import {expect} from "chai";
|
||||
|
||||
import "../main/js/Extensions"
|
||||
import "../main/js/Extensions";
|
||||
import {Path} from "../main/js/FileSystem";
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import "mocha";
|
||||
import {expect} from "chai";
|
||||
|
||||
import "../main/js/Extensions"
|
||||
import {escapeHtml, getFileExtension, parseCssPixels} from "../main/js/Shared";
|
||||
import {escapeHtml, extractWordBefore, getFileExtension, parseCssPixels} from "../main/js/Shared";
|
||||
|
||||
|
||||
describe("shared functions", () => {
|
||||
|
@ -20,6 +19,43 @@ describe("shared functions", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("extractWordBefore", () => {
|
||||
it("returns the word", () => {
|
||||
const parts = extractWordBefore("a b c", 3);
|
||||
expect(parts).to.deep.equal(["a ", "b", " c"]);
|
||||
});
|
||||
|
||||
it("returns the word including trailing whitespace", () => {
|
||||
const parts = extractWordBefore("a b c", 5);
|
||||
expect(parts).to.deep.equal(["a ", "b ", " c"]);
|
||||
});
|
||||
|
||||
it("returns the word including trailing forward slashes", () => {
|
||||
const parts = extractWordBefore("a b// c", 5);
|
||||
expect(parts).to.deep.equal(["a ", "b//", " c"]);
|
||||
});
|
||||
|
||||
it("returns the word including trailing whitespace and forward slashes", () => {
|
||||
const parts = extractWordBefore("a /b// c", 9);
|
||||
expect(parts).to.deep.equal(["a /", "b// ", " c"]);
|
||||
});
|
||||
|
||||
it("returns the word consisting of only forward slashes, delimited by whitespace", () => {
|
||||
const parts = extractWordBefore("a / c", 4);
|
||||
expect(parts).to.deep.equal(["a ", "/ ", "c"]);
|
||||
});
|
||||
|
||||
it("returns the word if there is no preceding delimiter", () => {
|
||||
const parts = extractWordBefore("ab c", 2);
|
||||
expect(parts).to.deep.equal(["", "ab", " c"]);
|
||||
});
|
||||
|
||||
it("returns the word based on custom delimiters", () => {
|
||||
const parts = extractWordBefore("a|b c|d", 6, "|");
|
||||
expect(parts).to.deep.equal(["a|", "b c|", "d"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFileExtension", () => {
|
||||
it("returns the extension of a file", () => {
|
||||
expect(getFileExtension("file.ext")).to.equal("ext");
|
||||
|
|
Loading…
Reference in New Issue