Add Option type to remove the need to use null/undefined
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good
This commit is contained in:
parent
76eacd835f
commit
1b3e9d9796
@ -3,6 +3,7 @@ import Buffer from './buffer.ts';
|
|||||||
import Document from './document.ts';
|
import Document from './document.ts';
|
||||||
import Editor from './editor.ts';
|
import Editor from './editor.ts';
|
||||||
import { highlightToColor, HighlightType } from './highlight.ts';
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
||||||
|
import _Option, { None, Some } from './option.ts';
|
||||||
import Position from './position.ts';
|
import Position from './position.ts';
|
||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
|
|
||||||
@ -12,11 +13,10 @@ import { getTestRunner } from './runtime.ts';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
assertStrictEquals: assertEquals,
|
assertStrictEquals: assertEquals,
|
||||||
assertEquals: assertLooseEquals,
|
assertEquals: assertEquivalent,
|
||||||
assertExists,
|
assertExists,
|
||||||
assertInstanceOf,
|
assertInstanceOf,
|
||||||
assertNotEquals,
|
assertNotEquals,
|
||||||
assertNull,
|
|
||||||
assertFalse,
|
assertFalse,
|
||||||
assertTrue,
|
assertTrue,
|
||||||
testSuite,
|
testSuite,
|
||||||
@ -30,8 +30,6 @@ const THIS_FILE = './src/common/all_test.ts';
|
|||||||
|
|
||||||
const fnTest = () => {
|
const fnTest = () => {
|
||||||
const {
|
const {
|
||||||
some,
|
|
||||||
none,
|
|
||||||
arrayInsert,
|
arrayInsert,
|
||||||
noop,
|
noop,
|
||||||
posSub,
|
posSub,
|
||||||
@ -48,39 +46,25 @@ const fnTest = () => {
|
|||||||
} = Fn;
|
} = Fn;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'some()': () => {
|
|
||||||
assertFalse(some(null));
|
|
||||||
assertFalse(some(void 0));
|
|
||||||
assertFalse(some(undefined));
|
|
||||||
assertTrue(some(0));
|
|
||||||
assertTrue(some(false));
|
|
||||||
},
|
|
||||||
'none()': () => {
|
|
||||||
assertTrue(none(null));
|
|
||||||
assertTrue(none(void 0));
|
|
||||||
assertTrue(none(undefined));
|
|
||||||
assertFalse(none(0));
|
|
||||||
assertFalse(none(false));
|
|
||||||
},
|
|
||||||
'arrayInsert() strings': () => {
|
'arrayInsert() strings': () => {
|
||||||
const a = ['😺', '😸', '😹'];
|
const a = ['😺', '😸', '😹'];
|
||||||
const b = arrayInsert(a, 1, 'x');
|
const b = arrayInsert(a, 1, 'x');
|
||||||
const c = ['😺', 'x', '😸', '😹'];
|
const c = ['😺', 'x', '😸', '😹'];
|
||||||
assertLooseEquals(b, c);
|
assertEquivalent(b, c);
|
||||||
|
|
||||||
const d = arrayInsert(c, 17, 'y');
|
const d = arrayInsert(c, 17, 'y');
|
||||||
const e = ['😺', 'x', '😸', '😹', 'y'];
|
const e = ['😺', 'x', '😸', '😹', 'y'];
|
||||||
assertLooseEquals(d, e);
|
assertEquivalent(d, e);
|
||||||
|
|
||||||
assertLooseEquals(arrayInsert([], 0, 'foo'), ['foo']);
|
assertEquivalent(arrayInsert([], 0, 'foo'), ['foo']);
|
||||||
},
|
},
|
||||||
'arrayInsert() numbers': () => {
|
'arrayInsert() numbers': () => {
|
||||||
const a = [1, 3, 5];
|
const a = [1, 3, 5];
|
||||||
const b = [1, 3, 4, 5];
|
const b = [1, 3, 4, 5];
|
||||||
assertLooseEquals(arrayInsert(a, 2, 4), b);
|
assertEquivalent(arrayInsert(a, 2, 4), b);
|
||||||
|
|
||||||
const c = [1, 2, 3, 4, 5];
|
const c = [1, 2, 3, 4, 5];
|
||||||
assertLooseEquals(arrayInsert(b, 1, 2), c);
|
assertEquivalent(arrayInsert(b, 1, 2), c);
|
||||||
},
|
},
|
||||||
'noop fn': () => {
|
'noop fn': () => {
|
||||||
assertExists(noop);
|
assertExists(noop);
|
||||||
@ -106,7 +90,7 @@ const fnTest = () => {
|
|||||||
assertEquals(ord('a'), 97);
|
assertEquals(ord('a'), 97);
|
||||||
},
|
},
|
||||||
'strChars() properly splits strings into unicode characters': () => {
|
'strChars() properly splits strings into unicode characters': () => {
|
||||||
assertLooseEquals(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
assertEquivalent(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
||||||
},
|
},
|
||||||
'ctrlKey()': () => {
|
'ctrlKey()': () => {
|
||||||
const ctrl_a = ctrlKey('a');
|
const ctrl_a = ctrlKey('a');
|
||||||
@ -432,6 +416,12 @@ const EditorTest = {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const OptionTest = {
|
||||||
|
// @TODO implement Option tests
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
const PositionTest = {
|
const PositionTest = {
|
||||||
'.default': () => {
|
'.default': () => {
|
||||||
const p = Position.default();
|
const p = Position.default();
|
||||||
@ -498,12 +488,12 @@ const RowTest = {
|
|||||||
},
|
},
|
||||||
'.find': () => {
|
'.find': () => {
|
||||||
const normalRow = Row.from('For whom the bell tolls');
|
const normalRow = Row.from('For whom the bell tolls');
|
||||||
assertEquals(normalRow.find('who'), 4);
|
assertEquivalent(normalRow.find('who'), Some(4));
|
||||||
assertNull(normalRow.find('foo'));
|
assertEquals(normalRow.find('foo'), None);
|
||||||
|
|
||||||
const emojiRow = Row.from('😺😸😹');
|
const emojiRow = Row.from('😺😸😹');
|
||||||
assertEquals(emojiRow.find('😹'), 2);
|
assertEquivalent(emojiRow.find('😹'), Some(2));
|
||||||
assertNull(emojiRow.find('🤰🏼'));
|
assertEquals(emojiRow.find('🤰🏼'), None);
|
||||||
},
|
},
|
||||||
'.byteIndexToCharIndex': () => {
|
'.byteIndexToCharIndex': () => {
|
||||||
// Each 'character' is two bytes
|
// Each 'character' is two bytes
|
||||||
@ -528,7 +518,7 @@ const RowTest = {
|
|||||||
},
|
},
|
||||||
'.cxToRx, .rxToCx': () => {
|
'.cxToRx, .rxToCx': () => {
|
||||||
const row = Row.from('foo\tbar\tbaz');
|
const row = Row.from('foo\tbar\tbaz');
|
||||||
row.update();
|
row.update(None);
|
||||||
assertNotEquals(row.chars, row.rchars);
|
assertNotEquals(row.chars, row.rchars);
|
||||||
assertNotEquals(row.size, row.rsize);
|
assertNotEquals(row.size, row.rsize);
|
||||||
assertEquals(row.size, 11);
|
assertEquals(row.size, 11);
|
||||||
@ -545,7 +535,9 @@ const RowTest = {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
const SearchTest = {};
|
const SearchTest = {
|
||||||
|
// @TODO implement Search tests
|
||||||
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Test Suite Setup
|
// Test Suite Setup
|
||||||
@ -559,6 +551,7 @@ testSuite({
|
|||||||
Buffer: BufferTest,
|
Buffer: BufferTest,
|
||||||
Document: DocumentTest,
|
Document: DocumentTest,
|
||||||
Editor: EditorTest,
|
Editor: EditorTest,
|
||||||
|
Option: OptionTest,
|
||||||
Position: PositionTest,
|
Position: PositionTest,
|
||||||
Row: RowTest,
|
Row: RowTest,
|
||||||
Search: SearchTest,
|
Search: SearchTest,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
import { arrayInsert, some, strlen } from './fns.ts';
|
import { arrayInsert, strlen } from './fns.ts';
|
||||||
import { HighlightType } from './highlight.ts';
|
import { HighlightType } from './highlight.ts';
|
||||||
|
import Option, { None, Some } from './option.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
import { Position } from './types.ts';
|
import { Position } from './types.ts';
|
||||||
import { Search } from './search.ts';
|
import { Search } from './search.ts';
|
||||||
@ -26,7 +27,7 @@ export class Document {
|
|||||||
|
|
||||||
public static default(): Document {
|
public static default(): Document {
|
||||||
const self = new Document();
|
const self = new Document();
|
||||||
self.#search.parent = self;
|
self.#search.parent = Some(self);
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@ -68,15 +69,17 @@ export class Document {
|
|||||||
|
|
||||||
public resetFind(): void {
|
public resetFind(): void {
|
||||||
this.#search = new Search();
|
this.#search = new Search();
|
||||||
this.#search.parent = this;
|
this.#search.parent = Some(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(
|
public find(
|
||||||
q: string,
|
q: string,
|
||||||
key: string,
|
key: string,
|
||||||
): Position | null {
|
): Option<Position> {
|
||||||
const potential = this.#search.search(q, key);
|
const possible = this.#search.search(q, key);
|
||||||
if (some(potential) && potential instanceof Position) {
|
if (possible.isSome()) {
|
||||||
|
const potential = possible.unwrap();
|
||||||
|
|
||||||
// Update highlight of search match
|
// Update highlight of search match
|
||||||
const row = this.#rows[potential.y];
|
const row = this.#rows[potential.y];
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ export class Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return potential;
|
return possible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public insert(at: Position, c: string): void {
|
public insert(at: Position, c: string): void {
|
||||||
@ -102,7 +105,7 @@ export class Document {
|
|||||||
this.insertRow(this.numRows, c);
|
this.insertRow(this.numRows, c);
|
||||||
} else {
|
} else {
|
||||||
this.#rows[at.y].insertChar(at.x, c);
|
this.#rows[at.y].insertChar(at.x, c);
|
||||||
this.#rows[at.y].update();
|
this.#rows[at.y].update(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
@ -126,7 +129,7 @@ export class Document {
|
|||||||
// Split the current row, and insert a new
|
// Split the current row, and insert a new
|
||||||
// row with the leftovers
|
// row with the leftovers
|
||||||
const newRow = this.#rows[at.y].split(at.x);
|
const newRow = this.#rows[at.y].split(at.x);
|
||||||
newRow.update();
|
newRow.update(None);
|
||||||
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
@ -165,7 +168,7 @@ export class Document {
|
|||||||
row.delete(at.x);
|
row.delete(at.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.update();
|
row.update(None);
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
@ -176,12 +179,12 @@ export class Document {
|
|||||||
|
|
||||||
public insertRow(at: number = this.numRows, s: string = ''): void {
|
public insertRow(at: number = this.numRows, s: string = ''): void {
|
||||||
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
||||||
this.#rows[at].update();
|
this.#rows[at].update(None);
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch?: string): void {
|
public highlight(searchMatch: Option<string>): void {
|
||||||
this.#rows.forEach((row) => {
|
this.#rows.forEach((row) => {
|
||||||
row.update(searchMatch);
|
row.update(searchMatch);
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,11 @@ import {
|
|||||||
ctrlKey,
|
ctrlKey,
|
||||||
isControl,
|
isControl,
|
||||||
maxAdd,
|
maxAdd,
|
||||||
none,
|
|
||||||
posSub,
|
posSub,
|
||||||
readKey,
|
readKey,
|
||||||
some,
|
|
||||||
truncate,
|
truncate,
|
||||||
} from './fns.ts';
|
} from './fns.ts';
|
||||||
|
import Option, { None, Some } from './option.ts';
|
||||||
import { getRuntime, log, LogLevel } from './runtime.ts';
|
import { getRuntime, log, LogLevel } from './runtime.ts';
|
||||||
import { ITerminalSize, Position } from './types.ts';
|
import { ITerminalSize, Position } from './types.ts';
|
||||||
|
|
||||||
@ -100,12 +99,12 @@ class Editor {
|
|||||||
public async save(): Promise<void> {
|
public async save(): Promise<void> {
|
||||||
if (this.#filename === '') {
|
if (this.#filename === '') {
|
||||||
const filename = await this.prompt('Save as: %s (ESC to cancel)');
|
const filename = await this.prompt('Save as: %s (ESC to cancel)');
|
||||||
if (filename === null) {
|
if (filename.isNone()) {
|
||||||
this.setStatusMessage('Save aborted');
|
this.setStatusMessage('Save aborted');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#filename = filename;
|
this.#filename = filename.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#document.save(this.#filename);
|
await this.#document.save(this.#filename);
|
||||||
@ -236,7 +235,7 @@ class Editor {
|
|||||||
public async prompt(
|
public async prompt(
|
||||||
p: string,
|
p: string,
|
||||||
callback?: (query: string, char: string) => void,
|
callback?: (query: string, char: string) => void,
|
||||||
): Promise<string | null> {
|
): Promise<Option<string>> {
|
||||||
const { term } = await getRuntime();
|
const { term } = await getRuntime();
|
||||||
|
|
||||||
let res = '';
|
let res = '';
|
||||||
@ -256,7 +255,7 @@ class Editor {
|
|||||||
await this.refreshScreen();
|
await this.refreshScreen();
|
||||||
for await (const chunk of term.inputLoop()) {
|
for await (const chunk of term.inputLoop()) {
|
||||||
const char = readKey(chunk);
|
const char = readKey(chunk);
|
||||||
if (none(char)) {
|
if (char.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,14 +272,14 @@ class Editor {
|
|||||||
this.setStatusMessage('');
|
this.setStatusMessage('');
|
||||||
maybeCallback(res, char);
|
maybeCallback(res, char);
|
||||||
|
|
||||||
return null;
|
return None;
|
||||||
|
|
||||||
// Return the input and end the prompt
|
// Return the input and end the prompt
|
||||||
case KeyCommand.Enter:
|
case KeyCommand.Enter:
|
||||||
if (res.length > 0) {
|
if (res.length > 0) {
|
||||||
this.setStatusMessage('');
|
this.setStatusMessage('');
|
||||||
maybeCallback(res, char);
|
maybeCallback(res, char);
|
||||||
return res;
|
return Some(res);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -314,11 +313,11 @@ class Editor {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (some(query) && query.length > 0) {
|
if (query.length > 0) {
|
||||||
const pos = this.#document.find(query, key);
|
const pos = this.#document.find(query, key);
|
||||||
if (pos !== null) {
|
if (pos.isSome()) {
|
||||||
// We have a match here
|
// We have a match here
|
||||||
this.#cursor = pos;
|
this.#cursor = pos.unwrap();
|
||||||
this.scroll();
|
this.scroll();
|
||||||
} else {
|
} else {
|
||||||
this.setStatusMessage('Not found');
|
this.setStatusMessage('Not found');
|
||||||
|
@ -11,20 +11,6 @@ const decoder = new TextDecoder();
|
|||||||
*/
|
*/
|
||||||
export const noop = () => {};
|
export const noop = () => {};
|
||||||
|
|
||||||
/**
|
|
||||||
* Does a value exist? (not null or undefined)
|
|
||||||
*/
|
|
||||||
export function some(v: unknown): boolean {
|
|
||||||
return v !== null && typeof v !== 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the value null or undefined?
|
|
||||||
*/
|
|
||||||
export function none(v: unknown): boolean {
|
|
||||||
return v === null || typeof v === 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert input from ANSI escape sequences into a form
|
* Convert input from ANSI escape sequences into a form
|
||||||
* that can be more easily mapped to editor commands
|
* that can be more easily mapped to editor commands
|
||||||
@ -149,10 +135,10 @@ export function ord(s: string): number {
|
|||||||
/**
|
/**
|
||||||
* Split a string by graphemes, not just bytes
|
* Split a string by graphemes, not just bytes
|
||||||
*
|
*
|
||||||
* @param s - the string to split into 'characters'
|
* @param s - the string to split into unicode code points
|
||||||
*/
|
*/
|
||||||
export function strChars(s: string): string[] {
|
export function strChars(s: string): string[] {
|
||||||
return s.split(/(?:)/u);
|
return [...s];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
175
src/common/option.ts
Normal file
175
src/common/option.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Rust-style optional type
|
||||||
|
*
|
||||||
|
* Based on https://gist.github.com/s-panferov/575da5a7131c285c0539
|
||||||
|
*/
|
||||||
|
export default interface Option<T> {
|
||||||
|
isSome(): boolean;
|
||||||
|
isNone(): boolean;
|
||||||
|
isSomeAnd(fn: (a: T) => boolean): boolean;
|
||||||
|
isNoneAnd(fn: () => boolean): boolean;
|
||||||
|
unwrap(): T | never;
|
||||||
|
unwrapOr(def: T): T;
|
||||||
|
unwrapOrElse(f: () => T): T;
|
||||||
|
map<U>(f: (a: T) => U): Option<U>;
|
||||||
|
mapOr<U>(def: U, f: (a: T) => U): U;
|
||||||
|
mapOrElse<U>(def: () => U, f: (a: T) => U): U;
|
||||||
|
and<U>(optb: Option<U>): Option<U>;
|
||||||
|
andThen<U>(f: (a: T) => Option<U>): Option<U>;
|
||||||
|
or(optb: Option<T>): Option<T>;
|
||||||
|
orElse(f: () => Option<T>): Option<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Some<T> implements Option<T> {
|
||||||
|
private value: T;
|
||||||
|
|
||||||
|
constructor(v: T) {
|
||||||
|
this.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wrapNull<T>(value: T): Option<T> {
|
||||||
|
if (value == null) {
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
return new _Some<T>(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(fn: (a: T) => U): Option<U> {
|
||||||
|
return new _Some(fn(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
mapOr<U>(_def: U, f: (a: T) => U): U {
|
||||||
|
return f(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapOrElse<U>(_def: () => U, f: (a: T) => U): U {
|
||||||
|
return f(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSome(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSomeAnd(fn: (a: T) => boolean): boolean {
|
||||||
|
return fn(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNoneAnd(_fn: () => boolean): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrap(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapOr(_def: T): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapOrElse(_f: () => T): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
and<U>(optb: Option<U>): Option<U> {
|
||||||
|
return optb;
|
||||||
|
}
|
||||||
|
|
||||||
|
andThen<U>(f: (a: T) => Option<U>): Option<U> {
|
||||||
|
return f(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
or(_optb: Option<T>): Option<T> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
orElse(_f: () => Option<T>): Option<T> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return 'Some ' + this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _None<T> implements Option<T> {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(_fn: (a: T) => U): Option<U> {
|
||||||
|
return <Option<U>> _None._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSome(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNone(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSomeAnd(_fn: (a: T) => boolean): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNoneAnd(fn: () => boolean): boolean {
|
||||||
|
return fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrap(): never {
|
||||||
|
console.error('None.unwrap()');
|
||||||
|
throw 'None.get';
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapOr(def: T): T {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapOrElse(f: () => T): T {
|
||||||
|
return f();
|
||||||
|
}
|
||||||
|
|
||||||
|
mapOr<U>(def: U, _f: (a: T) => U): U {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapOrElse<U>(def: () => U, _f: (a: T) => U): U {
|
||||||
|
return def();
|
||||||
|
}
|
||||||
|
|
||||||
|
and<U>(_optb: Option<U>): Option<U> {
|
||||||
|
return _None.instance<U>();
|
||||||
|
}
|
||||||
|
|
||||||
|
andThen<U>(_f: (a: T) => Option<U>): Option<U> {
|
||||||
|
return _None.instance<U>();
|
||||||
|
}
|
||||||
|
|
||||||
|
or(optb: Option<T>): Option<T> {
|
||||||
|
return optb;
|
||||||
|
}
|
||||||
|
|
||||||
|
orElse(f: () => Option<T>): Option<T> {
|
||||||
|
return f();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _instance: Option<any> = new _None();
|
||||||
|
|
||||||
|
public static instance<X>(): Option<X> {
|
||||||
|
return <Option<X>> _None._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const None: Option<any> = _None.instance();
|
||||||
|
|
||||||
|
export function Some<T>(value: T): Option<T> {
|
||||||
|
return _Some.wrapNull(value);
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import { SCROLL_TAB_SIZE } from './config.ts';
|
|
||||||
import { arrayInsert, isAsciiDigit, some, strChars } from './fns.ts';
|
|
||||||
import { highlightToColor, HighlightType } from './highlight.ts';
|
|
||||||
import Ansi from './ansi.ts';
|
import Ansi from './ansi.ts';
|
||||||
|
|
||||||
|
import { SCROLL_TAB_SIZE } from './config.ts';
|
||||||
|
import { arrayInsert, isAsciiDigit, strChars } from './fns.ts';
|
||||||
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
||||||
|
import Option, { None, Some } from './option.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One row of text in the current document. In order to handle
|
* One row of text in the current document. In order to handle
|
||||||
* multi-byte graphemes, all operations are done on an
|
* multi-byte graphemes, all operations are done on an
|
||||||
@ -56,7 +58,7 @@ export class Row {
|
|||||||
|
|
||||||
public append(s: string): void {
|
public append(s: string): void {
|
||||||
this.chars = this.chars.concat(strChars(s));
|
this.chars = this.chars.concat(strChars(s));
|
||||||
this.update();
|
this.update(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertChar(at: number, c: string): void {
|
public insertChar(at: number, c: string): void {
|
||||||
@ -74,7 +76,7 @@ export class Row {
|
|||||||
public split(at: number): Row {
|
public split(at: number): Row {
|
||||||
const newRow = new Row(this.chars.slice(at));
|
const newRow = new Row(this.chars.slice(at));
|
||||||
this.chars = this.chars.slice(0, at);
|
this.chars = this.chars.slice(0, at);
|
||||||
this.update();
|
this.update(None);
|
||||||
|
|
||||||
return newRow;
|
return newRow;
|
||||||
}
|
}
|
||||||
@ -92,25 +94,31 @@ export class Row {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the current row for the specified string, and return
|
* Search the current row for the specified string, and return
|
||||||
* the index of the start of that match
|
* the 'character' index of the start of that match
|
||||||
*/
|
*/
|
||||||
public find(s: string, offset: number = 0): number | null {
|
public find(s: string, offset: number = 0): Option<number> {
|
||||||
const thisStr = this.toString();
|
const thisStr = this.toString();
|
||||||
if (!this.toString().includes(s)) {
|
if (!this.toString().includes(s)) {
|
||||||
return null;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
const byteCount = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
// Look for the search query `s`, starting from the 'character' `offset`
|
||||||
|
const byteIndex = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
||||||
|
|
||||||
|
// No match after the specified offset
|
||||||
|
if (byteIndex < 0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// In many cases, the string length will
|
// In many cases, the string length will
|
||||||
// equal the number of characters. So
|
// equal the number of characters. So
|
||||||
// searching is fairly easy
|
// searching is fairly easy
|
||||||
if (thisStr.length === this.chars.length) {
|
if (thisStr.length === this.chars.length) {
|
||||||
return byteCount;
|
return Some(byteIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emoji/Extended Unicode-friendly search
|
// Emoji/Extended Unicode-friendly search
|
||||||
return this.byteIndexToCharIndex(byteCount);
|
return Some(this.byteIndexToCharIndex(byteIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,6 +187,9 @@ export class Row {
|
|||||||
return charIndex;
|
return charIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The char index will be the same size or smaller than
|
||||||
|
// the JS string index, as a 'character' can consist
|
||||||
|
// of multiple JS string indicies
|
||||||
return this.chars.slice(0, charIndex).reduce(
|
return this.chars.slice(0, charIndex).reduce(
|
||||||
(prev, current) => prev += current.length,
|
(prev, current) => prev += current.length,
|
||||||
0,
|
0,
|
||||||
@ -189,21 +200,22 @@ export class Row {
|
|||||||
return this.chars.join('');
|
return this.chars.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(searchMatch?: string): void {
|
public update(word: Option<string>): void {
|
||||||
const newString = this.chars.join('').replaceAll(
|
const newString = this.chars.join('').replaceAll(
|
||||||
'\t',
|
'\t',
|
||||||
' '.repeat(SCROLL_TAB_SIZE),
|
' '.repeat(SCROLL_TAB_SIZE),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.rchars = strChars(newString);
|
this.rchars = strChars(newString);
|
||||||
this.highlight(searchMatch);
|
this.highlight(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch?: string): void {
|
public highlight(word: Option<string>): void {
|
||||||
const highlighting = [];
|
const highlighting = [];
|
||||||
|
// let searchIndex = 0;
|
||||||
|
|
||||||
if (some(searchMatch)) {
|
if (word.isSome()) {
|
||||||
// TODO: highlight search here
|
// const searchMatch = this.find(word.unwrap(), searchIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ch of this.rchars) {
|
for (const ch of this.rchars) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Position } from './types.ts';
|
|
||||||
import { KeyCommand } from './ansi.ts';
|
|
||||||
import Document from './document.ts';
|
import Document from './document.ts';
|
||||||
|
|
||||||
|
import { KeyCommand } from './ansi.ts';
|
||||||
|
import Option, { None } from './option.ts';
|
||||||
|
import { Position } from './types.ts';
|
||||||
|
|
||||||
enum SearchDirection {
|
enum SearchDirection {
|
||||||
Forward = 1,
|
Forward = 1,
|
||||||
Backward = -1,
|
Backward = -1,
|
||||||
@ -11,7 +13,7 @@ export class Search {
|
|||||||
private lastMatch: number = -1;
|
private lastMatch: number = -1;
|
||||||
private current: number = -1;
|
private current: number = -1;
|
||||||
private direction: SearchDirection = SearchDirection.Forward;
|
private direction: SearchDirection = SearchDirection.Forward;
|
||||||
public parent: Document | null = null;
|
public parent: Option<Document> = None;
|
||||||
|
|
||||||
private parseInput(key: string) {
|
private parseInput(key: string) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@ -48,29 +50,31 @@ export class Search {
|
|||||||
return this.current;
|
return this.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
public search(q: string, key: string): Position | null {
|
public search(q: string, key: string): Option<Position> {
|
||||||
if (this.parent === null) {
|
if (this.parent.isNone()) {
|
||||||
return null;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parent = this.parent.unwrap();
|
||||||
|
|
||||||
this.parseInput(key);
|
this.parseInput(key);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (; i < this.parent.numRows; i++) {
|
for (; i < parent.numRows; i++) {
|
||||||
const current = this.getNextRow(this.parent.numRows);
|
const current = this.getNextRow(parent.numRows);
|
||||||
const row = this.parent.row(current);
|
const row = parent.row(current);
|
||||||
|
|
||||||
if (row === null) {
|
if (row === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const possible = row.find(q);
|
const possible = row.find(q);
|
||||||
if (possible !== null) {
|
if (possible.isSome()) {
|
||||||
this.lastMatch = current;
|
this.lastMatch = current;
|
||||||
return Position.at(possible, current);
|
return possible.map((p: number) => Position.at(p, current));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user