Implement basic searching
This commit is contained in:
parent
32e4030a4a
commit
f5599b5192
@ -24,6 +24,7 @@ const BunTestBase: ITestBase = {
|
|||||||
expect(actual).toBeInstanceOf(expectedType),
|
expect(actual).toBeInstanceOf(expectedType),
|
||||||
assertNotEquals: (actual: unknown, expected: unknown) =>
|
assertNotEquals: (actual: unknown, expected: unknown) =>
|
||||||
expect(actual).not.toBe(expected),
|
expect(actual).not.toBe(expected),
|
||||||
|
assertNull: (actual: unknown) => expect(actual).toBeNull(),
|
||||||
assertStrictEquals: (actual: unknown, expected: unknown) =>
|
assertStrictEquals: (actual: unknown, expected: unknown) =>
|
||||||
expect(actual).toBe(expected),
|
expect(actual).toBe(expected),
|
||||||
assertTrue: (actual: boolean) => expect(actual).toBe(true),
|
assertTrue: (actual: boolean) => expect(actual).toBe(true),
|
||||||
|
@ -3,7 +3,7 @@ import Document from './document.ts';
|
|||||||
import Editor from './editor.ts';
|
import Editor from './editor.ts';
|
||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
import { Ansi, KeyCommand } from './ansi.ts';
|
import { Ansi, KeyCommand } from './ansi.ts';
|
||||||
import { defaultTerminalSize } from './config.ts';
|
import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
|
||||||
import { getTestRunner } from './runtime.ts';
|
import { getTestRunner } from './runtime.ts';
|
||||||
import { Position } from './types.ts';
|
import { Position } from './types.ts';
|
||||||
import * as Util from './fns.ts';
|
import * as Util from './fns.ts';
|
||||||
@ -13,6 +13,7 @@ const {
|
|||||||
assertExists,
|
assertExists,
|
||||||
assertInstanceOf,
|
assertInstanceOf,
|
||||||
assertNotEquals,
|
assertNotEquals,
|
||||||
|
assertNull,
|
||||||
assertFalse,
|
assertFalse,
|
||||||
assertTrue,
|
assertTrue,
|
||||||
testSuite,
|
testSuite,
|
||||||
@ -154,6 +155,71 @@ testSuite({
|
|||||||
// From 'chars'
|
// From 'chars'
|
||||||
assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹');
|
assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹');
|
||||||
},
|
},
|
||||||
|
'.append': () => {
|
||||||
|
const row = Row.from('foo');
|
||||||
|
row.append('bar');
|
||||||
|
assertEquals(row.toString(), 'foobar');
|
||||||
|
},
|
||||||
|
'.delete': () => {
|
||||||
|
const row = Row.from('foof');
|
||||||
|
row.delete(3);
|
||||||
|
assertEquals(row.toString(), 'foo');
|
||||||
|
|
||||||
|
row.delete(4);
|
||||||
|
assertEquals(row.toString(), 'foo');
|
||||||
|
},
|
||||||
|
'.split': () => {
|
||||||
|
// When you split a row, it's from the cursor position
|
||||||
|
// (Kind of like if the string were one-indexed)
|
||||||
|
const row = Row.from('foobar');
|
||||||
|
const row2 = Row.from('bar');
|
||||||
|
assertEquals(row.split(3).toString(), row2.toString());
|
||||||
|
},
|
||||||
|
'.find': () => {
|
||||||
|
const normalRow = Row.from('For whom the bell tolls');
|
||||||
|
assertEquals(normalRow.find('who'), 4);
|
||||||
|
assertNull(normalRow.find('foo'));
|
||||||
|
|
||||||
|
const emojiRow = Row.from('😺😸😹');
|
||||||
|
assertEquals(emojiRow.find('😹'), 2);
|
||||||
|
assertNull(emojiRow.find('🤰🏼'));
|
||||||
|
},
|
||||||
|
'.byteIndexToCharIndex': () => {
|
||||||
|
// Each 'character' is two bytes
|
||||||
|
const row = Row.from('😺😸😹👨👩👧👦');
|
||||||
|
assertEquals(row.byteIndexToCharIndex(4), 2);
|
||||||
|
assertEquals(row.byteIndexToCharIndex(2), 1);
|
||||||
|
assertEquals(row.byteIndexToCharIndex(0), 0);
|
||||||
|
|
||||||
|
// Return count on nonsense index
|
||||||
|
assertEquals(Util.strlen(row.toString()), 10);
|
||||||
|
assertEquals(row.byteIndexToCharIndex(72), 10);
|
||||||
|
|
||||||
|
const row2 = Row.from('foobar');
|
||||||
|
assertEquals(row2.byteIndexToCharIndex(2), 2);
|
||||||
|
},
|
||||||
|
'.charIndexToByteIndex': () => {
|
||||||
|
// Each 'character' is two bytes
|
||||||
|
const row = Row.from('😺😸😹👨👩👧👦');
|
||||||
|
assertEquals(row.charIndexToByteIndex(2), 4);
|
||||||
|
assertEquals(row.charIndexToByteIndex(1), 2);
|
||||||
|
assertEquals(row.charIndexToByteIndex(0), 0);
|
||||||
|
},
|
||||||
|
'.cxToRx, .rxToCx': () => {
|
||||||
|
const row = Row.from('foo\tbar\tbaz');
|
||||||
|
row.updateRender();
|
||||||
|
assertNotEquals(row.chars, row.render);
|
||||||
|
assertNotEquals(row.size, row.rsize);
|
||||||
|
assertEquals(row.size, 11);
|
||||||
|
assertEquals(row.rsize, row.size + (SCROLL_TAB_SIZE * 2) - 2);
|
||||||
|
|
||||||
|
const cx = 11;
|
||||||
|
const aRx = row.cxToRx(cx);
|
||||||
|
const rx = 11;
|
||||||
|
const aCx = row.rxToCx(aRx);
|
||||||
|
assertEquals(aCx, cx);
|
||||||
|
assertEquals(aRx, rx);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'fns': {
|
'fns': {
|
||||||
'arrayInsert() strings': () => {
|
'arrayInsert() strings': () => {
|
||||||
|
@ -5,7 +5,11 @@ import { Position } from './types.ts';
|
|||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
#rows: Row[];
|
#rows: Row[];
|
||||||
dirty: boolean;
|
|
||||||
|
/**
|
||||||
|
* Has the document been modified?
|
||||||
|
*/
|
||||||
|
public dirty: boolean;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
@ -49,6 +53,21 @@ export class Document {
|
|||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public find(
|
||||||
|
q: string,
|
||||||
|
offset: Position = Position.default(),
|
||||||
|
): Position | null {
|
||||||
|
let i = offset.y;
|
||||||
|
for (; i < this.numRows; i++) {
|
||||||
|
const possible = this.#rows[i].find(q, offset.x);
|
||||||
|
if (possible !== null) {
|
||||||
|
return Position.at(possible, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public insert(at: Position, c: string): void {
|
public insert(at: Position, c: string): void {
|
||||||
if (at.y === this.numRows) {
|
if (at.y === this.numRows) {
|
||||||
this.insertRow(this.numRows, c);
|
this.insertRow(this.numRows, c);
|
||||||
|
@ -123,6 +123,10 @@ class Editor {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// Ctrl-key chords
|
// Ctrl-key chords
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
case ctrlKey('f'):
|
||||||
|
await this.find();
|
||||||
|
break;
|
||||||
|
|
||||||
case ctrlKey('s'):
|
case ctrlKey('s'):
|
||||||
await this.save();
|
await this.save();
|
||||||
break;
|
break;
|
||||||
@ -270,6 +274,21 @@ class Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find text within the document
|
||||||
|
*/
|
||||||
|
public async find(): Promise<void> {
|
||||||
|
const res = await this.prompt('Search: %s (ESC to cancel)');
|
||||||
|
if (res !== null && res.length > 0) {
|
||||||
|
const pos = this.#document.find(res);
|
||||||
|
if (pos !== null) {
|
||||||
|
this.#cursor = pos;
|
||||||
|
} else {
|
||||||
|
this.setStatusMessage('Not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter out any additional unwanted keyboard input
|
* Filter out any additional unwanted keyboard input
|
||||||
* @param input
|
* @param input
|
||||||
|
@ -34,7 +34,9 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.setStatusMessage('HELP: Ctrl-S = save | Ctrl-Q = quit');
|
editor.setStatusMessage(
|
||||||
|
'HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find',
|
||||||
|
);
|
||||||
|
|
||||||
// Clear the screen
|
// Clear the screen
|
||||||
await editor.refreshScreen();
|
await editor.refreshScreen();
|
||||||
|
@ -75,6 +75,25 @@ export class Row {
|
|||||||
this.chars.splice(at, 1);
|
this.chars.splice(at, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public find(s: string, offset: number = 0): number | null {
|
||||||
|
const thisStr = this.toString();
|
||||||
|
if (!this.toString().includes(s)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteCount = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
||||||
|
|
||||||
|
// In many cases, the string length will
|
||||||
|
// equal the number of characters. So
|
||||||
|
// searching is fairly easy
|
||||||
|
if (thisStr.length === this.chars.length) {
|
||||||
|
return byteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emoji/Extended Unicode-friendly search
|
||||||
|
return this.byteIndexToCharIndex(byteCount);
|
||||||
|
}
|
||||||
|
|
||||||
public cxToRx(cx: number): number {
|
public cxToRx(cx: number): number {
|
||||||
let rx = 0;
|
let rx = 0;
|
||||||
let j = 0;
|
let j = 0;
|
||||||
@ -88,12 +107,57 @@ export class Row {
|
|||||||
return rx;
|
return rx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public rxToCx(rx: number): number {
|
||||||
|
let curRx = 0;
|
||||||
|
let cx = 0;
|
||||||
|
for (; cx < this.size; cx++) {
|
||||||
|
if (this.chars[cx] === '\t') {
|
||||||
|
curRx += (SCROLL_TAB_SIZE - 1) - (curRx % SCROLL_TAB_SIZE);
|
||||||
|
}
|
||||||
|
curRx++;
|
||||||
|
|
||||||
|
if (curRx > rx) {
|
||||||
|
return cx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byteIndexToCharIndex(byteIndex: number): number {
|
||||||
|
if (this.toString().length === this.chars.length) {
|
||||||
|
return byteIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
let byteCount = 0;
|
||||||
|
for (; n < this.chars.length; n++) {
|
||||||
|
byteCount += this.chars[n].length;
|
||||||
|
if (byteCount > byteIndex) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chars.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public charIndexToByteIndex(charIndex: number): number {
|
||||||
|
if (charIndex === 0 || this.toString().length === this.chars.length) {
|
||||||
|
return charIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chars.slice(0, charIndex).reduce(
|
||||||
|
(prev, current) => prev += current.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.chars.join('');
|
return this.chars.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateRender(): void {
|
public updateRender(): void {
|
||||||
const newString = this.chars.join('').replace(
|
const newString = this.chars.join('').replaceAll(
|
||||||
'\t',
|
'\t',
|
||||||
' '.repeat(SCROLL_TAB_SIZE),
|
' '.repeat(SCROLL_TAB_SIZE),
|
||||||
);
|
);
|
||||||
|
@ -59,9 +59,6 @@ class Termios {
|
|||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (!this.#cleaned) {
|
if (!this.#cleaned) {
|
||||||
this.#ptr = null;
|
|
||||||
this.#cookedTermios = new Uint8Array(0);
|
|
||||||
this.#termios = new Uint8Array(0);
|
|
||||||
this.#ffi.close();
|
this.#ffi.close();
|
||||||
|
|
||||||
this.#cleaned = true;
|
this.#cleaned = true;
|
||||||
|
@ -166,6 +166,7 @@ export interface ITestBase {
|
|||||||
assertFalse(actual: boolean): void;
|
assertFalse(actual: boolean): void;
|
||||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
assertInstanceOf(actual: unknown, expectedType: any): void;
|
||||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
assertNotEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertNull(actual: unknown): void;
|
||||||
assertStrictEquals(actual: unknown, expected: unknown): void;
|
assertStrictEquals(actual: unknown, expected: unknown): void;
|
||||||
assertTrue(actual: boolean): void;
|
assertTrue(actual: boolean): void;
|
||||||
testSuite(testObj: any): void;
|
testSuite(testObj: any): void;
|
||||||
|
@ -5,6 +5,8 @@ import { IRuntime, RunTimeType } from '../common/runtime.ts';
|
|||||||
import DenoTerminalIO from './terminal_io.ts';
|
import DenoTerminalIO from './terminal_io.ts';
|
||||||
import DenoFileIO from './file_io.ts';
|
import DenoFileIO from './file_io.ts';
|
||||||
|
|
||||||
|
import * as node_process from 'node:process';
|
||||||
|
|
||||||
const DenoRuntime: IRuntime = {
|
const DenoRuntime: IRuntime = {
|
||||||
name: RunTimeType.Deno,
|
name: RunTimeType.Deno,
|
||||||
file: DenoFileIO,
|
file: DenoFileIO,
|
||||||
@ -13,8 +15,9 @@ const DenoRuntime: IRuntime = {
|
|||||||
globalThis.addEventListener(eventName, handler),
|
globalThis.addEventListener(eventName, handler),
|
||||||
onExit: (cb: () => void): void => {
|
onExit: (cb: () => void): void => {
|
||||||
globalThis.addEventListener('onbeforeunload', cb);
|
globalThis.addEventListener('onbeforeunload', cb);
|
||||||
|
globalThis.onbeforeunload = cb;
|
||||||
},
|
},
|
||||||
exit: (code?: number) => Deno.exit(code),
|
exit: (code?: number) => node_process.exit(code),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DenoRuntime;
|
export default DenoRuntime;
|
||||||
|
@ -34,6 +34,11 @@ const DenoTestBase: ITestBase = {
|
|||||||
throw new AssertionError(`actual: "${actual}" expected to be false"`);
|
throw new AssertionError(`actual: "${actual}" expected to be false"`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
assertNull(actual: boolean): void {
|
||||||
|
if (actual !== null) {
|
||||||
|
throw new AssertionError(`actual: "${actual}" expected to be null"`);
|
||||||
|
}
|
||||||
|
},
|
||||||
testSuite,
|
testSuite,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user