forked from tools/josh
parent
76cdab24df
commit
100f341c3e
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fwdekker.com",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.1",
|
||||
"description": "The source code of [my personal website](https://fwdekker.com/).",
|
||||
"author": "Felix W. Dekker",
|
||||
"repository": {
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
import {IllegalArgumentError} from "./Shared";
|
||||
|
||||
|
||||
/**
|
||||
* A history of inputs that grows downwards and can be accessed in sequence relative to the newest entry.
|
||||
*
|
||||
* An input history keeps a "read index" that starts at `-1`. After entries have been added, calling `previous` will
|
||||
* increase the read index to `0` and return the newest entry. Calling `previous` again will increase the read index to
|
||||
* `1` and return the first-to-last entry. Calling `next` at this point will decrease the read index to `0` and will
|
||||
* return the last entry again. Adding a new entry to the history resets the read index to `-1`. Calling `next` while
|
||||
* the read index is at `-1` will return an empty string without decrementing the read index further. Calling `previous`
|
||||
* at the highest possible index will return the first entry without incrementing the read index further.
|
||||
*/
|
||||
export class InputHistory {
|
||||
/**
|
||||
* The list of previous input.
|
||||
*/
|
||||
private readonly _entries: string[];
|
||||
/**
|
||||
* The current index that the history is being read from.
|
||||
*/
|
||||
private index: number;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new input history.
|
||||
*
|
||||
* @param history the records currently in the history
|
||||
*/
|
||||
constructor(history: string[] = []) {
|
||||
this._entries = history;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a copy of the entries in this history.
|
||||
*/
|
||||
get entries(): string[] {
|
||||
return this._entries.slice();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new input to the bottom of the history and resets the read index.
|
||||
*
|
||||
* @param entry the entry to add
|
||||
*/
|
||||
add(entry: string): void {
|
||||
if (entry.trim() !== "" && entry.trim() !== this._entries[0]?.trim())
|
||||
this._entries.unshift(entry);
|
||||
|
||||
this.resetIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from the history and resets the read index.
|
||||
*/
|
||||
clear(): void {
|
||||
this._entries.length = 0;
|
||||
this.resetIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry at the given index, or an empty string if the index is negative.
|
||||
*
|
||||
* @param index the index to return the entry of, where `0` is the newest entry and `-1` returns an empty string
|
||||
* @throws if the index is out of bounds and not `-1`
|
||||
*/
|
||||
get(index: number): string {
|
||||
if (index < -1 || index >= this._entries.length)
|
||||
throw new IllegalArgumentError(`Index '${index}' is out of bounds.`);
|
||||
|
||||
if (index === -1)
|
||||
return "";
|
||||
|
||||
return this._entries[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next (newer) entry in the history, or an empty string if the read index has gone past the newest
|
||||
* entry.
|
||||
*
|
||||
* The read counter is decremented if possible.
|
||||
*/
|
||||
next(): string {
|
||||
this.index--;
|
||||
if (this.index < -1)
|
||||
this.index = -1;
|
||||
|
||||
return this.get(this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous (older) entry in the history, or the oldest entry if the read index is already at the oldest
|
||||
* entry.
|
||||
*
|
||||
* The read counter is incremented if possible.
|
||||
*/
|
||||
previous(): string {
|
||||
this.index++;
|
||||
if (this.index >= this._entries.length)
|
||||
this.index = this._entries.length - 1;
|
||||
|
||||
return this.get(this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the read index without changing any entries.
|
||||
*/
|
||||
resetIndex(): void {
|
||||
this.index = -1;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import * as Cookies from "js-cookie";
|
||||
import {Environment} from "./Environment";
|
||||
import {Directory, FileSystem, Node} from "./FileSystem";
|
||||
import {InputHistory} from "./InputHistory";
|
||||
import {UserList} from "./UserList";
|
||||
import {InputHistory} from "./Terminal";
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {Commands} from "./Commands";
|
||||
import {Environment} from "./Environment";
|
||||
import {Directory, FileSystem, Path} from "./FileSystem";
|
||||
import {InputHistory} from "./InputHistory";
|
||||
import {InputParser} from "./InputParser";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {asciiHeaderHtml, IllegalStateError} from "./Shared";
|
||||
import {EscapeCharacters, InputHistory} from "./Terminal";
|
||||
import {EscapeCharacters} from "./Terminal";
|
||||
import {UserList} from "./UserList";
|
||||
import {StreamSet} from "./Stream";
|
||||
|
||||
|
@ -144,7 +145,7 @@ export class Shell {
|
|||
return 0;
|
||||
}
|
||||
|
||||
this.inputHistory.addEntry(inputString);
|
||||
this.inputHistory.add(inputString);
|
||||
|
||||
let input;
|
||||
try {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {InputHistory} from "./InputHistory";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {escapeHtml, moveCaretToEndOf, parseCssPixels} from "./Shared";
|
||||
import {Shell} from "./Shell";
|
||||
|
@ -288,11 +289,11 @@ export class Terminal {
|
|||
|
||||
switch (event.key.toLowerCase()) {
|
||||
case "arrowup":
|
||||
this.inputText = this.inputHistory.previousEntry();
|
||||
this.inputText = this.inputHistory.previous();
|
||||
window.setTimeout(() => moveCaretToEndOf(this.input), 0);
|
||||
break;
|
||||
case "arrowdown":
|
||||
this.inputText = this.inputHistory.nextEntry();
|
||||
this.inputText = this.inputHistory.next();
|
||||
window.setTimeout(() => moveCaretToEndOf(this.input), 0);
|
||||
break;
|
||||
case "tab":
|
||||
|
@ -332,114 +333,3 @@ export enum EscapeCharacters {
|
|||
*/
|
||||
ShowInput = "\u0003"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A history of inputs that grows downwards and can be accessed in sequence relative to the newest entry.
|
||||
*
|
||||
* An input history keeps a "read index" that starts at `-1`. After entries have been added, calling `previousEntry`
|
||||
* will increase the read index to `0` and return the newest entry. Calling `previousEntry` again will increase the read
|
||||
* index to `1` and return the first-to-last entry. Calling `nextEntry` at this point will decrease the read index to
|
||||
* `0` and will return the last entry again. Adding a new entry to the history resets the read index to `-1`. Calling
|
||||
* `nextEntry` while the read index is at `-1` will return an empty string without decrementing the read index further.
|
||||
* Calling `previousEntry` at the highest possible index will return the first entry without incrementing the read index
|
||||
* further.
|
||||
*/
|
||||
export class InputHistory {
|
||||
/**
|
||||
* The list of previous input.
|
||||
*/
|
||||
private readonly _entries: string[];
|
||||
/**
|
||||
* The current index that the history is being read from.
|
||||
*/
|
||||
private index: number;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new input history.
|
||||
*
|
||||
* @param history the records currently in the history
|
||||
*/
|
||||
constructor(history: string[] = []) {
|
||||
this._entries = history;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a copy of the entries in this history.
|
||||
*/
|
||||
get entries(): string[] {
|
||||
return this._entries.slice();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new input to the bottom of the history and resets the read index.
|
||||
*
|
||||
* @param entry the entry to add
|
||||
*/
|
||||
addEntry(entry: string): void {
|
||||
if (entry.trim() !== "" && entry.trim() !== this._entries[0]?.trim())
|
||||
this._entries.unshift(entry);
|
||||
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from the history and resets the read index.
|
||||
*/
|
||||
clear(): void {
|
||||
this._entries.length = 0;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry at the given index, or an empty string if the index is negative.
|
||||
*
|
||||
* @param index the index to return the entry of, where `0` is the newest entry and `-1` returns an empty string
|
||||
* @throws if the index is out of bounds and not `-1`
|
||||
*/
|
||||
getEntry(index: number): string {
|
||||
if (index === -1)
|
||||
return "";
|
||||
|
||||
return this._entries[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next (newer) entry in the history, or an empty string if the read index has gone past the newest
|
||||
* entry.
|
||||
*
|
||||
* The read counter is decremented if possible.
|
||||
*/
|
||||
nextEntry(): string {
|
||||
this.index--;
|
||||
if (this.index < -1)
|
||||
this.index = -1;
|
||||
|
||||
return this.getEntry(this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous (older) entry in the history, or the oldest entry if the read index is already at the oldest
|
||||
* entry.
|
||||
*
|
||||
* The read counter is incremented if possible.
|
||||
*/
|
||||
previousEntry(): string {
|
||||
this.index++;
|
||||
if (this.index >= this._entries.length)
|
||||
this.index = this._entries.length - 1;
|
||||
|
||||
return this.getEntry(this.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the read index without changing any entries.
|
||||
*/
|
||||
resetIndex(): void {
|
||||
this.index = -1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
import "mocha";
|
||||
import {expect} from "chai";
|
||||
|
||||
import {InputHistory} from "../main/js/InputHistory";
|
||||
|
||||
|
||||
describe("input history", () => {
|
||||
let history: InputHistory;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
history = new InputHistory();
|
||||
});
|
||||
|
||||
|
||||
describe("entries", () => {
|
||||
it("returns a copy of the entries", () => {
|
||||
history.add("old");
|
||||
|
||||
history.entries[0] = "new";
|
||||
|
||||
expect(history.entries[0]).to.equal("old");
|
||||
});
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
it("does not add empty entries", () => {
|
||||
history.add("");
|
||||
|
||||
expect(history.entries.length).to.equal(0);
|
||||
});
|
||||
|
||||
it("does not add duplicate entries", () => {
|
||||
history.add("command");
|
||||
history.add("command");
|
||||
|
||||
expect(history.entries.length).to.equal(1);
|
||||
});
|
||||
|
||||
it("does not add duplicate entries, ignoring leading and trailing whitespace", () => {
|
||||
history.add("command ");
|
||||
history.add(" command");
|
||||
|
||||
expect(history.entries.length).to.equal(1);
|
||||
});
|
||||
|
||||
it("adds entries including leading and trailing whitespace", () => {
|
||||
history.add(" command ");
|
||||
|
||||
expect(history.entries[0]).to.equal(" command ");
|
||||
});
|
||||
|
||||
it("resets the index", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
history.previous();
|
||||
|
||||
history.add("command3");
|
||||
|
||||
expect(history.previous()).to.equal("command3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("removes all entries", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
|
||||
history.clear();
|
||||
|
||||
expect(history.entries).to.have.length(0);
|
||||
});
|
||||
|
||||
it("resets the index", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
history.previous();
|
||||
history.previous();
|
||||
|
||||
history.clear();
|
||||
|
||||
expect(history.previous()).to.equal("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("throws an error if the index is below -1", () => {
|
||||
expect(() => history.get(-2)).to.throw();
|
||||
});
|
||||
|
||||
it("throws an error if the index is greater than or equal to the history size", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
|
||||
expect(() => history.get(2)).to.throw();
|
||||
});
|
||||
|
||||
it("returns an empty string if the index is -1 and the history is empty", () => {
|
||||
expect(history.get(-1)).to.equal("");
|
||||
});
|
||||
|
||||
it("returns an empty string if the index is -1 and the history is non-empty", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
|
||||
expect(history.get(-1)).to.equal("");
|
||||
});
|
||||
|
||||
it("returns the most recent entry at index 0", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
|
||||
expect(history.get(0)).to.equal("command2");
|
||||
});
|
||||
|
||||
it("returns the least recent entry at the highest index", () => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
|
||||
expect(history.get(1)).to.equal("command1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("next", () => {
|
||||
beforeEach(() => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
history.add("command3");
|
||||
});
|
||||
|
||||
|
||||
it("returns an empty string at the first call", () => {
|
||||
expect(history.next()).to.equal("");
|
||||
});
|
||||
|
||||
it("returns an empty string if the index is -1", () => {
|
||||
history.next();
|
||||
history.next();
|
||||
history.next();
|
||||
|
||||
expect(history.next()).to.equal("");
|
||||
});
|
||||
|
||||
it("returns an empty string if the index is currently at the most recent entry", () => {
|
||||
history.previous();
|
||||
|
||||
expect(history.next()).to.equal("");
|
||||
});
|
||||
|
||||
it("returns the most recent entry if the index is currently at the second-most recent", () => {
|
||||
history.previous();
|
||||
history.previous();
|
||||
|
||||
expect(history.next()).to.equal("command3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("previous", () => {
|
||||
beforeEach(() => {
|
||||
history.add("command1");
|
||||
history.add("command2");
|
||||
history.add("command3");
|
||||
});
|
||||
|
||||
|
||||
it("returns the newest entry at the first call", () => {
|
||||
expect(history.previous()).to.equal("command3");
|
||||
});
|
||||
|
||||
it("returns the second-newest entry at the second call", () => {
|
||||
history.previous();
|
||||
|
||||
expect(history.previous()).to.equal("command2");
|
||||
});
|
||||
|
||||
it("always returns the oldest entry once the index has reached the oldest entry", () => {
|
||||
history.previous();
|
||||
history.previous();
|
||||
history.previous();
|
||||
history.previous();
|
||||
|
||||
expect(history.previous()).to.equal("command1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetIndex", () => {
|
||||
// Covered indirectly in `add`
|
||||
});
|
||||
});
|
|
@ -110,7 +110,7 @@ describe("paths", () => {
|
|||
|
||||
describe("ancestors", () => {
|
||||
it("returns an empty list for the root path", () => {
|
||||
expect(new Path("/").ancestors.map(it => it.toString())).to.be.empty;
|
||||
expect(new Path("/").ancestors.map(it => it.toString())).to.have.length(0);
|
||||
});
|
||||
|
||||
it("returns the root path for a path to a subdirectory of root", () => {
|
||||
|
@ -152,11 +152,11 @@ describe("paths", () => {
|
|||
|
||||
describe("ancestors until", () => {
|
||||
it("returns no ancestors between root and itself", () => {
|
||||
expect(new Path("/").getAncestorsUntil(new Path("/"))).to.be.empty;
|
||||
expect(new Path("/").getAncestorsUntil(new Path("/"))).to.have.length(0);
|
||||
});
|
||||
|
||||
it("returns no ancestors between a non-root and itself", () => {
|
||||
expect(new Path("/dir/file").getAncestorsUntil(new Path("/dir/file"))).to.be.empty;
|
||||
expect(new Path("/dir/file").getAncestorsUntil(new Path("/dir/file"))).to.have.length(0);
|
||||
});
|
||||
|
||||
it("returns the root parent if there are no other ancestors in between", () => {
|
||||
|
|
Loading…
Reference in New Issue