Compare commits
No commits in common. "master" and "linear-input" have entirely different histories.
master
...
linear-inp
7
.gitignore
vendored
7
.gitignore
vendored
@ -335,13 +335,8 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,vim,node,deno,macos,windows,linux
|
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,vim,node,deno,macos,windows,linux
|
||||||
|
|
||||||
# Other editors
|
|
||||||
.nova/
|
|
||||||
.zed/
|
|
||||||
|
|
||||||
## Misc generated files
|
## Misc generated files
|
||||||
scroll*.log
|
scroll.err
|
||||||
docs
|
|
||||||
deno.lock
|
deno.lock
|
||||||
cov_profile/
|
cov_profile/
|
||||||
coverage/
|
coverage/
|
||||||
|
20
README.md
20
README.md
@ -1,30 +1,16 @@
|
|||||||
# Scroll
|
# Scroll
|
||||||
|
|
||||||
Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This
|
Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This
|
||||||
runs on
|
runs on [Bun](https://bun.sh/) (v1.0 or later) and [Deno](https://deno.com/)
|
||||||
|
(v1.37 or later).
|
||||||
- [Bun](https://bun.sh/) (v1.0 or later)
|
|
||||||
- [Deno](https://deno.com/) (v1.37 or later)
|
|
||||||
- [TSX](https://tsx.is/) - this is a Typescript wrapper using NodeJS (v20 or
|
|
||||||
later)
|
|
||||||
|
|
||||||
To simplify running, I'm using [Just](https://github.com/casey/just).
|
To simplify running, I'm using [Just](https://github.com/casey/just).
|
||||||
|
|
||||||
- Bun: `just bun-run [filename]`
|
- Bun: `just bun-run [filename]`
|
||||||
- Deno: `just deno-run [filename]`
|
- Deno: `just deno-run [filename]`
|
||||||
- TSX: `just tsx-run [filename]`
|
|
||||||
|
|
||||||
Alternatively, there are shell scripts for each runtime in the `bin` folder. So
|
|
||||||
you can run the editor by calling `./bin/deno.sh [filename]` without installing
|
|
||||||
Just.
|
|
||||||
|
|
||||||
Deno is generally used for dev tools, but each runtime should be functionally
|
|
||||||
equivalent running the text editor.
|
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
- Implementation is based on [Kilo](https://viewsourcecode.org/snaptoken/kilo/)
|
|
||||||
and [Hecto](https://archive.flenker.blog/hecto/)
|
|
||||||
- Runtime differences are adapted into a common interface
|
- Runtime differences are adapted into a common interface
|
||||||
- Runtime implementations are in the `src/deno`, `src/bun`, `src/tsx` folders
|
- Runtime implementations are in the `src/deno` and `src/bun` folders
|
||||||
- The main implementation is in `src/common`
|
- The main implementation is in `src/common`
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
PARENT_DIR="$(dirname "$(realpath "$0")")"
|
|
||||||
SCROLL="$(realpath "${PARENT_DIR}/../src/scroll.ts")"
|
|
||||||
bun run "${SCROLL}" "$@"
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
PARENT_DIR="$(dirname "$(realpath "$0")")"
|
|
||||||
SCROLL="$(realpath "${PARENT_DIR}/../src/scroll.ts")"
|
|
||||||
deno run --allow-all --deny-hrtime "${SCROLL}" "$@"
|
|
@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
PARENT_DIR="$(dirname "$(realpath "$0")")"
|
|
||||||
TSX="$(realpath "${PARENT_DIR}/../node_modules/.bin/tsx")"
|
|
||||||
SCROLL="$(realpath "${PARENT_DIR}/../src/scroll.ts")"
|
|
||||||
"${TSX}" "${SCROLL}" "$@";
|
|
@ -4,3 +4,6 @@ deno test --allow-all --coverage=coverage
|
|||||||
deno coverage coverage --lcov > coverage/coverage.lcov
|
deno coverage coverage --lcov > coverage/coverage.lcov
|
||||||
genhtml -o coverage coverage/coverage.lcov
|
genhtml -o coverage coverage/coverage.lcov
|
||||||
rm coverage/*.json
|
rm coverage/*.json
|
||||||
|
open coverage/index.html
|
||||||
|
|
||||||
|
|
||||||
|
155
demo/colors.ts
155
demo/colors.ts
@ -1,155 +0,0 @@
|
|||||||
/**
|
|
||||||
* This is a test file and a terminal color table program
|
|
||||||
*/
|
|
||||||
import Ansi, { AnsiColor, Ground } from '../src/common/ansi.ts';
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Display table of the 256 type color console escape codes
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const addColor = (fore: string, back: string): string => {
|
|
||||||
let output = '';
|
|
||||||
output += fore;
|
|
||||||
output += back;
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
const padNum = (num: number): string =>
|
|
||||||
String(num).padStart(3, ' ').padEnd(5, ' ');
|
|
||||||
|
|
||||||
const colorBlock = (
|
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
block: (i: number) => [string, string],
|
|
||||||
): string => {
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
const [fg, bg] = block(i);
|
|
||||||
|
|
||||||
output += addColor(fg, bg);
|
|
||||||
output += padNum(i);
|
|
||||||
output += Ansi.ResetFormatting;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
function print16colorTable(): void {
|
|
||||||
const drawRow = (start: number): string => {
|
|
||||||
let end = start + 8;
|
|
||||||
|
|
||||||
let blocks = [
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.textFormat(i), Ansi.color.background.Black],
|
|
||||||
),
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.textFormat(i), Ansi.color.background.BrightWhite],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
start += 10;
|
|
||||||
end += 10;
|
|
||||||
|
|
||||||
blocks.push(
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.color.Black, Ansi.textFormat(i)],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
blocks.push(
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.color.BrightWhite, Ansi.textFormat(i)],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return blocks.join(' '.repeat(5));
|
|
||||||
};
|
|
||||||
|
|
||||||
let colorTable = [
|
|
||||||
drawRow(30),
|
|
||||||
drawRow(90),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
colorTable += '\n';
|
|
||||||
|
|
||||||
console.log(colorTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
function print256colorTable(): void {
|
|
||||||
let colorTable = '';
|
|
||||||
// deno-fmt-ignore
|
|
||||||
const breaks = [
|
|
||||||
7, 15,
|
|
||||||
21, 27, 33, 39, 45, 51,
|
|
||||||
57, 63, 69, 75, 81, 87,
|
|
||||||
93, 99, 105, 111, 117, 123,
|
|
||||||
129, 135, 141, 147, 153, 159,
|
|
||||||
165, 171, 177, 183, 189, 195,
|
|
||||||
201, 207, 213, 219, 225, 231,
|
|
||||||
237, 243, 249, 255,
|
|
||||||
];
|
|
||||||
const doubleBreaks = [15, 51, 87, 123, 159, 195, 231, 255];
|
|
||||||
|
|
||||||
breaks.forEach((line, n) => {
|
|
||||||
const start = (n > 0) ? breaks[n - 1] + 1 : 0;
|
|
||||||
const end = line + 1;
|
|
||||||
|
|
||||||
const blocks = [
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(
|
|
||||||
i: number,
|
|
||||||
) => [Ansi.color256(i, Ground.Fore), Ansi.color.background.Black],
|
|
||||||
),
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(
|
|
||||||
i: number,
|
|
||||||
) => [Ansi.color256(i, Ground.Fore), Ansi.color.background.BrightWhite],
|
|
||||||
),
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.color.Black, Ansi.color256(i, Ground.Back)],
|
|
||||||
),
|
|
||||||
colorBlock(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
(i: number) => [Ansi.color.BrightWhite, Ansi.color256(i, Ground.Back)],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
colorTable += blocks.join(' '.repeat(5));
|
|
||||||
colorTable += '\n';
|
|
||||||
|
|
||||||
if (doubleBreaks.includes(line)) {
|
|
||||||
colorTable += '\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(colorTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
print16colorTable();
|
|
||||||
print256colorTable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test code for highlighting
|
|
||||||
*/
|
|
||||||
const decimal: number[] = [0, 117];
|
|
||||||
const bigDecimal = 123456789123456789n;
|
|
||||||
const octal: number[] = [0o15, 0o1];
|
|
||||||
const bigOctal = 0o777777777777n;
|
|
||||||
const hexadecimal: number[] = [0x1123, 0x00111];
|
|
||||||
const bigHex = 0x123456789ABCDEFn;
|
|
||||||
const binary: number[] = [0b11, 0b0011];
|
|
||||||
const bigBinary = 0b11101001010101010101n;
|
|
1686
demo/editor.rs
1686
demo/editor.rs
File diff suppressed because it is too large
Load Diff
1468
demo/kilo.c
1468
demo/kilo.c
File diff suppressed because it is too large
Load Diff
271
demo/test.css
271
demo/test.css
@ -1,271 +0,0 @@
|
|||||||
/* -----------------------------------------------------------------------------
|
|
||||||
CSS loading icon
|
|
||||||
------------------------------------------------------------------------------*/
|
|
||||||
.cssload-loader {
|
|
||||||
position: relative;
|
|
||||||
left: calc(50% - 31px);
|
|
||||||
width: 62px;
|
|
||||||
height: 62px;
|
|
||||||
border-radius: 50%;
|
|
||||||
perspective: 780px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cssload-inner {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cssload-inner.cssload-one {
|
|
||||||
left: 0%;
|
|
||||||
top: 0%;
|
|
||||||
animation: cssload-rotate-one 1.15s linear infinite;
|
|
||||||
border-bottom: 3px solid rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cssload-inner.cssload-two {
|
|
||||||
right: 0%;
|
|
||||||
top: 0%;
|
|
||||||
animation: cssload-rotate-two 1.15s linear infinite;
|
|
||||||
border-right: 3px solid rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cssload-inner.cssload-three {
|
|
||||||
right: 0%;
|
|
||||||
bottom: 0%;
|
|
||||||
animation: cssload-rotate-three 1.15s linear infinite;
|
|
||||||
border-top: 3px solid rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cssload-rotate-one {
|
|
||||||
0% {
|
|
||||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cssload-rotate-two {
|
|
||||||
0% {
|
|
||||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cssload-rotate-three {
|
|
||||||
0% {
|
|
||||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
Loading overlay
|
|
||||||
-----------------------------------------------------------------------------*/
|
|
||||||
#loading-shadow {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
z-index: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-shadow .loading-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 501;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-shadow .loading-content {
|
|
||||||
position: relative;
|
|
||||||
color: #fff
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-content .cssload-inner.cssload-one,
|
|
||||||
.loading-content .cssload-inner.cssload-two,
|
|
||||||
.loading-content .cssload-inner.cssload-three {
|
|
||||||
border-color: #fff
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
CSS Tabs
|
|
||||||
-----------------------------------------------------------------------------*/
|
|
||||||
.tabs {
|
|
||||||
display: inline-block;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
background: #efefef;
|
|
||||||
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
|
|
||||||
margin-top: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > label {
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px 30px;
|
|
||||||
background: #e5e5e5;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #7f7f7f;
|
|
||||||
transition: background 0.1s, color 0.1s;
|
|
||||||
/* margin-left: 4em; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > label:hover {
|
|
||||||
background: #d8d8d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > label:active {
|
|
||||||
background: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > [type=radio]:focus + label {
|
|
||||||
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > [type=radio] {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > [type=radio]:checked + label {
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs > [type=radio]:checked + label + .content {
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
border-top: 0;
|
|
||||||
display: block;
|
|
||||||
padding: 15px;
|
|
||||||
background: #fff;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: auto;
|
|
||||||
/* text-align: center; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs .content, .single-tab {
|
|
||||||
display: none;
|
|
||||||
max-height: 950px;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
border-top: 0;
|
|
||||||
padding: 15px;
|
|
||||||
background: #fff;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-tab {
|
|
||||||
display: block;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
|
|
||||||
margin-top: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs .content.full-height, .single-tab.full-height {
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.tabs > label {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs .content {
|
|
||||||
order: 99;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Vertical Tabs
|
|
||||||
----------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
.vertical-tabs {
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs input[type="radio"] {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab {
|
|
||||||
align-items: center;
|
|
||||||
display: inline-block;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab label {
|
|
||||||
align-items: center;
|
|
||||||
background: #e5e5e5;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
color: #7f7f7f;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0 20px;
|
|
||||||
width: 28%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab label:hover {
|
|
||||||
background: #d8d8d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab label:active {
|
|
||||||
background: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab .content {
|
|
||||||
display: none;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
max-height: 950px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs .tab .content.full-height {
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs [type=radio]:checked + label {
|
|
||||||
border: 0;
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
width: 38%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs [type=radio]:focus + label {
|
|
||||||
box-shadow: inset 0px 0px 0px 3px #2aa1c0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs [type=radio]:checked ~ .content {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,5 @@
|
|||||||
"semiColons": true,
|
"semiColons": true,
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"nodeModulesDir": true,
|
"nodeModulesDir": true
|
||||||
"exclude": ["src/bun/"]
|
|
||||||
}
|
}
|
||||||
|
58
justfile
58
justfile
@ -3,41 +3,31 @@ default:
|
|||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Test coverage
|
# Test coverage
|
||||||
coverage: deno-coverage bun-coverage
|
coverage: bun-test deno-coverage
|
||||||
|
|
||||||
# Generate test coverage and open report in default browser
|
|
||||||
open-coverage: coverage
|
|
||||||
open coverage/index.html
|
|
||||||
|
|
||||||
# Typescript checking
|
# Typescript checking
|
||||||
check: deno-check bun-check
|
check: deno-check bun-check
|
||||||
|
|
||||||
# Generate source docs
|
|
||||||
docs:
|
docs:
|
||||||
deno doc --html --name="Scroll" ./src/common/*.ts ./src/common/**/*.ts
|
deno doc --html --unstable-ffi --name="Scroll" ./src/scroll.ts ./src/common/mod.ts ./src/deno/mod.ts ./src/bun/mod.ts
|
||||||
|
|
||||||
# Generate source docs and open in default browser
|
|
||||||
open-docs: docs
|
|
||||||
open docs/all_symbols.html
|
|
||||||
|
|
||||||
# Reformat the code
|
# Reformat the code
|
||||||
fmt:
|
fmt:
|
||||||
deno fmt
|
deno fmt
|
||||||
|
|
||||||
# Run tests with all the runtimes
|
# Run tests with all the runtimes
|
||||||
test: deno-test tsx-test bun-test
|
test: deno-test bun-test
|
||||||
|
|
||||||
# Run all code-quality related tasks
|
# Run all code-quality related tasks
|
||||||
quality: check test
|
quality: check test
|
||||||
|
|
||||||
# Clean up any generated files
|
# Clean up any generated files
|
||||||
clean:
|
clean:
|
||||||
rm -f test.file
|
|
||||||
rm -rf .deno-cover
|
rm -rf .deno-cover
|
||||||
rm -rf coverage
|
rm -rf coverage
|
||||||
rm -rf docs
|
rm -rf docs
|
||||||
rm -f scroll*.log
|
rm -f scroll.log
|
||||||
rm -f test.file
|
rm -f scroll.err
|
||||||
rm -f tsconfig.tsbuildinfo
|
rm -f tsconfig.tsbuildinfo
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
@ -46,19 +36,15 @@ clean:
|
|||||||
|
|
||||||
# Check code with actual Typescript compiler
|
# Check code with actual Typescript compiler
|
||||||
bun-check:
|
bun-check:
|
||||||
bun run bun-check
|
bunx tsc
|
||||||
|
|
||||||
# Test with bun
|
# Test with bun
|
||||||
bun-test:
|
bun-test:
|
||||||
bun run bun-test
|
bun test --coverage
|
||||||
|
|
||||||
# CLI test coverage report
|
|
||||||
bun-coverage:
|
|
||||||
bun run bun-coverage
|
|
||||||
|
|
||||||
# Run with bun
|
# Run with bun
|
||||||
bun-run file="":
|
bun-run file="":
|
||||||
bun run bun-run {{file}}
|
bun run ./src/scroll.ts {{file}}
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
# Deno-specific commands
|
# Deno-specific commands
|
||||||
@ -66,35 +52,17 @@ bun-run file="":
|
|||||||
|
|
||||||
# Lint code and check types
|
# Lint code and check types
|
||||||
deno-check:
|
deno-check:
|
||||||
deno task deno-lint
|
deno lint
|
||||||
deno task deno-check
|
deno check --unstable-ffi --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
||||||
|
|
||||||
# Test with deno
|
# Test with deno
|
||||||
deno-test:
|
deno-test:
|
||||||
deno task deno-test
|
deno test --allow-all --unstable-ffi
|
||||||
|
|
||||||
# Create test coverage report with deno
|
# Create test coverage report with deno
|
||||||
deno-coverage:
|
deno-coverage:
|
||||||
deno task deno-coverage
|
./coverage.sh
|
||||||
|
|
||||||
# Run with deno
|
# Run with deno
|
||||||
deno-run file="":
|
deno-run file="":
|
||||||
deno task deno-run {{file}}
|
deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts {{file}}
|
||||||
|
|
||||||
##########################################################################################
|
|
||||||
# tsx(Node JS)-specific commands
|
|
||||||
##########################################################################################
|
|
||||||
|
|
||||||
# Check code with actual Typescript compiler
|
|
||||||
tsx-check:
|
|
||||||
npm run tsx-check
|
|
||||||
|
|
||||||
# Test with tsx (NodeJS)
|
|
||||||
tsx-test:
|
|
||||||
npm run tsx-test
|
|
||||||
|
|
||||||
# Run with tsx (NodeJS)
|
|
||||||
tsx-run file="":
|
|
||||||
npm run tsx-run {{file}}
|
|
||||||
|
|
||||||
|
|
||||||
|
22
package.json
22
package.json
@ -1,24 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "*",
|
"bun-types": "^1.0.11"
|
||||||
"bun-types": "*",
|
}
|
||||||
"typescript": "^5.5",
|
|
||||||
"tsx": "*"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"bun-check": "bunx tsc",
|
|
||||||
"bun-coverage": "bun test --coverage",
|
|
||||||
"bun-run": "bun run ./src/scroll.ts",
|
|
||||||
"bun-test": "bun test",
|
|
||||||
"deno-lint": "deno lint",
|
|
||||||
"deno-check": "deno check --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts ./src/tsx/*.ts",
|
|
||||||
"deno-coverage": "./coverage.sh",
|
|
||||||
"deno-run": "deno run --allow-all --deny-hrtime ./src/scroll.ts",
|
|
||||||
"deno-test": "deno test --allow-all",
|
|
||||||
"tsx-check": "npx tsc",
|
|
||||||
"tsx-run": "tsx ./src/scroll.ts",
|
|
||||||
"tsx-test": "tsx --test './src/common/all_test.ts'"
|
|
||||||
},
|
|
||||||
"type": "module"
|
|
||||||
}
|
}
|
||||||
|
20
src/bun/file_io.ts
Normal file
20
src/bun/file_io.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
|
||||||
|
|
||||||
|
import { IFileIO } from '../common/runtime.ts';
|
||||||
|
import { appendFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
const BunFileIO: IFileIO = {
|
||||||
|
openFile: async (path: string): Promise<string> => {
|
||||||
|
const file = await globalThis.Bun.file(path);
|
||||||
|
return await file.text();
|
||||||
|
},
|
||||||
|
appendFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
return await appendFile(path, contents);
|
||||||
|
},
|
||||||
|
saveFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
await globalThis.Bun.write(path, contents);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BunFileIO;
|
@ -1,14 +1,25 @@
|
|||||||
|
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
|
||||||
/**
|
/**
|
||||||
* The main entrypoint when using Bun as the runtime
|
* The main entrypoint when using Bun as the runtime
|
||||||
*/
|
*/
|
||||||
import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime.ts';
|
|
||||||
|
|
||||||
/**
|
import { IRuntime, RunTimeType } from '../common/runtime.ts';
|
||||||
* The Bun Runtime implementation
|
import BunTerminalIO from './terminal_io.ts';
|
||||||
*/
|
import BunFileIO from './file_io.ts';
|
||||||
|
|
||||||
|
import * as process from 'node:process';
|
||||||
|
|
||||||
const BunRuntime: IRuntime = {
|
const BunRuntime: IRuntime = {
|
||||||
...CommonRuntime,
|
|
||||||
name: RunTimeType.Bun,
|
name: RunTimeType.Bun,
|
||||||
|
file: BunFileIO,
|
||||||
|
term: BunTerminalIO,
|
||||||
|
onEvent: (eventName: string, handler) => process.on(eventName, handler),
|
||||||
|
onExit: (cb: () => void): void => {
|
||||||
|
process.on('beforeExit', cb);
|
||||||
|
process.on('exit', cb);
|
||||||
|
process.on('SIGINT', cb);
|
||||||
|
},
|
||||||
|
exit: (code?: number) => process.exit(code),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BunRuntime;
|
export default BunRuntime;
|
||||||
|
90
src/bun/terminal_io.ts
Normal file
90
src/bun/terminal_io.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
|
||||||
|
/**
|
||||||
|
* Wrap the runtime-specific hook into stdin
|
||||||
|
*/
|
||||||
|
import process from 'node:process';
|
||||||
|
import Ansi from '../common/ansi.ts';
|
||||||
|
import { defaultTerminalSize } from '../common/config.ts';
|
||||||
|
import { readKey } from '../common/fns.ts';
|
||||||
|
import { ITerminal, ITerminalSize } from '../common/types.ts';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
async function _getTerminalSizeFromAnsi(): Promise<ITerminalSize> {
|
||||||
|
// Tell the cursor to move to Row 999 and Column 999
|
||||||
|
// Since this command specifically doesn't go off the screen
|
||||||
|
// When we ask where the cursor is, we should get the size of the screen
|
||||||
|
await BunTerminalIO.writeStdout(
|
||||||
|
Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ask where the cursor is
|
||||||
|
await BunTerminalIO.writeStdout(Ansi.GetCursorLocation);
|
||||||
|
|
||||||
|
// Get the first chunk from stdin
|
||||||
|
// The response is \x1b[(rows);(cols)R..
|
||||||
|
const chunk = await BunTerminalIO.readStdinRaw();
|
||||||
|
if (chunk === null) {
|
||||||
|
return defaultTerminalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawCode = (new TextDecoder()).decode(chunk);
|
||||||
|
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
|
||||||
|
const [srows, scols] = res.split(';');
|
||||||
|
const rows = parseInt(srows, 10) ?? 24;
|
||||||
|
const cols = parseInt(scols, 10) ?? 80;
|
||||||
|
|
||||||
|
// Clear the screen
|
||||||
|
await BunTerminalIO.writeStdout(Ansi.ClearScreen + Ansi.ResetCursor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const BunTerminalIO: ITerminal = {
|
||||||
|
// Deno only returns arguments passed to the script, so
|
||||||
|
// remove the bun runtime executable, and entry script arguments
|
||||||
|
// to have consistent argument lists
|
||||||
|
argv: (Bun.argv.length > 2) ? Bun.argv.slice(2) : [],
|
||||||
|
inputLoop: async function* inputLoop() {
|
||||||
|
// for await (const chunk of Bun.stdin.stream()) {
|
||||||
|
// yield chunk;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return null;
|
||||||
|
for await (const chunk of process.stdin) {
|
||||||
|
yield encoder.encode(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the size of the terminal window via ANSI codes
|
||||||
|
* @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way
|
||||||
|
*/
|
||||||
|
getTerminalSize: function getTerminalSize(): Promise<ITerminalSize> {
|
||||||
|
const [cols, rows] = process.stdout.getWindowSize();
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
readStdin: async function (): Promise<string | null> {
|
||||||
|
const raw = await BunTerminalIO.readStdinRaw();
|
||||||
|
return readKey(raw ?? new Uint8Array(0));
|
||||||
|
},
|
||||||
|
readStdinRaw: async function (): Promise<Uint8Array | null> {
|
||||||
|
const chunk = await BunTerminalIO.inputLoop().next();
|
||||||
|
return chunk.value ?? null;
|
||||||
|
},
|
||||||
|
writeStdout: async function write(s: string): Promise<void> {
|
||||||
|
const buffer = encoder.encode(s);
|
||||||
|
|
||||||
|
await Bun.write(Bun.stdout, buffer);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BunTerminalIO;
|
@ -1,10 +1,11 @@
|
|||||||
|
if (!('Bun' in globalThis)) throw new Error('This module requires Bun');
|
||||||
/**
|
/**
|
||||||
* Adapt the bun test interface to the shared testing interface
|
* Adapt the bun test interface to the shared testing interface
|
||||||
*/
|
*/
|
||||||
import { describe, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
import AbstractTestBase from '../common/runtime/test_base.ts';
|
import { ITestBase } from '../common/types.ts';
|
||||||
class BunTestBase extends AbstractTestBase {
|
|
||||||
public static testSuite(testObj: any): void {
|
export function testSuite(testObj: any) {
|
||||||
Object.keys(testObj).forEach((group) => {
|
Object.keys(testObj).forEach((group) => {
|
||||||
describe(group, () => {
|
describe(group, () => {
|
||||||
const groupObj = testObj[group];
|
const groupObj = testObj[group];
|
||||||
@ -13,7 +14,22 @@ class BunTestBase extends AbstractTestBase {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BunTestBase: ITestBase = {
|
||||||
|
assertEquals: (actual: unknown, expected: unknown) =>
|
||||||
|
expect(actual).toEqual(expected),
|
||||||
|
assertExists: (actual: unknown) => expect(actual).toBeDefined(),
|
||||||
|
assertFalse: (actual: boolean) => expect(actual).toBe(false),
|
||||||
|
assertInstanceOf: (actual: unknown, expectedType: any) =>
|
||||||
|
expect(actual).toBeInstanceOf(expectedType),
|
||||||
|
assertNotEquals: (actual: unknown, expected: unknown) =>
|
||||||
|
expect(actual).not.toBe(expected),
|
||||||
|
assertNull: (actual: unknown) => expect(actual).toBeNull(),
|
||||||
|
assertStrictEquals: (actual: unknown, expected: unknown) =>
|
||||||
|
expect(actual).toBe(expected),
|
||||||
|
assertTrue: (actual: boolean) => expect(actual).toBe(true),
|
||||||
|
testSuite,
|
||||||
|
};
|
||||||
|
|
||||||
export default BunTestBase;
|
export default BunTestBase;
|
||||||
|
@ -2,41 +2,270 @@ import Ansi, * as _Ansi from './ansi.ts';
|
|||||||
import Buffer from './buffer.ts';
|
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 { FileLang } from './filetype.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';
|
||||||
|
|
||||||
import FileType, * as FT from './filetype.ts';
|
|
||||||
import * as Fn from './fns.ts';
|
import * as Fn from './fns.ts';
|
||||||
import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
|
import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
|
||||||
import { getTestRunner } from './runtime.ts';
|
import { getTestRunner } from './runtime.ts';
|
||||||
import { HighlightType, SearchDirection } from './types.ts';
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
assertEquals,
|
assertEquals,
|
||||||
assertEquivalent,
|
|
||||||
assertExists,
|
assertExists,
|
||||||
assertInstanceOf,
|
assertInstanceOf,
|
||||||
assertNotEquals,
|
assertNotEquals,
|
||||||
|
assertNull,
|
||||||
assertFalse,
|
assertFalse,
|
||||||
assertTrue,
|
assertTrue,
|
||||||
assertSome,
|
|
||||||
assertNone,
|
|
||||||
testSuite,
|
testSuite,
|
||||||
} = await getTestRunner();
|
} = await getTestRunner();
|
||||||
|
|
||||||
const THIS_FILE = './src/common/all_test.ts';
|
// ----------------------------------------------------------------------------
|
||||||
const KILO_FILE = './demo/kilo.c';
|
|
||||||
|
const ANSITest = () => {
|
||||||
|
const { AnsiColor, Ground } = _Ansi;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'color()': () => {
|
||||||
|
assertEquals(Ansi.color(AnsiColor.FgBlue), '\x1b[34m');
|
||||||
|
},
|
||||||
|
'color256()': () => {
|
||||||
|
assertEquals(Ansi.color256(128, Ground.Back), '\x1b[48;5;128m');
|
||||||
|
assertEquals(Ansi.color256(128, Ground.Fore), '\x1b[38;5;128m');
|
||||||
|
},
|
||||||
|
'rgb()': () => {
|
||||||
|
assertEquals(Ansi.rgb(32, 64, 128, Ground.Back), '\x1b[48;2;32;64;128m');
|
||||||
|
assertEquals(Ansi.rgb(32, 64, 128, Ground.Fore), '\x1b[38;2;32;64;128m');
|
||||||
|
},
|
||||||
|
'moveCursor()': () => {
|
||||||
|
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
||||||
|
},
|
||||||
|
'moveCursorForward()': () => {
|
||||||
|
assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
|
||||||
|
},
|
||||||
|
'moveCursorDown()': () => {
|
||||||
|
assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helper Function Tests
|
|
||||||
|
const BufferTest = {
|
||||||
|
'new Buffer': () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
assertInstanceOf(b, Buffer);
|
||||||
|
assertEquals(b.strlen(), 0);
|
||||||
|
},
|
||||||
|
'.appendLine': () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
|
||||||
|
// Carriage return and line feed
|
||||||
|
b.appendLine();
|
||||||
|
assertEquals(b.strlen(), 2);
|
||||||
|
|
||||||
|
b.clear();
|
||||||
|
assertEquals(b.strlen(), 0);
|
||||||
|
|
||||||
|
b.appendLine('foo');
|
||||||
|
assertEquals(b.strlen(), 5);
|
||||||
|
},
|
||||||
|
'.append': () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
|
||||||
|
b.append('foobar');
|
||||||
|
assertEquals(b.strlen(), 6);
|
||||||
|
b.clear();
|
||||||
|
|
||||||
|
b.append('foobar', 3);
|
||||||
|
assertEquals(b.strlen(), 3);
|
||||||
|
},
|
||||||
|
'.flush': async () => {
|
||||||
|
const b = new Buffer();
|
||||||
|
b.appendLine('foobarbaz' + Ansi.ClearLine);
|
||||||
|
assertEquals(b.strlen(), 14);
|
||||||
|
|
||||||
|
await b.flush();
|
||||||
|
|
||||||
|
assertEquals(b.strlen(), 0);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const DocumentTest = {
|
||||||
|
'.default': () => {
|
||||||
|
const doc = Document.default();
|
||||||
|
assertEquals(doc.numRows, 0);
|
||||||
|
assertTrue(doc.isEmpty());
|
||||||
|
assertEquals(doc.row(0), null);
|
||||||
|
},
|
||||||
|
'.insertRow': () => {
|
||||||
|
const doc = Document.default();
|
||||||
|
doc.insertRow(undefined, 'foobar');
|
||||||
|
assertEquals(doc.numRows, 1);
|
||||||
|
assertFalse(doc.isEmpty());
|
||||||
|
assertInstanceOf(doc.row(0), Row);
|
||||||
|
},
|
||||||
|
'.insert': () => {
|
||||||
|
const doc = Document.default();
|
||||||
|
assertFalse(doc.dirty);
|
||||||
|
doc.insert(Position.at(0, 0), 'foobar');
|
||||||
|
assertEquals(doc.numRows, 1);
|
||||||
|
assertTrue(doc.dirty);
|
||||||
|
|
||||||
|
doc.insert(Position.at(2, 0), 'baz');
|
||||||
|
assertEquals(doc.numRows, 1);
|
||||||
|
assertTrue(doc.dirty);
|
||||||
|
|
||||||
|
doc.insert(Position.at(9, 0), 'buzz');
|
||||||
|
assertEquals(doc.numRows, 1);
|
||||||
|
assertTrue(doc.dirty);
|
||||||
|
const row0 = doc.row(0);
|
||||||
|
assertEquals(row0?.toString(), 'foobazbarbuzz');
|
||||||
|
assertEquals(row0?.rstring(), 'foobazbarbuzz');
|
||||||
|
assertEquals(row0?.rsize, 13);
|
||||||
|
|
||||||
|
doc.insert(Position.at(0, 1), 'Lorem Ipsum');
|
||||||
|
assertEquals(doc.numRows, 2);
|
||||||
|
assertTrue(doc.dirty);
|
||||||
|
},
|
||||||
|
'.delete': () => {
|
||||||
|
const doc = Document.default();
|
||||||
|
doc.insert(Position.default(), 'foobar');
|
||||||
|
doc.delete(Position.at(3, 0));
|
||||||
|
assertEquals(doc.row(0)?.toString(), 'fooar');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const EditorTest = {
|
||||||
|
'new Editor': () => {
|
||||||
|
const e = new Editor(defaultTerminalSize);
|
||||||
|
assertInstanceOf(e, Editor);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const PositionTest = {
|
||||||
|
'.default': () => {
|
||||||
|
const p = Position.default();
|
||||||
|
assertEquals(p.x, 0);
|
||||||
|
assertEquals(p.y, 0);
|
||||||
|
},
|
||||||
|
'.at': () => {
|
||||||
|
const p = Position.at(5, 7);
|
||||||
|
assertEquals(p.x, 5);
|
||||||
|
assertEquals(p.y, 7);
|
||||||
|
},
|
||||||
|
'.from': () => {
|
||||||
|
const p1 = Position.at(1, 2);
|
||||||
|
const p2 = Position.from(p1);
|
||||||
|
|
||||||
|
p1.x = 2;
|
||||||
|
p1.y = 4;
|
||||||
|
|
||||||
|
assertEquals(p1.x, 2);
|
||||||
|
assertEquals(p1.y, 4);
|
||||||
|
|
||||||
|
assertEquals(p2.x, 1);
|
||||||
|
assertEquals(p2.y, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const RowTest = {
|
||||||
|
'.default': () => {
|
||||||
|
const row = Row.default();
|
||||||
|
assertEquals(row.toString(), '');
|
||||||
|
},
|
||||||
|
'.from': () => {
|
||||||
|
// From string
|
||||||
|
const row = Row.from('xyz');
|
||||||
|
assertEquals(row.toString(), 'xyz');
|
||||||
|
|
||||||
|
// From existing Row
|
||||||
|
assertEquals(Row.from(row).toString(), row.toString());
|
||||||
|
|
||||||
|
// From 'chars'
|
||||||
|
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(Fn.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.update();
|
||||||
|
assertNotEquals(row.chars, row.rchars);
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
const fnTest = () => {
|
const fnTest = () => {
|
||||||
const {
|
const {
|
||||||
|
some,
|
||||||
|
none,
|
||||||
arrayInsert,
|
arrayInsert,
|
||||||
noop,
|
noop,
|
||||||
posSub,
|
posSub,
|
||||||
@ -50,49 +279,47 @@ const fnTest = () => {
|
|||||||
isAsciiDigit,
|
isAsciiDigit,
|
||||||
strlen,
|
strlen,
|
||||||
truncate,
|
truncate,
|
||||||
highlightToColor,
|
|
||||||
} = 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', '😸', '😹'];
|
||||||
assertEquivalent(b, c);
|
assertEquals(b, c);
|
||||||
|
|
||||||
const d = arrayInsert(c, 17, 'y');
|
const d = arrayInsert(c, 17, 'y');
|
||||||
const e = ['😺', 'x', '😸', '😹', 'y'];
|
const e = ['😺', 'x', '😸', '😹', 'y'];
|
||||||
assertEquivalent(d, e);
|
assertEquals(d, e);
|
||||||
|
|
||||||
assertEquivalent(arrayInsert([], 0, 'foo'), ['foo']);
|
assertEquals(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];
|
||||||
assertEquivalent(arrayInsert(a, 2, 4), b);
|
assertEquals(arrayInsert(a, 2, 4), b);
|
||||||
|
|
||||||
const c = [1, 2, 3, 4, 5];
|
const c = [1, 2, 3, 4, 5];
|
||||||
assertEquivalent(arrayInsert(b, 1, 2), c);
|
assertEquals(arrayInsert(b, 1, 2), c);
|
||||||
},
|
},
|
||||||
'noop fn': () => {
|
'noop fn': () => {
|
||||||
assertExists(noop);
|
assertExists(noop);
|
||||||
assertEquals(noop(), undefined);
|
assertEquals(noop(), undefined);
|
||||||
},
|
},
|
||||||
'highlightToColor()': () => {
|
|
||||||
[
|
|
||||||
HighlightType.Number,
|
|
||||||
HighlightType.Match,
|
|
||||||
HighlightType.String,
|
|
||||||
HighlightType.SingleLineComment,
|
|
||||||
HighlightType.MultiLineComment,
|
|
||||||
HighlightType.Keyword1,
|
|
||||||
HighlightType.Keyword2,
|
|
||||||
HighlightType.Operator,
|
|
||||||
HighlightType.None,
|
|
||||||
].forEach((type) => {
|
|
||||||
assertTrue(highlightToColor(type).length > 0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
'posSub()': () => {
|
'posSub()': () => {
|
||||||
assertEquals(posSub(14, 15), 0);
|
assertEquals(posSub(14, 15), 0);
|
||||||
assertEquals(posSub(15, 1), 14);
|
assertEquals(posSub(15, 1), 14);
|
||||||
@ -113,7 +340,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': () => {
|
||||||
assertEquivalent(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
assertEquals(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
||||||
},
|
},
|
||||||
'ctrlKey()': () => {
|
'ctrlKey()': () => {
|
||||||
const ctrl_a = ctrlKey('a');
|
const ctrl_a = ctrlKey('a');
|
||||||
@ -164,6 +391,8 @@ const fnTest = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
const readKeyTest = () => {
|
const readKeyTest = () => {
|
||||||
const { KeyCommand } = _Ansi;
|
const { KeyCommand } = _Ansi;
|
||||||
const { readKey, ctrlKey } = Fn;
|
const { readKey, ctrlKey } = Fn;
|
||||||
@ -216,502 +445,17 @@ const readKeyTest = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Tests by module
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const ANSITest = () => {
|
|
||||||
const { Ground } = _Ansi;
|
|
||||||
|
|
||||||
return {
|
|
||||||
'color()': () => {
|
|
||||||
assertEquals(Ansi.color.Blue, '\x1b[34m');
|
|
||||||
},
|
|
||||||
'color256()': () => {
|
|
||||||
assertEquals(Ansi.color256(128, Ground.Back), '\x1b[48;5;128m');
|
|
||||||
assertEquals(Ansi.color256(128, Ground.Fore), '\x1b[38;5;128m');
|
|
||||||
},
|
|
||||||
'rgb()': () => {
|
|
||||||
assertEquals(Ansi.rgb(32, 64, 128, Ground.Back), '\x1b[48;2;32;64;128m');
|
|
||||||
assertEquals(Ansi.rgb(32, 64, 128, Ground.Fore), '\x1b[38;2;32;64;128m');
|
|
||||||
},
|
|
||||||
'moveCursor()': () => {
|
|
||||||
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
|
||||||
},
|
|
||||||
'moveCursorForward()': () => {
|
|
||||||
assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
|
|
||||||
},
|
|
||||||
'moveCursorDown()': () => {
|
|
||||||
assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const BufferTest = {
|
|
||||||
'new Buffer': () => {
|
|
||||||
const b = Buffer.default();
|
|
||||||
assertInstanceOf(b, Buffer);
|
|
||||||
assertEquals(b.strlen(), 0);
|
|
||||||
},
|
|
||||||
'.appendLine': () => {
|
|
||||||
const b = Buffer.default();
|
|
||||||
|
|
||||||
// Carriage return and line feed
|
|
||||||
b.appendLine();
|
|
||||||
assertEquals(b.strlen(), 2);
|
|
||||||
|
|
||||||
b.clear();
|
|
||||||
assertEquals(b.strlen(), 0);
|
|
||||||
|
|
||||||
b.appendLine('foo');
|
|
||||||
assertEquals(b.strlen(), 5);
|
|
||||||
},
|
|
||||||
'.append': () => {
|
|
||||||
const b = Buffer.default();
|
|
||||||
|
|
||||||
b.append('foobar');
|
|
||||||
assertEquals(b.strlen(), 6);
|
|
||||||
b.clear();
|
|
||||||
|
|
||||||
b.append('foobar', 3);
|
|
||||||
assertEquals(b.strlen(), 3);
|
|
||||||
},
|
|
||||||
'.flush': async () => {
|
|
||||||
const b = Buffer.default();
|
|
||||||
b.appendLine('foobarbaz' + Ansi.ClearLine);
|
|
||||||
assertEquals(b.strlen(), 14);
|
|
||||||
|
|
||||||
await b.flush();
|
|
||||||
|
|
||||||
assertEquals(b.strlen(), 0);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const DocumentTest = {
|
|
||||||
'.default': () => {
|
|
||||||
const doc = Document.default();
|
|
||||||
assertEquals(doc.numRows, 0);
|
|
||||||
assertTrue(doc.isEmpty());
|
|
||||||
assertEquivalent(doc.row(0), None);
|
|
||||||
},
|
|
||||||
'.open': async () => {
|
|
||||||
const oldDoc = Document.default();
|
|
||||||
oldDoc.insert(Position.default(), 'foobarbaz');
|
|
||||||
assertTrue(oldDoc.dirty);
|
|
||||||
assertEquals(oldDoc.numRows, 1);
|
|
||||||
|
|
||||||
const doc = await oldDoc.open(THIS_FILE);
|
|
||||||
assertEquals(FileLang.TypeScript, doc.fileType);
|
|
||||||
assertFalse(doc.dirty);
|
|
||||||
assertFalse(doc.isEmpty());
|
|
||||||
assertTrue(doc.numRows > 1);
|
|
||||||
},
|
|
||||||
'.save': async () => {
|
|
||||||
const doc = await Document.default().open(THIS_FILE);
|
|
||||||
doc.insertNewline(Position.default());
|
|
||||||
assertTrue(doc.dirty);
|
|
||||||
|
|
||||||
await doc.save('test.file');
|
|
||||||
|
|
||||||
fs.rm('test.file', (err: any) => {
|
|
||||||
assertNone(Option.from(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
assertFalse(doc.dirty);
|
|
||||||
},
|
|
||||||
'.find': async () => {
|
|
||||||
const doc = await Document.default().open(KILO_FILE);
|
|
||||||
|
|
||||||
// First search forward from the beginning of the file
|
|
||||||
const query1 = doc.find(
|
|
||||||
'editor',
|
|
||||||
Position.default(),
|
|
||||||
SearchDirection.Forward,
|
|
||||||
);
|
|
||||||
assertTrue(query1.isSome());
|
|
||||||
const pos1 = query1.unwrap();
|
|
||||||
assertEquivalent(pos1, Position.at(5, 27));
|
|
||||||
|
|
||||||
// Now search backwards from line 400
|
|
||||||
const query2 = doc.find(
|
|
||||||
'realloc',
|
|
||||||
Position.at(44, 400),
|
|
||||||
SearchDirection.Backward,
|
|
||||||
);
|
|
||||||
assertTrue(query2.isSome());
|
|
||||||
const pos2 = query2.unwrap();
|
|
||||||
assertEquivalent(pos2, Position.at(11, 330));
|
|
||||||
|
|
||||||
// And backwards again
|
|
||||||
const query3 = doc.find(
|
|
||||||
'editor',
|
|
||||||
Position.from(pos2),
|
|
||||||
SearchDirection.Backward,
|
|
||||||
);
|
|
||||||
assertTrue(query3.isSome());
|
|
||||||
const pos3 = query3.unwrap();
|
|
||||||
assertEquivalent(pos3, Position.at(5, 328));
|
|
||||||
},
|
|
||||||
'.find - empty result': () => {
|
|
||||||
const doc = Document.default();
|
|
||||||
doc.insertNewline(Position.default());
|
|
||||||
|
|
||||||
const query = doc.find('foo', Position.default(), SearchDirection.Forward);
|
|
||||||
assertNone(query);
|
|
||||||
|
|
||||||
const query2 = doc.find('bar', Position.at(0, 5), SearchDirection.Forward);
|
|
||||||
assertNone(query2);
|
|
||||||
},
|
|
||||||
'.insert': () => {
|
|
||||||
const doc = Document.default();
|
|
||||||
assertFalse(doc.dirty);
|
|
||||||
doc.insert(Position.at(0, 0), 'foobar');
|
|
||||||
assertEquals(doc.numRows, 1);
|
|
||||||
assertTrue(doc.dirty);
|
|
||||||
|
|
||||||
doc.insert(Position.at(2, 0), 'baz');
|
|
||||||
assertEquals(doc.numRows, 1);
|
|
||||||
assertTrue(doc.dirty);
|
|
||||||
|
|
||||||
doc.insert(Position.at(9, 0), 'buzz');
|
|
||||||
assertEquals(doc.numRows, 1);
|
|
||||||
assertTrue(doc.dirty);
|
|
||||||
|
|
||||||
// Update row
|
|
||||||
doc.highlight(None, None);
|
|
||||||
|
|
||||||
const row0 = doc.row(0).unwrap();
|
|
||||||
assertEquals(row0.toString(), 'foobazbarbuzz');
|
|
||||||
assertEquals(row0.rstring(), 'foobazbarbuzz');
|
|
||||||
assertEquals(row0.rsize, 13);
|
|
||||||
|
|
||||||
doc.insert(Position.at(0, 1), 'Lorem Ipsum');
|
|
||||||
assertEquals(doc.numRows, 2);
|
|
||||||
assertTrue(doc.dirty);
|
|
||||||
},
|
|
||||||
'.insertNewline': () => {
|
|
||||||
// Invalid insert location
|
|
||||||
const doc = Document.default();
|
|
||||||
doc.insertNewline(Position.at(0, 3));
|
|
||||||
assertFalse(doc.dirty);
|
|
||||||
assertTrue(doc.isEmpty());
|
|
||||||
|
|
||||||
// Add new empty row
|
|
||||||
const doc2 = Document.default();
|
|
||||||
doc2.insertNewline(Position.default());
|
|
||||||
assertTrue(doc2.dirty);
|
|
||||||
assertFalse(doc2.isEmpty());
|
|
||||||
|
|
||||||
// Split an existing line
|
|
||||||
const doc3 = Document.default();
|
|
||||||
doc3.insert(Position.default(), 'foobar');
|
|
||||||
doc3.insertNewline(Position.at(3, 0));
|
|
||||||
assertEquals(doc3.numRows, 2);
|
|
||||||
assertEquals(doc3.row(0).unwrap().toString(), 'foo');
|
|
||||||
assertEquals(doc3.row(1).unwrap().toString(), 'bar');
|
|
||||||
},
|
|
||||||
'.delete': () => {
|
|
||||||
const doc = Document.default();
|
|
||||||
doc.insert(Position.default(), 'foobar');
|
|
||||||
doc.delete(Position.at(3, 0));
|
|
||||||
assertEquals(doc.row(0).unwrap().toString(), 'fooar');
|
|
||||||
|
|
||||||
// Merge next row
|
|
||||||
const doc2 = Document.default();
|
|
||||||
doc2.insertNewline(Position.default());
|
|
||||||
doc2.insert(Position.at(0, 1), 'foobar');
|
|
||||||
doc2.delete(Position.at(0, 0));
|
|
||||||
assertEquals(doc2.row(0).unwrap().toString(), 'foobar');
|
|
||||||
|
|
||||||
// Invalid delete location
|
|
||||||
const doc3 = Document.default();
|
|
||||||
doc3.insert(Position.default(), 'foobar');
|
|
||||||
doc3.delete(Position.at(0, 3));
|
|
||||||
assertEquals(doc3.row(0).unwrap().toString(), 'foobar');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const EditorTest = {
|
|
||||||
'new Editor': () => {
|
|
||||||
const e = Editor.create(defaultTerminalSize);
|
|
||||||
assertInstanceOf(e, Editor);
|
|
||||||
},
|
|
||||||
'.open': async () => {
|
|
||||||
const e = Editor.create(defaultTerminalSize);
|
|
||||||
await e.open(THIS_FILE);
|
|
||||||
assertInstanceOf(e, Editor);
|
|
||||||
},
|
|
||||||
'.processKeyPress - letters': async () => {
|
|
||||||
const e = Editor.create(defaultTerminalSize);
|
|
||||||
const res = await e.processKeyPress('a');
|
|
||||||
assertTrue(res);
|
|
||||||
},
|
|
||||||
'.processKeyPress - ctrl-q': async () => {
|
|
||||||
// Dirty file (Need to clear confirmation messages)
|
|
||||||
const e = Editor.create(defaultTerminalSize);
|
|
||||||
await e.processKeyPress('d');
|
|
||||||
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
|
|
||||||
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
|
|
||||||
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
|
|
||||||
assertFalse(await e.processKeyPress(Fn.ctrlKey('q')));
|
|
||||||
|
|
||||||
// Clean file
|
|
||||||
const e2 = Editor.create(defaultTerminalSize);
|
|
||||||
const res = await e2.processKeyPress(Fn.ctrlKey('q'));
|
|
||||||
assertFalse(res);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const FileTypeTest = {
|
|
||||||
'FileType.from()': () => {
|
|
||||||
for (const [ext, typeClass] of FT.fileTypeMap.entries()) {
|
|
||||||
const file = `test${ext}`;
|
|
||||||
const syntax = FileType.from(file);
|
|
||||||
|
|
||||||
assertInstanceOf(syntax, typeClass);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const OptionTest = {
|
|
||||||
'Option.from()': () => {
|
|
||||||
assertNone(Option.from(null));
|
|
||||||
assertNone(Option.from());
|
|
||||||
assertEquivalent(Option.from(undefined), None);
|
|
||||||
|
|
||||||
assertSome(Option.from('foo'));
|
|
||||||
assertSome(Option.from(234));
|
|
||||||
assertSome(Option.from({}));
|
|
||||||
assertSome(Some([1, 2, 3]));
|
|
||||||
|
|
||||||
assertEquivalent(Option.from(Some('foo')), Some('foo'));
|
|
||||||
assertEquivalent(Some(Some('bar')), Some('bar'));
|
|
||||||
},
|
|
||||||
'.isSome': () => {
|
|
||||||
assertFalse(None.isSome());
|
|
||||||
assertTrue(Option.from('foo').isSome());
|
|
||||||
assertTrue(Some('foo').isSome());
|
|
||||||
},
|
|
||||||
'.isNone': () => {
|
|
||||||
assertTrue(None.isNone());
|
|
||||||
assertFalse(Option.from('foo').isNone());
|
|
||||||
assertFalse(Some('foo').isNone());
|
|
||||||
},
|
|
||||||
'.toString': () => {
|
|
||||||
assertEquals(Some({}).toString(), 'Some ({})');
|
|
||||||
assertEquals(Some([1, 2, 3]).toString(), 'Some ([1,2,3])');
|
|
||||||
assertEquals(None.toString(), 'None');
|
|
||||||
},
|
|
||||||
'.isSomeAnd': () => {
|
|
||||||
assertFalse(Option.from().isSomeAnd((_a) => true));
|
|
||||||
assertTrue(Option.from('foo').isSomeAnd((a) => typeof a === 'string'));
|
|
||||||
},
|
|
||||||
'.isNoneAnd': () => {
|
|
||||||
assertTrue(None.isNoneAnd(() => true));
|
|
||||||
assertFalse(None.isNoneAnd(() => false));
|
|
||||||
assertFalse(Some('x').isNoneAnd(() => true));
|
|
||||||
},
|
|
||||||
'.map': () => {
|
|
||||||
const fn = (_a: any) => 'bar';
|
|
||||||
|
|
||||||
assertEquivalent(Some('bar'), Some('foo').map(fn));
|
|
||||||
assertNone(None.map(fn));
|
|
||||||
},
|
|
||||||
'.mapOr': () => {
|
|
||||||
const fn = (_a: any) => 'bar';
|
|
||||||
|
|
||||||
assertEquals('bar', Some('foo').mapOr('baz', fn));
|
|
||||||
assertEquals('baz', None.mapOr('baz', fn));
|
|
||||||
},
|
|
||||||
'.mapOrElse': () => {
|
|
||||||
const fn = (_a: any) => 'bar';
|
|
||||||
const defFn = () => 'baz';
|
|
||||||
|
|
||||||
assertEquals('bar', Some('foo').mapOrElse(defFn, fn));
|
|
||||||
assertEquals('baz', None.mapOrElse(defFn, fn));
|
|
||||||
},
|
|
||||||
'.unwrapOr': () => {
|
|
||||||
assertEquals('foo', Some('foo').unwrapOr('bar'));
|
|
||||||
assertEquals('bar', None.unwrapOr('bar'));
|
|
||||||
},
|
|
||||||
'.unwrapOrElse': () => {
|
|
||||||
const fn = () => 'bar';
|
|
||||||
assertEquals('foo', Some('foo').unwrapOrElse(fn));
|
|
||||||
assertEquals('bar', None.unwrapOrElse(fn));
|
|
||||||
},
|
|
||||||
'.and': () => {
|
|
||||||
const optb = Some('bar');
|
|
||||||
assertEquivalent(optb, Some('foo').and(optb));
|
|
||||||
assertEquivalent(None, None.and(optb));
|
|
||||||
},
|
|
||||||
'.andThen': () => {
|
|
||||||
const fn = (x: any) => Some(typeof x === 'string');
|
|
||||||
assertEquivalent(Some(true), Some('foo').andThen(fn));
|
|
||||||
assertNone(None.andThen(fn));
|
|
||||||
},
|
|
||||||
'.or': () => {
|
|
||||||
const optb = Some('bar');
|
|
||||||
assertEquivalent(Some('foo'), Some('foo').or(optb));
|
|
||||||
assertEquivalent(optb, None.or(optb));
|
|
||||||
},
|
|
||||||
'.orElse': () => {
|
|
||||||
const fn = () => Some('bar');
|
|
||||||
assertEquivalent(Some('foo'), Some('foo').orElse(fn));
|
|
||||||
assertEquivalent(Some('bar'), None.orElse(fn));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const PositionTest = {
|
|
||||||
'.default': () => {
|
|
||||||
const p = Position.default();
|
|
||||||
assertEquals(p.x, 0);
|
|
||||||
assertEquals(p.y, 0);
|
|
||||||
},
|
|
||||||
'.at': () => {
|
|
||||||
const p = Position.at(5, 7);
|
|
||||||
assertEquals(p.x, 5);
|
|
||||||
assertEquals(p.y, 7);
|
|
||||||
},
|
|
||||||
'.from': () => {
|
|
||||||
const p1 = Position.at(1, 2);
|
|
||||||
const p2 = Position.from(p1);
|
|
||||||
|
|
||||||
p1.x = 2;
|
|
||||||
p1.y = 4;
|
|
||||||
|
|
||||||
assertEquals(p1.x, 2);
|
|
||||||
assertEquals(p1.y, 4);
|
|
||||||
|
|
||||||
assertEquals(p2.x, 1);
|
|
||||||
assertEquals(p2.y, 2);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const RowTest = {
|
|
||||||
'.default': () => {
|
|
||||||
const row = Row.default();
|
|
||||||
assertEquals(row.toString(), '');
|
|
||||||
},
|
|
||||||
'.from': () => {
|
|
||||||
// From string
|
|
||||||
const row = Row.from('xyz');
|
|
||||||
assertEquals(row.toString(), 'xyz');
|
|
||||||
|
|
||||||
// From existing Row
|
|
||||||
assertEquals(Row.from(row).toString(), row.toString());
|
|
||||||
|
|
||||||
// From 'chars'
|
|
||||||
assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹');
|
|
||||||
},
|
|
||||||
'.append': () => {
|
|
||||||
const row = Row.from('foo');
|
|
||||||
row.append('bar', FileType.default());
|
|
||||||
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, FileType.default()).toString(), row2.toString());
|
|
||||||
},
|
|
||||||
'.find': () => {
|
|
||||||
const normalRow = Row.from('\tFor whom the bell tolls');
|
|
||||||
assertEquivalent(
|
|
||||||
normalRow.find('who', 0, SearchDirection.Forward),
|
|
||||||
Some(5),
|
|
||||||
);
|
|
||||||
assertEquals(normalRow.find('foo', 0, SearchDirection.Forward), None);
|
|
||||||
|
|
||||||
const emojiRow = Row.from('\t😺😸😹');
|
|
||||||
assertEquivalent(emojiRow.find('😹', 0, SearchDirection.Forward), Some(3));
|
|
||||||
assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Forward), None);
|
|
||||||
},
|
|
||||||
'.find backwards': () => {
|
|
||||||
const normalRow = Row.from('For whom the bell tolls');
|
|
||||||
assertEquivalent(
|
|
||||||
normalRow.find('who', 23, SearchDirection.Backward),
|
|
||||||
Some(4),
|
|
||||||
);
|
|
||||||
assertEquals(normalRow.find('foo', 10, SearchDirection.Backward), None);
|
|
||||||
|
|
||||||
const emojiRow = Row.from('😺😸😹');
|
|
||||||
assertEquivalent(emojiRow.find('😸', 2, SearchDirection.Backward), Some(1));
|
|
||||||
assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Backward), None);
|
|
||||||
},
|
|
||||||
'.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(Fn.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.update(None, FileType.default());
|
|
||||||
assertNotEquals(row.chars, row.rchars);
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Test Suite Setup
|
// Test Suite Setup
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
testSuite({
|
testSuite({
|
||||||
fns: fnTest(),
|
|
||||||
'readKey()': readKeyTest(),
|
|
||||||
'ANSI utils': ANSITest(),
|
'ANSI utils': ANSITest(),
|
||||||
Buffer: BufferTest,
|
Buffer: BufferTest,
|
||||||
Document: DocumentTest,
|
Document: DocumentTest,
|
||||||
Editor: EditorTest,
|
Editor: EditorTest,
|
||||||
FileType: FileTypeTest,
|
|
||||||
Option: OptionTest,
|
|
||||||
Position: PositionTest,
|
Position: PositionTest,
|
||||||
Row: RowTest,
|
Row: RowTest,
|
||||||
|
fns: fnTest(),
|
||||||
|
'readKey()': readKeyTest(),
|
||||||
});
|
});
|
||||||
|
@ -42,7 +42,6 @@ export enum AnsiColor {
|
|||||||
FgMagenta,
|
FgMagenta,
|
||||||
FgCyan,
|
FgCyan,
|
||||||
FgWhite,
|
FgWhite,
|
||||||
ForegroundColor,
|
|
||||||
FgDefault,
|
FgDefault,
|
||||||
|
|
||||||
// Background Colors
|
// Background Colors
|
||||||
@ -54,7 +53,6 @@ export enum AnsiColor {
|
|||||||
BgMagenta,
|
BgMagenta,
|
||||||
BgCyan,
|
BgCyan,
|
||||||
BgWhite,
|
BgWhite,
|
||||||
BackgroundColor,
|
|
||||||
BgDefault,
|
BgDefault,
|
||||||
|
|
||||||
// Bright Foreground Colors
|
// Bright Foreground Colors
|
||||||
@ -79,8 +77,8 @@ export enum AnsiColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Ground {
|
export enum Ground {
|
||||||
Fore = AnsiColor.ForegroundColor,
|
Fore = AnsiColor.FgDefault,
|
||||||
Back = AnsiColor.BackgroundColor,
|
Back = AnsiColor.BgDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@ -108,6 +106,7 @@ const moveCursorForward = (col: number): string => code(col, 'C');
|
|||||||
const moveCursorDown = (row: number): string => code(row, 'B');
|
const moveCursorDown = (row: number): string => code(row, 'B');
|
||||||
const textFormat = (param: string | number | string[] | number[]): string =>
|
const textFormat = (param: string | number | string[] | number[]): string =>
|
||||||
code(param, 'm');
|
code(param, 'm');
|
||||||
|
const color = (value: AnsiColor): string => textFormat(value);
|
||||||
const color256 = (value: number, ground: Ground = Ground.Fore): string =>
|
const color256 = (value: number, ground: Ground = Ground.Fore): string =>
|
||||||
textFormat([ground, AnsiColor.Type256, value]);
|
textFormat([ground, AnsiColor.Type256, value]);
|
||||||
const rgb = (
|
const rgb = (
|
||||||
@ -117,9 +116,6 @@ const rgb = (
|
|||||||
ground: Ground = Ground.Fore,
|
ground: Ground = Ground.Fore,
|
||||||
): string => textFormat([ground, AnsiColor.TypeRGB, r, g, b]);
|
): string => textFormat([ground, AnsiColor.TypeRGB, r, g, b]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Ansi terminal codes and helper functions
|
|
||||||
*/
|
|
||||||
export const Ansi = {
|
export const Ansi = {
|
||||||
ClearLine: code('K'),
|
ClearLine: code('K'),
|
||||||
ClearScreen: code('2J'),
|
ClearScreen: code('2J'),
|
||||||
@ -133,46 +129,7 @@ export const Ansi = {
|
|||||||
moveCursorForward,
|
moveCursorForward,
|
||||||
moveCursorDown,
|
moveCursorDown,
|
||||||
textFormat,
|
textFormat,
|
||||||
color: {
|
color,
|
||||||
Black: textFormat(AnsiColor.FgBlack),
|
|
||||||
Red: textFormat(AnsiColor.FgRed),
|
|
||||||
Green: textFormat(AnsiColor.FgGreen),
|
|
||||||
Yellow: textFormat(AnsiColor.FgYellow),
|
|
||||||
Blue: textFormat(AnsiColor.FgBlue),
|
|
||||||
Magenta: textFormat(AnsiColor.FgMagenta),
|
|
||||||
Cyan: textFormat(AnsiColor.FgCyan),
|
|
||||||
White: textFormat(AnsiColor.FgWhite),
|
|
||||||
Default: textFormat(AnsiColor.FgDefault),
|
|
||||||
BrightBlack: textFormat(AnsiColor.FgBrightBlack),
|
|
||||||
BrightRed: textFormat(AnsiColor.FgBrightRed),
|
|
||||||
BrightGreen: textFormat(AnsiColor.FgBrightGreen),
|
|
||||||
BrightYellow: textFormat(AnsiColor.FgBrightYellow),
|
|
||||||
BrightBlue: textFormat(AnsiColor.FgBrightBlue),
|
|
||||||
BrightMagenta: textFormat(AnsiColor.FgBrightMagenta),
|
|
||||||
BrightCyan: textFormat(AnsiColor.FgBrightCyan),
|
|
||||||
BrightWhite: textFormat(AnsiColor.FgBrightWhite),
|
|
||||||
Invert: textFormat(AnsiColor.Invert),
|
|
||||||
background: {
|
|
||||||
Black: textFormat(AnsiColor.BgBlack),
|
|
||||||
Red: textFormat(AnsiColor.BgRed),
|
|
||||||
Green: textFormat(AnsiColor.BgGreen),
|
|
||||||
Yellow: textFormat(AnsiColor.BgYellow),
|
|
||||||
Blue: textFormat(AnsiColor.BgBlue),
|
|
||||||
Magenta: textFormat(AnsiColor.BgMagenta),
|
|
||||||
Cyan: textFormat(AnsiColor.BgCyan),
|
|
||||||
White: textFormat(AnsiColor.BgWhite),
|
|
||||||
Default: textFormat(AnsiColor.BgDefault),
|
|
||||||
BrightBlack: textFormat(AnsiColor.BgBrightBlack),
|
|
||||||
BrightRed: textFormat(AnsiColor.BgBrightRed),
|
|
||||||
BrightGreen: textFormat(AnsiColor.BgBrightGreen),
|
|
||||||
BrightYellow: textFormat(AnsiColor.BgBrightYellow),
|
|
||||||
BrightBlue: textFormat(AnsiColor.BgBrightBlue),
|
|
||||||
BrightMagenta: textFormat(AnsiColor.BgBrightMagenta),
|
|
||||||
BrightCyan: textFormat(AnsiColor.BgBrightCyan),
|
|
||||||
BrightWhite: textFormat(AnsiColor.BgBrightWhite),
|
|
||||||
Invert: textFormat(AnsiColor.Invert),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
color256,
|
color256,
|
||||||
rgb,
|
rgb,
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import { strlen, truncate } from './fns.ts';
|
import { strlen, truncate } from './fns.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple string buffer
|
|
||||||
*/
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
#b = '';
|
#b = '';
|
||||||
|
|
||||||
private constructor() {
|
constructor() {
|
||||||
}
|
|
||||||
|
|
||||||
public static default(): Buffer {
|
|
||||||
return new Buffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public append(s: string, maxLen?: number): void {
|
public append(s: string, maxLen?: number): void {
|
||||||
|
@ -1,27 +1,13 @@
|
|||||||
import Ansi from './ansi.ts';
|
import { ITerminalSize } from './types.ts';
|
||||||
import { HighlightType, ITerminalSize } from './types.ts';
|
|
||||||
|
|
||||||
export const SCROLL_VERSION = '0.1.0';
|
export const SCROLL_VERSION = '0.0.1';
|
||||||
export const SCROLL_QUIT_TIMES = 3;
|
export const SCROLL_QUIT_TIMES = 3;
|
||||||
export const SCROLL_TAB_SIZE = 4;
|
export const SCROLL_TAB_SIZE = 4;
|
||||||
|
|
||||||
export const SCROLL_LOG_FILE_PREFIX = './scroll';
|
export const SCROLL_LOG_FILE = './scroll.log';
|
||||||
export const SCROLL_LOG_FILE_SUFFIX = '.log';
|
export const SCROLL_ERR_FILE = './scroll.err';
|
||||||
|
|
||||||
export const defaultTerminalSize: ITerminalSize = {
|
export const defaultTerminalSize: ITerminalSize = {
|
||||||
rows: 24,
|
rows: 24,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SCROLL_COLOR_SCHEME: Map<HighlightType, string> = new Map([
|
|
||||||
[HighlightType.Match, Ansi.color.Invert], // Inverted color
|
|
||||||
[HighlightType.Number, Ansi.color256(196)], // Bright Red
|
|
||||||
[HighlightType.Character, Ansi.color256(207)], // Magenta
|
|
||||||
[HighlightType.String, Ansi.color256(45)], // Cyan
|
|
||||||
[HighlightType.SingleLineComment, Ansi.color256(248)], // Light Gray
|
|
||||||
[HighlightType.MultiLineComment, Ansi.color256(240)], // Medium-light Gray
|
|
||||||
[HighlightType.Keyword1, Ansi.color256(226)], // Yellow
|
|
||||||
[HighlightType.Keyword2, Ansi.color256(118)], // Green
|
|
||||||
[HighlightType.Operator, Ansi.color256(215)], // Orange/Brown
|
|
||||||
[HighlightType.None, Ansi.ResetFormatting],
|
|
||||||
]);
|
|
||||||
|
@ -1,36 +1,34 @@
|
|||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
import { FileType } from './filetype.ts';
|
import { arrayInsert, some, strlen } from './fns.ts';
|
||||||
import { arrayInsert } from './fns.ts';
|
import { HighlightType } from './highlight.ts';
|
||||||
import Option, { None, Some } from './option.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
import { getRuntime, logWarning } from './runtime.ts';
|
import { Position } from './types.ts';
|
||||||
import { Position, SearchDirection } from './types.ts';
|
import { Search } from './search.ts';
|
||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
/**
|
#rows: Row[];
|
||||||
* Each line of the current document
|
#search: Search;
|
||||||
*/
|
|
||||||
#rows: Row[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param dirty - Has the document been modified?
|
* Has the document been modified?
|
||||||
* @param type - The meta-data for the file type of the current document
|
|
||||||
*/
|
*/
|
||||||
private constructor(
|
public dirty: boolean;
|
||||||
public dirty: boolean = false,
|
|
||||||
public type: FileType = FileType.default(),
|
private constructor() {
|
||||||
) {
|
this.#rows = [];
|
||||||
|
this.#search = new Search();
|
||||||
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get fileType(): string {
|
get numRows(): number {
|
||||||
return this.type.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get numRows(): number {
|
|
||||||
return this.#rows.length;
|
return this.#rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static default(): Document {
|
public static default(): Document {
|
||||||
return new Document();
|
const self = new Document();
|
||||||
|
self.#search.parent = self;
|
||||||
|
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
@ -48,13 +46,9 @@ export class Document {
|
|||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.type = FileType.from(filename);
|
|
||||||
|
|
||||||
const rawFile = await file.openFile(filename);
|
const rawFile = await file.openFile(filename);
|
||||||
rawFile.split(/\r?\n/)
|
rawFile.split(/\r?\n/)
|
||||||
.forEach((row: string) => {
|
.forEach((row) => this.insertRow(this.numRows, row));
|
||||||
this.#rows.push(Row.from(row));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
|
||||||
@ -64,104 +58,71 @@ export class Document {
|
|||||||
/**
|
/**
|
||||||
* Save the current document
|
* Save the current document
|
||||||
*/
|
*/
|
||||||
public async save(filename: string): Promise<void> {
|
public async save(filename: string) {
|
||||||
const { file } = await getRuntime();
|
const { file } = await getRuntime();
|
||||||
|
|
||||||
await file.saveFile(filename, this.rowsToString());
|
await file.saveFile(filename, this.rowsToString());
|
||||||
this.type = FileType.from(filename);
|
|
||||||
|
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public resetFind() {
|
||||||
* Find the cursor position of the query, if it exists
|
this.#search = new Search();
|
||||||
*
|
this.#search.parent = this;
|
||||||
* @param q - the search query
|
}
|
||||||
* @param at - the point from which to start the search
|
|
||||||
* @param direction - which direction to search, backward or forward
|
|
||||||
*/
|
|
||||||
public find(
|
public find(
|
||||||
q: string,
|
q: string,
|
||||||
at: Position,
|
key: string,
|
||||||
direction: SearchDirection,
|
): Position | null {
|
||||||
): Option<Position> {
|
const potential = this.#search.search(q, key);
|
||||||
if (at.y >= this.numRows) {
|
if (some(potential) && potential instanceof Position) {
|
||||||
logWarning('Trying to search beyond the end of the current file', {
|
// Update highlight of search match
|
||||||
at,
|
const row = this.#rows[potential.y];
|
||||||
document: this,
|
|
||||||
});
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = Position.from(at);
|
// Okay, we have to take the Javascript string index (potential.x), convert
|
||||||
|
// it to the Row 'character' index, and then convert that to the Row render index
|
||||||
|
// so that the highlighted color starts in the right place.
|
||||||
|
const start = row.cxToRx(row.byteIndexToCharIndex(potential.x));
|
||||||
|
|
||||||
for (let y = at.y; y >= 0 && y < this.numRows; y += direction) {
|
// Just to be safe with unicode searches, take the number of 'characters'
|
||||||
const maybeMatch = this.#rows[y].find(q, position.x, direction);
|
// as the search query length, not the JS string length.
|
||||||
if (maybeMatch.isSome()) {
|
const end = start + strlen(q);
|
||||||
position.x = maybeMatch.unwrap();
|
|
||||||
return Some(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === SearchDirection.Forward) {
|
for (let i = start; i < end; i++) {
|
||||||
position.y += 1;
|
row.hl[i] = HighlightType.Match;
|
||||||
position.x = 0;
|
|
||||||
} else if (direction === SearchDirection.Backward) {
|
|
||||||
position.y -= 1;
|
|
||||||
position.x = this.#rows[position.y].size;
|
|
||||||
|
|
||||||
console.assert(position.y < this.numRows);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return None;
|
return potential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public insert(at: Position, c: string): void {
|
||||||
|
if (at.y === this.numRows) {
|
||||||
|
this.insertRow(this.numRows, c);
|
||||||
|
} else {
|
||||||
|
this.#rows[at.y].insertChar(at.x, c);
|
||||||
|
this.#rows[at.y].update();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a new line, splitting and/or creating a new row as needed
|
|
||||||
*/
|
|
||||||
public insertNewline(at: Position): void {
|
public insertNewline(at: Position): void {
|
||||||
if (at.y > this.numRows) {
|
if (at.y > this.numRows) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dirty = true;
|
|
||||||
|
|
||||||
// Just add a simple blank line
|
|
||||||
if (at.y === this.numRows) {
|
if (at.y === this.numRows) {
|
||||||
this.#rows.push(Row.default());
|
this.#rows.push(Row.default());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the current row, and insert a new
|
const newRow = this.#rows[at.y].split(at.x);
|
||||||
// row with the leftovers
|
newRow.update();
|
||||||
const currentRow = this.#rows[at.y];
|
|
||||||
const newRow = currentRow.split(at.x, this.type);
|
|
||||||
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);
|
||||||
}
|
|
||||||
|
|
||||||
public insert(at: Position, c: string): void {
|
|
||||||
if (at.y > this.numRows) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
|
|
||||||
if (at.y === this.numRows) {
|
|
||||||
this.#rows.push(Row.from(c));
|
|
||||||
} else {
|
|
||||||
this.#rows[at.y].insertChar(at.x, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unHighlightRows(at.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unHighlightRows(start: number): void {
|
|
||||||
if (this.numRows < start && start >= 1) {
|
|
||||||
for (let i = start - 1; i < this.numRows; i++) {
|
|
||||||
this.#rows[i].isHighlighted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,71 +135,65 @@ export class Document {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dirty = true;
|
const row = this.row(at.y)!;
|
||||||
|
const mergeNextRow = at.x === row.size && at.y + 1 < len;
|
||||||
const maybeRow = this.row(at.y);
|
const mergeIntoPrevRow = at.x === 0 && at.y > 0;
|
||||||
if (maybeRow.isNone()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = maybeRow.unwrap();
|
|
||||||
|
|
||||||
const mergeNextRow = at.x === row.size && this.row(at.y + 1).isSome();
|
|
||||||
|
|
||||||
// If we are at the end of a line, and press delete,
|
// If we are at the end of a line, and press delete,
|
||||||
// add the contents of the next row, and delete
|
// add the contents of the next row, and delete
|
||||||
// the merged row object (This also works for pressing
|
// the merged row object
|
||||||
// backspace at the beginning of a line: the cursor is
|
|
||||||
// moved to the end of the previous line)
|
|
||||||
if (mergeNextRow) {
|
if (mergeNextRow) {
|
||||||
// At the end of a line, pressing delete will merge
|
// At the end of a line, pressing delete will merge
|
||||||
// the next line into the current one
|
// the next line into the current on
|
||||||
const rowToAppend = this.#rows[at.y + 1].toString();
|
const rowToAppend = this.#rows.at(at.y + 1)!.toString();
|
||||||
row.append(rowToAppend, this.type);
|
row.append(rowToAppend);
|
||||||
this.deleteRow(at.y + 1);
|
this.deleteRow(at.y + 1);
|
||||||
|
} else if (mergeIntoPrevRow) {
|
||||||
|
// At the beginning of a line, merge the current line
|
||||||
|
// into the previous Row
|
||||||
|
const rowToAppend = row.toString();
|
||||||
|
this.#rows[at.y - 1].append(rowToAppend);
|
||||||
|
this.deleteRow(at.y);
|
||||||
} else {
|
} else {
|
||||||
row.delete(at.x);
|
row.delete(at.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unHighlightRows(at.y);
|
row.update();
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public row(i: number): Option<Row> {
|
public row(i: number): Row | null {
|
||||||
if (i >= this.numRows || i < 0) {
|
return this.#rows[i] ?? null;
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Option.from(this.#rows.at(i));
|
public insertRow(at: number = this.numRows, s: string = ''): void {
|
||||||
|
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
||||||
|
this.#rows[at].update();
|
||||||
|
|
||||||
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch: Option<string>, limit: Option<number>): void {
|
public highlight(searchMatch?: string): void {
|
||||||
let startWithComment = false;
|
this.#rows.forEach((row) => {
|
||||||
let until = this.numRows;
|
row.update(searchMatch);
|
||||||
if (limit.isSome() && (limit.unwrap() + 1 < this.numRows)) {
|
});
|
||||||
until = limit.unwrap() + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < until; i++) {
|
|
||||||
startWithComment = this.#rows[i].update(
|
|
||||||
searchMatch,
|
|
||||||
this.type,
|
|
||||||
startWithComment,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the specified row
|
* Delete the specified row
|
||||||
* @param at - the index of the row to delete
|
* @param at - the index of the row to delete
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
protected deleteRow(at: number): void {
|
private deleteRow(at: number): void {
|
||||||
this.#rows.splice(at, 1);
|
this.#rows.splice(at, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the array of row objects into one string
|
* Convert the array of row objects into one string
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
protected rowsToString(): string {
|
private rowsToString(): string {
|
||||||
return this.#rows.map((r) => r.toString()).join('\n');
|
return this.#rows.map((r) => r.toString()).join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +0,0 @@
|
|||||||
export * from './filetype/base.ts';
|
|
||||||
export * from './filetype/filetype.ts';
|
|
||||||
|
|
||||||
import FileType from './filetype/filetype.ts';
|
|
||||||
export default FileType;
|
|
@ -1,91 +0,0 @@
|
|||||||
import Option, { None } from '../option.ts';
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// File-related types
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export enum FileLang {
|
|
||||||
C = 'C',
|
|
||||||
CPP = 'C++',
|
|
||||||
TypeScript = 'TypeScript',
|
|
||||||
JavaScript = 'JavaScript',
|
|
||||||
PHP = 'PHP',
|
|
||||||
Go = 'Golang',
|
|
||||||
Rust = 'Rust',
|
|
||||||
CSS = 'CSS',
|
|
||||||
Shell = 'Shell',
|
|
||||||
Plain = 'Plain Text',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HighlightingOptions {
|
|
||||||
characters: boolean;
|
|
||||||
numbers: boolean;
|
|
||||||
octalNumbers: boolean;
|
|
||||||
hexNumbers: boolean;
|
|
||||||
binNumbers: boolean;
|
|
||||||
jsBigInt: boolean;
|
|
||||||
strings: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IFileType {
|
|
||||||
readonly name: FileLang;
|
|
||||||
readonly singleLineComment: Option<string>;
|
|
||||||
readonly multiLineCommentStart: Option<string>;
|
|
||||||
readonly multiLineCommentEnd: Option<string>;
|
|
||||||
readonly keywords1: string[];
|
|
||||||
readonly keywords2: string[];
|
|
||||||
readonly operators: string[];
|
|
||||||
readonly hlOptions: HighlightingOptions;
|
|
||||||
get flags(): HighlightingOptions;
|
|
||||||
get primaryKeywords(): string[];
|
|
||||||
get secondaryKeywords(): string[];
|
|
||||||
hasMultilineComments(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base class for File Types
|
|
||||||
*/
|
|
||||||
export abstract class AbstractFileType implements IFileType {
|
|
||||||
public readonly name: FileLang = FileLang.Plain;
|
|
||||||
public readonly singleLineComment = None;
|
|
||||||
public readonly multiLineCommentStart: Option<string> = None;
|
|
||||||
public readonly multiLineCommentEnd: Option<string> = None;
|
|
||||||
public readonly keywords1: string[] = [];
|
|
||||||
public readonly keywords2: string[] = [];
|
|
||||||
public readonly operators: string[] = [];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
characters: false,
|
|
||||||
numbers: false,
|
|
||||||
octalNumbers: false,
|
|
||||||
hexNumbers: false,
|
|
||||||
binNumbers: false,
|
|
||||||
jsBigInt: false,
|
|
||||||
strings: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
get flags(): HighlightingOptions {
|
|
||||||
return this.hlOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
get primaryKeywords(): string[] {
|
|
||||||
return this.keywords1;
|
|
||||||
}
|
|
||||||
|
|
||||||
get secondaryKeywords(): string[] {
|
|
||||||
return this.keywords2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasMultilineComments(): boolean {
|
|
||||||
return this.multiLineCommentStart.and(this.multiLineCommentEnd).isSome();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultHighlightOptions: HighlightingOptions = {
|
|
||||||
characters: false,
|
|
||||||
numbers: true,
|
|
||||||
octalNumbers: false,
|
|
||||||
hexNumbers: false,
|
|
||||||
binNumbers: false,
|
|
||||||
jsBigInt: false,
|
|
||||||
strings: true,
|
|
||||||
};
|
|
@ -1,151 +0,0 @@
|
|||||||
import Option, { Some } from '../option.ts';
|
|
||||||
import {
|
|
||||||
AbstractFileType,
|
|
||||||
defaultHighlightOptions,
|
|
||||||
FileLang,
|
|
||||||
HighlightingOptions,
|
|
||||||
} from './base.ts';
|
|
||||||
|
|
||||||
export class CFile extends AbstractFileType {
|
|
||||||
public readonly name: FileLang = FileLang.C;
|
|
||||||
public readonly singleLineComment = Some('//');
|
|
||||||
public readonly multiLineCommentStart: Option<string> = Some('/*');
|
|
||||||
public readonly multiLineCommentEnd: Option<string> = Some('*/');
|
|
||||||
public readonly keywords1 = [
|
|
||||||
'continue',
|
|
||||||
'register',
|
|
||||||
'restrict',
|
|
||||||
'volatile',
|
|
||||||
'default',
|
|
||||||
'typedef',
|
|
||||||
'typedef',
|
|
||||||
'switch',
|
|
||||||
'return',
|
|
||||||
'static',
|
|
||||||
'struct',
|
|
||||||
'extern',
|
|
||||||
'inline',
|
|
||||||
'return',
|
|
||||||
'sizeof',
|
|
||||||
'switch',
|
|
||||||
'break',
|
|
||||||
'const',
|
|
||||||
'while',
|
|
||||||
'break',
|
|
||||||
'union',
|
|
||||||
'class',
|
|
||||||
'union',
|
|
||||||
'auto',
|
|
||||||
'case',
|
|
||||||
'else',
|
|
||||||
'enum',
|
|
||||||
'case',
|
|
||||||
'for',
|
|
||||||
'do',
|
|
||||||
'if',
|
|
||||||
];
|
|
||||||
public readonly keywords2 = [
|
|
||||||
'#include',
|
|
||||||
'unsigned',
|
|
||||||
'uint32_t',
|
|
||||||
'uint64_t',
|
|
||||||
'uint16_t',
|
|
||||||
'#define',
|
|
||||||
'#ifndef',
|
|
||||||
'wchar_t',
|
|
||||||
'int32_t',
|
|
||||||
'int64_t',
|
|
||||||
'int16_t',
|
|
||||||
'uint8_t',
|
|
||||||
'double',
|
|
||||||
'signed',
|
|
||||||
'#endif',
|
|
||||||
'#ifdef',
|
|
||||||
'#error',
|
|
||||||
'#undef',
|
|
||||||
'int8_t',
|
|
||||||
'time_t',
|
|
||||||
'size_t',
|
|
||||||
'float',
|
|
||||||
'#elif',
|
|
||||||
'long',
|
|
||||||
'char',
|
|
||||||
'void',
|
|
||||||
'int',
|
|
||||||
'#if',
|
|
||||||
];
|
|
||||||
public readonly operators = [
|
|
||||||
'>>>=',
|
|
||||||
'**=',
|
|
||||||
'&&=',
|
|
||||||
'||=',
|
|
||||||
'??=',
|
|
||||||
'>>>',
|
|
||||||
'<=>',
|
|
||||||
'<<=',
|
|
||||||
'>>=',
|
|
||||||
'+=',
|
|
||||||
'-=',
|
|
||||||
'*=',
|
|
||||||
'/=',
|
|
||||||
'%=',
|
|
||||||
'&=',
|
|
||||||
'^=',
|
|
||||||
'|=',
|
|
||||||
'==',
|
|
||||||
'!=',
|
|
||||||
'>=',
|
|
||||||
'<=',
|
|
||||||
'++',
|
|
||||||
'--',
|
|
||||||
'**',
|
|
||||||
'<<',
|
|
||||||
'>>',
|
|
||||||
'&&',
|
|
||||||
'||',
|
|
||||||
'??',
|
|
||||||
'?.',
|
|
||||||
'++',
|
|
||||||
'--',
|
|
||||||
'==',
|
|
||||||
'!=',
|
|
||||||
'>=',
|
|
||||||
'<=',
|
|
||||||
'&&',
|
|
||||||
'||',
|
|
||||||
'<<',
|
|
||||||
'>>',
|
|
||||||
'+=',
|
|
||||||
'-=',
|
|
||||||
'*=',
|
|
||||||
'/=',
|
|
||||||
'%=',
|
|
||||||
'&=',
|
|
||||||
'|=',
|
|
||||||
'^=',
|
|
||||||
'->',
|
|
||||||
'::',
|
|
||||||
'?',
|
|
||||||
':',
|
|
||||||
'=',
|
|
||||||
'>',
|
|
||||||
'<',
|
|
||||||
'%',
|
|
||||||
'-',
|
|
||||||
'+',
|
|
||||||
'*',
|
|
||||||
'&',
|
|
||||||
'|',
|
|
||||||
'^',
|
|
||||||
'~',
|
|
||||||
'!',
|
|
||||||
'.',
|
|
||||||
',',
|
|
||||||
';',
|
|
||||||
];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
...defaultHighlightOptions,
|
|
||||||
characters: true,
|
|
||||||
hexNumbers: true,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,393 +0,0 @@
|
|||||||
import Option, { None, Some } from '../option.ts';
|
|
||||||
import {
|
|
||||||
AbstractFileType,
|
|
||||||
defaultHighlightOptions,
|
|
||||||
FileLang,
|
|
||||||
HighlightingOptions,
|
|
||||||
} from './base.ts';
|
|
||||||
|
|
||||||
export class CSSFile extends AbstractFileType {
|
|
||||||
public readonly name: FileLang = FileLang.CSS;
|
|
||||||
public readonly singleLineComment = None;
|
|
||||||
public readonly multiLineCommentStart: Option<string> = Some('/*');
|
|
||||||
public readonly multiLineCommentEnd: Option<string> = Some('*/');
|
|
||||||
public readonly keywords1 = [
|
|
||||||
':active',
|
|
||||||
':any-link',
|
|
||||||
':autofill',
|
|
||||||
':checked',
|
|
||||||
':default',
|
|
||||||
':disabled',
|
|
||||||
':empty',
|
|
||||||
':enabled',
|
|
||||||
':first-child',
|
|
||||||
':first-of-type',
|
|
||||||
':focus-visible',
|
|
||||||
':focus-within',
|
|
||||||
':focus',
|
|
||||||
':fullscreen',
|
|
||||||
':hover',
|
|
||||||
':in-range',
|
|
||||||
':indeterminate',
|
|
||||||
':invalid',
|
|
||||||
':last-child',
|
|
||||||
':last-of-type',
|
|
||||||
':link',
|
|
||||||
':modal',
|
|
||||||
':nth-child',
|
|
||||||
':nth-last-child',
|
|
||||||
':nth-last-of-type',
|
|
||||||
':nth-of-type',
|
|
||||||
':only-child',
|
|
||||||
':only-of-type',
|
|
||||||
':optional',
|
|
||||||
':out-of-range',
|
|
||||||
':paused',
|
|
||||||
':picture-in-picture',
|
|
||||||
':placeholder-shown',
|
|
||||||
':playing',
|
|
||||||
':read-only',
|
|
||||||
':read-write',
|
|
||||||
':required',
|
|
||||||
':root',
|
|
||||||
':scope',
|
|
||||||
':target',
|
|
||||||
':user-valid',
|
|
||||||
':valid',
|
|
||||||
':visited',
|
|
||||||
'::after',
|
|
||||||
'::backdrop',
|
|
||||||
'::before',
|
|
||||||
'::cue',
|
|
||||||
'::file-selector-button',
|
|
||||||
'::first-letter',
|
|
||||||
'::first-line',
|
|
||||||
'::grammar-error',
|
|
||||||
'::marker',
|
|
||||||
'::placeholder',
|
|
||||||
'::selection',
|
|
||||||
'::spelling-error',
|
|
||||||
'@charset',
|
|
||||||
'@color-profile',
|
|
||||||
'@container',
|
|
||||||
'@counter-style',
|
|
||||||
'@font-face',
|
|
||||||
'@font-feature-values',
|
|
||||||
'@font-palette-values',
|
|
||||||
'@import',
|
|
||||||
'@keyframes',
|
|
||||||
'@layer',
|
|
||||||
'@media',
|
|
||||||
'@namespace',
|
|
||||||
'@page',
|
|
||||||
'@position-try',
|
|
||||||
'@property',
|
|
||||||
'@scope',
|
|
||||||
'@starting-style',
|
|
||||||
'@supports',
|
|
||||||
'@view-transition',
|
|
||||||
];
|
|
||||||
public readonly keywords2 = [
|
|
||||||
'animation-range-end',
|
|
||||||
'animation-range-start',
|
|
||||||
'accent-color',
|
|
||||||
'animation-timeline',
|
|
||||||
'animation',
|
|
||||||
'animation-timing-function',
|
|
||||||
'animation-composition',
|
|
||||||
'animation-delay',
|
|
||||||
'animation-direction',
|
|
||||||
'appearance',
|
|
||||||
'align-content',
|
|
||||||
'animation-duration',
|
|
||||||
'align-items',
|
|
||||||
'animation-fill-mode',
|
|
||||||
'align-self',
|
|
||||||
'animation-iteration-count',
|
|
||||||
'aspect-ratio',
|
|
||||||
'align-tracks',
|
|
||||||
'animation-name',
|
|
||||||
'all',
|
|
||||||
'animation-play-state',
|
|
||||||
'animation-name',
|
|
||||||
'anchor-name',
|
|
||||||
'border-block-start-color',
|
|
||||||
'border-inline-style',
|
|
||||||
'backdrop-filter',
|
|
||||||
'border-block-start-style',
|
|
||||||
'border-inline-width',
|
|
||||||
'backface-visibility',
|
|
||||||
'border-block-start-width',
|
|
||||||
'border-left',
|
|
||||||
'background',
|
|
||||||
'border-block-style',
|
|
||||||
'border-left-color',
|
|
||||||
'background-attachment',
|
|
||||||
'border-block-width',
|
|
||||||
'border-left-style',
|
|
||||||
'background-blend-mode',
|
|
||||||
'border-bottom',
|
|
||||||
'border-left-width',
|
|
||||||
'background-clip',
|
|
||||||
'border-bottom-color',
|
|
||||||
'border-radius',
|
|
||||||
'background-color',
|
|
||||||
'border-bottom-left-radius',
|
|
||||||
'border-right',
|
|
||||||
'background-image',
|
|
||||||
'border-bottom-right-radius',
|
|
||||||
'border-right-color',
|
|
||||||
'background-origin',
|
|
||||||
'border-bottom-style',
|
|
||||||
'border-right-style',
|
|
||||||
'background-position',
|
|
||||||
'border-bottom-width',
|
|
||||||
'border-right-width',
|
|
||||||
'background-position-x',
|
|
||||||
'border-collapse',
|
|
||||||
'border-spacing',
|
|
||||||
'background-position-y',
|
|
||||||
'border-color',
|
|
||||||
'border-start-end-radius',
|
|
||||||
'background-repeat',
|
|
||||||
'border-end-end-radius',
|
|
||||||
'border-start-start-radius',
|
|
||||||
'background-size',
|
|
||||||
'border-end-start-radius',
|
|
||||||
'border-style',
|
|
||||||
'border-image',
|
|
||||||
'border-top',
|
|
||||||
'border-image-outset',
|
|
||||||
'border-top-color',
|
|
||||||
'border-image-repeat',
|
|
||||||
'border-top-left-radius',
|
|
||||||
'border-image-slice',
|
|
||||||
'border-top-right-radius',
|
|
||||||
'border-image-source',
|
|
||||||
'border-top-style',
|
|
||||||
'border-image-width',
|
|
||||||
'border-top-width',
|
|
||||||
'border-inline',
|
|
||||||
'border-width',
|
|
||||||
'block-size',
|
|
||||||
'border-inline-color',
|
|
||||||
'bottom',
|
|
||||||
'border-inline-end',
|
|
||||||
'border',
|
|
||||||
'border-inline-end-color',
|
|
||||||
'box-decoration-break',
|
|
||||||
'border-block',
|
|
||||||
'border-inline-end-style',
|
|
||||||
'box-shadow',
|
|
||||||
'border-block-color',
|
|
||||||
'border-inline-end-width',
|
|
||||||
'box-sizing',
|
|
||||||
'border-block-end',
|
|
||||||
'border-inline-start',
|
|
||||||
'break-after',
|
|
||||||
'border-block-end-color',
|
|
||||||
'border-inline-start-color',
|
|
||||||
'break-before',
|
|
||||||
'border-block-end-style',
|
|
||||||
'border-inline-start-style',
|
|
||||||
'break-inside',
|
|
||||||
'border-block-end-width',
|
|
||||||
'border-inline-start-width',
|
|
||||||
'border-block-start',
|
|
||||||
'column-rule',
|
|
||||||
'content-visibility',
|
|
||||||
'caption-side',
|
|
||||||
'column-rule-color',
|
|
||||||
'column-rule-style',
|
|
||||||
'caret-color',
|
|
||||||
'column-rule-width',
|
|
||||||
'column-span',
|
|
||||||
'counter-increment',
|
|
||||||
'column-width',
|
|
||||||
'counter-reset',
|
|
||||||
'columns',
|
|
||||||
'counter-set',
|
|
||||||
'contain',
|
|
||||||
'contain-intrinsic-block-size',
|
|
||||||
'clear',
|
|
||||||
'contain-intrinsic-height',
|
|
||||||
'clip',
|
|
||||||
'contain-intrinsic-inline-size',
|
|
||||||
'clip-path',
|
|
||||||
'color',
|
|
||||||
'contain-intrinsic-width',
|
|
||||||
'cursor',
|
|
||||||
'color-scheme',
|
|
||||||
'container',
|
|
||||||
'column-count',
|
|
||||||
'container-name',
|
|
||||||
'column-fill',
|
|
||||||
'container-type',
|
|
||||||
'column-gap',
|
|
||||||
'content',
|
|
||||||
'direction',
|
|
||||||
'display',
|
|
||||||
'empty-cells',
|
|
||||||
'font-synthesis-position',
|
|
||||||
'field-sizing',
|
|
||||||
'font',
|
|
||||||
'font-synthesis-small-caps',
|
|
||||||
'filter',
|
|
||||||
'font-synthesis-style',
|
|
||||||
'font-synthesis-weight',
|
|
||||||
'font-family',
|
|
||||||
'font-variant',
|
|
||||||
'font-variant-alternates',
|
|
||||||
'font-variant-caps',
|
|
||||||
'font-feature-settings',
|
|
||||||
'font-variant-east-asian',
|
|
||||||
'font-variant-emoji',
|
|
||||||
'font-variant-ligatures',
|
|
||||||
'font-variant-numeric',
|
|
||||||
'font-variant-position',
|
|
||||||
'flex',
|
|
||||||
'font-kerning',
|
|
||||||
'font-variation-settings',
|
|
||||||
'flex-basis',
|
|
||||||
'font-language-override',
|
|
||||||
'flex-direction',
|
|
||||||
'font-optical-sizing',
|
|
||||||
'flex-flow',
|
|
||||||
'font-palette',
|
|
||||||
'font-weight',
|
|
||||||
'flex-grow',
|
|
||||||
'flex-shrink',
|
|
||||||
'font-size',
|
|
||||||
'forced-color-adjust',
|
|
||||||
'flex-wrap',
|
|
||||||
'font-size-adjust',
|
|
||||||
'font-stretch',
|
|
||||||
'float',
|
|
||||||
'font-style',
|
|
||||||
'font-synthesis',
|
|
||||||
'grid-auto-columns',
|
|
||||||
'grid-row-end',
|
|
||||||
'gap',
|
|
||||||
'grid-auto-flow',
|
|
||||||
'grid-row-start',
|
|
||||||
'grid-auto-rows',
|
|
||||||
'grid-template',
|
|
||||||
'grid-column',
|
|
||||||
'grid-template-areas',
|
|
||||||
'grid-column-end',
|
|
||||||
'grid-template-columns',
|
|
||||||
'grid',
|
|
||||||
'grid-column-start',
|
|
||||||
'grid-template-rows',
|
|
||||||
'grid-area',
|
|
||||||
'grid-row',
|
|
||||||
'hanging-punctuation',
|
|
||||||
'hyphenate-character',
|
|
||||||
'hyphenate-limit-chars',
|
|
||||||
'height',
|
|
||||||
'hyphens',
|
|
||||||
'initial',
|
|
||||||
'inset-inline',
|
|
||||||
'initial-letter',
|
|
||||||
'inset-inline-end',
|
|
||||||
'image-orientation',
|
|
||||||
'image-rendering',
|
|
||||||
'inline-size',
|
|
||||||
'image-resolution',
|
|
||||||
'inset',
|
|
||||||
'isolation',
|
|
||||||
'inset-area',
|
|
||||||
'inset-block',
|
|
||||||
'inherit',
|
|
||||||
'inset-block-end',
|
|
||||||
'inset-block-start',
|
|
||||||
'justify-content',
|
|
||||||
'justify-self',
|
|
||||||
'justify-items',
|
|
||||||
'justify-tracks',
|
|
||||||
'letter-spacing',
|
|
||||||
'list-style',
|
|
||||||
'list-style-image',
|
|
||||||
'line-break',
|
|
||||||
'list-style-position',
|
|
||||||
'line-clamp',
|
|
||||||
'list-style-type',
|
|
||||||
'line-height',
|
|
||||||
'left',
|
|
||||||
'line-height-step',
|
|
||||||
'mask-border-outset',
|
|
||||||
'margin',
|
|
||||||
'mask-border-repeat',
|
|
||||||
'margin-block',
|
|
||||||
'mask-border-slice',
|
|
||||||
'margin-block-end',
|
|
||||||
'mask-border-source',
|
|
||||||
'margin-block-start',
|
|
||||||
'mask-border-width',
|
|
||||||
'max-height',
|
|
||||||
'margin-bottom',
|
|
||||||
'mask-clip',
|
|
||||||
'max-inline-size',
|
|
||||||
'margin-inline',
|
|
||||||
'mask-composite',
|
|
||||||
'margin-inline-end',
|
|
||||||
'mask-image',
|
|
||||||
'max-width',
|
|
||||||
'margin-inline-start',
|
|
||||||
'mask-mode',
|
|
||||||
'margin-left',
|
|
||||||
'mask-origin',
|
|
||||||
'margin-right',
|
|
||||||
'mask-position',
|
|
||||||
'min-block-size',
|
|
||||||
'margin-top',
|
|
||||||
'mask-repeat',
|
|
||||||
'min-height',
|
|
||||||
'margin-trim',
|
|
||||||
'mask-size',
|
|
||||||
'min-inline-size',
|
|
||||||
'mask-type',
|
|
||||||
'min-width',
|
|
||||||
'masonry-auto-flow',
|
|
||||||
'mask',
|
|
||||||
'math-depth',
|
|
||||||
'mix-blend-mode',
|
|
||||||
'mask-border',
|
|
||||||
'math-shift',
|
|
||||||
'mask-border-mode',
|
|
||||||
'math-style',
|
|
||||||
'object-fit',
|
|
||||||
'order',
|
|
||||||
'overflow-inline',
|
|
||||||
'object-position',
|
|
||||||
'overflow-wrap',
|
|
||||||
'offset',
|
|
||||||
'orphans',
|
|
||||||
'overflow-x',
|
|
||||||
'offset-anchor',
|
|
||||||
'overflow-y',
|
|
||||||
'offset-distance',
|
|
||||||
'outline',
|
|
||||||
'overlay',
|
|
||||||
'offset-path',
|
|
||||||
'outline-color',
|
|
||||||
'offset-position',
|
|
||||||
'outline-offset',
|
|
||||||
'offset-rotate',
|
|
||||||
'outline-style',
|
|
||||||
'overscroll-behavior',
|
|
||||||
'outline-width',
|
|
||||||
'overscroll-behavior-block',
|
|
||||||
'overscroll-behavior-inline',
|
|
||||||
'opacity',
|
|
||||||
'overflow-anchor',
|
|
||||||
'overscroll-behavior-x',
|
|
||||||
'overflow-block',
|
|
||||||
'overscroll-behavior-y',
|
|
||||||
'overflow-clip-margin',
|
|
||||||
];
|
|
||||||
public readonly operators = ['::', ':', ',', '+', '>', '~', '-'];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
...defaultHighlightOptions,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import { node_path as path } from '../runtime.ts';
|
|
||||||
import { AbstractFileType } from './base.ts';
|
|
||||||
import { CFile } from './c.ts';
|
|
||||||
import { CSSFile } from './css.ts';
|
|
||||||
import { JavaScriptFile, TypeScriptFile } from './javascript.ts';
|
|
||||||
import { RustFile } from './rust.ts';
|
|
||||||
import { ShellFile } from './shell.ts';
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// External interface
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const fileTypeMap = new Map([
|
|
||||||
['.bash', ShellFile],
|
|
||||||
['.c', CFile],
|
|
||||||
['.css', CSSFile],
|
|
||||||
['.h', CFile],
|
|
||||||
['.js', JavaScriptFile],
|
|
||||||
['.json', JavaScriptFile],
|
|
||||||
['.jsx', JavaScriptFile],
|
|
||||||
['.mjs', JavaScriptFile],
|
|
||||||
['.rs', RustFile],
|
|
||||||
['.sh', ShellFile],
|
|
||||||
['.ts', TypeScriptFile],
|
|
||||||
['.tsx', TypeScriptFile],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export class FileType extends AbstractFileType {
|
|
||||||
public static default(): FileType {
|
|
||||||
return new FileType();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static from(filename: string): FileType {
|
|
||||||
const ext = path.extname(filename);
|
|
||||||
const type = fileTypeMap.get(ext) ?? FileType;
|
|
||||||
|
|
||||||
return new type();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileType;
|
|
@ -1,152 +0,0 @@
|
|||||||
import Option, { Some } from '../option.ts';
|
|
||||||
import {
|
|
||||||
AbstractFileType,
|
|
||||||
defaultHighlightOptions,
|
|
||||||
FileLang,
|
|
||||||
HighlightingOptions,
|
|
||||||
} from './base.ts';
|
|
||||||
|
|
||||||
export class JavaScriptFile extends AbstractFileType {
|
|
||||||
public readonly name: FileLang = FileLang.JavaScript;
|
|
||||||
public readonly singleLineComment = Some('//');
|
|
||||||
public readonly multiLineCommentStart: Option<string> = Some('/*');
|
|
||||||
public readonly multiLineCommentEnd: Option<string> = Some('*/');
|
|
||||||
public readonly keywords1 = [
|
|
||||||
'=>',
|
|
||||||
'await',
|
|
||||||
'break',
|
|
||||||
'case',
|
|
||||||
'catch',
|
|
||||||
'class',
|
|
||||||
'const',
|
|
||||||
'continue',
|
|
||||||
'debugger',
|
|
||||||
'default',
|
|
||||||
'delete',
|
|
||||||
'do',
|
|
||||||
'else',
|
|
||||||
'export',
|
|
||||||
'extends',
|
|
||||||
'false',
|
|
||||||
'finally',
|
|
||||||
'for',
|
|
||||||
'function',
|
|
||||||
'if',
|
|
||||||
'import',
|
|
||||||
'in',
|
|
||||||
'instanceof',
|
|
||||||
'let',
|
|
||||||
'new',
|
|
||||||
'null',
|
|
||||||
'return',
|
|
||||||
'static',
|
|
||||||
'super',
|
|
||||||
'switch',
|
|
||||||
'this',
|
|
||||||
'throw',
|
|
||||||
'true',
|
|
||||||
'try',
|
|
||||||
'typeof',
|
|
||||||
'var',
|
|
||||||
'void',
|
|
||||||
'while',
|
|
||||||
'with',
|
|
||||||
'yield',
|
|
||||||
];
|
|
||||||
public readonly keywords2 = [
|
|
||||||
'arguments',
|
|
||||||
'as',
|
|
||||||
'async',
|
|
||||||
'BigInt',
|
|
||||||
'Boolean',
|
|
||||||
'eval',
|
|
||||||
'from',
|
|
||||||
'get',
|
|
||||||
'JSON',
|
|
||||||
'Math',
|
|
||||||
'Number',
|
|
||||||
'Object',
|
|
||||||
'of',
|
|
||||||
'set',
|
|
||||||
'String',
|
|
||||||
'Symbol',
|
|
||||||
'undefined',
|
|
||||||
];
|
|
||||||
public readonly operators = [
|
|
||||||
'>>>=',
|
|
||||||
'**=',
|
|
||||||
'<<=',
|
|
||||||
'>>=',
|
|
||||||
'&&=',
|
|
||||||
'||=',
|
|
||||||
'??=',
|
|
||||||
'===',
|
|
||||||
'!==',
|
|
||||||
'>>>',
|
|
||||||
'+=',
|
|
||||||
'-=',
|
|
||||||
'*=',
|
|
||||||
'/=',
|
|
||||||
'%=',
|
|
||||||
'&=',
|
|
||||||
'^=',
|
|
||||||
'|=',
|
|
||||||
'==',
|
|
||||||
'!=',
|
|
||||||
'>=',
|
|
||||||
'<=',
|
|
||||||
'++',
|
|
||||||
'--',
|
|
||||||
'**',
|
|
||||||
'<<',
|
|
||||||
'>>',
|
|
||||||
'&&',
|
|
||||||
'||',
|
|
||||||
'??',
|
|
||||||
'?.',
|
|
||||||
'?',
|
|
||||||
':',
|
|
||||||
'=',
|
|
||||||
'>',
|
|
||||||
'<',
|
|
||||||
'%',
|
|
||||||
'-',
|
|
||||||
'+',
|
|
||||||
'&',
|
|
||||||
'|',
|
|
||||||
'^',
|
|
||||||
'~',
|
|
||||||
'!',
|
|
||||||
'.',
|
|
||||||
',',
|
|
||||||
';',
|
|
||||||
];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
...defaultHighlightOptions,
|
|
||||||
octalNumbers: true,
|
|
||||||
hexNumbers: true,
|
|
||||||
binNumbers: true,
|
|
||||||
jsBigInt: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TypeScriptFile extends JavaScriptFile {
|
|
||||||
public readonly name: FileLang = FileLang.TypeScript;
|
|
||||||
public readonly keywords2 = [
|
|
||||||
...super.secondaryKeywords,
|
|
||||||
// Typescript-specific
|
|
||||||
'any',
|
|
||||||
'bigint',
|
|
||||||
'boolean',
|
|
||||||
'enum',
|
|
||||||
'interface',
|
|
||||||
'keyof',
|
|
||||||
'number',
|
|
||||||
'private',
|
|
||||||
'protected',
|
|
||||||
'public',
|
|
||||||
'string',
|
|
||||||
'type',
|
|
||||||
'unknown',
|
|
||||||
];
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
import Option, { Some } from '../option.ts';
|
|
||||||
import {
|
|
||||||
AbstractFileType,
|
|
||||||
defaultHighlightOptions,
|
|
||||||
FileLang,
|
|
||||||
HighlightingOptions,
|
|
||||||
} from './base.ts';
|
|
||||||
|
|
||||||
export class RustFile extends AbstractFileType {
|
|
||||||
public readonly name: FileLang = FileLang.Rust;
|
|
||||||
public readonly singleLineComment = Some('//');
|
|
||||||
public readonly multiLineCommentStart: Option<string> = Some('/*');
|
|
||||||
public readonly multiLineCommentEnd: Option<string> = Some('*/');
|
|
||||||
public readonly keywords1 = [
|
|
||||||
'continue',
|
|
||||||
'return',
|
|
||||||
'static',
|
|
||||||
'struct',
|
|
||||||
'unsafe',
|
|
||||||
'break',
|
|
||||||
'const',
|
|
||||||
'crate',
|
|
||||||
'extern',
|
|
||||||
'match',
|
|
||||||
'super',
|
|
||||||
'trait',
|
|
||||||
'where',
|
|
||||||
'else',
|
|
||||||
'enum',
|
|
||||||
'false',
|
|
||||||
'impl',
|
|
||||||
'loop',
|
|
||||||
'move',
|
|
||||||
'self',
|
|
||||||
'type',
|
|
||||||
'while',
|
|
||||||
'for',
|
|
||||||
'let',
|
|
||||||
'mod',
|
|
||||||
'pub',
|
|
||||||
'ref',
|
|
||||||
'true',
|
|
||||||
'use',
|
|
||||||
'mut',
|
|
||||||
'as',
|
|
||||||
'fn',
|
|
||||||
'if',
|
|
||||||
'in',
|
|
||||||
];
|
|
||||||
public readonly keywords2 = [
|
|
||||||
'DoubleEndedIterator',
|
|
||||||
'ExactSizeIterator',
|
|
||||||
'IntoIterator',
|
|
||||||
'PartialOrd',
|
|
||||||
'PartialEq',
|
|
||||||
'Iterator',
|
|
||||||
'ToString',
|
|
||||||
'Default',
|
|
||||||
'ToOwned',
|
|
||||||
'Extend',
|
|
||||||
'FnOnce',
|
|
||||||
'Option',
|
|
||||||
'String',
|
|
||||||
'AsMut',
|
|
||||||
'AsRef',
|
|
||||||
'Clone',
|
|
||||||
'Debug',
|
|
||||||
'FnMut',
|
|
||||||
'Sized',
|
|
||||||
'Unpin',
|
|
||||||
'array',
|
|
||||||
'isize',
|
|
||||||
'usize',
|
|
||||||
'&str',
|
|
||||||
'Copy',
|
|
||||||
'Drop',
|
|
||||||
'From',
|
|
||||||
'Into',
|
|
||||||
'None',
|
|
||||||
'Self',
|
|
||||||
'Send',
|
|
||||||
'Some',
|
|
||||||
'Sync',
|
|
||||||
'bool',
|
|
||||||
'char',
|
|
||||||
'i128',
|
|
||||||
'u128',
|
|
||||||
'Box',
|
|
||||||
'Err',
|
|
||||||
'Ord',
|
|
||||||
'Vec',
|
|
||||||
'dyn',
|
|
||||||
'f32',
|
|
||||||
'f64',
|
|
||||||
'i16',
|
|
||||||
'i32',
|
|
||||||
'i64',
|
|
||||||
'str',
|
|
||||||
'u16',
|
|
||||||
'u32',
|
|
||||||
'u64',
|
|
||||||
'Eq',
|
|
||||||
'Fn',
|
|
||||||
'Ok',
|
|
||||||
'i8',
|
|
||||||
'u8',
|
|
||||||
'&mut self',
|
|
||||||
'&mut',
|
|
||||||
'&self',
|
|
||||||
'self',
|
|
||||||
];
|
|
||||||
public readonly operators = [
|
|
||||||
'||=',
|
|
||||||
'>>=',
|
|
||||||
'<=>',
|
|
||||||
'<<=',
|
|
||||||
'&&=',
|
|
||||||
'**=',
|
|
||||||
'..=',
|
|
||||||
'...',
|
|
||||||
'||',
|
|
||||||
'|=',
|
|
||||||
'>>',
|
|
||||||
'>=',
|
|
||||||
'=>',
|
|
||||||
'==',
|
|
||||||
'<=',
|
|
||||||
'<<',
|
|
||||||
'<-',
|
|
||||||
'+=',
|
|
||||||
'++',
|
|
||||||
'^=',
|
|
||||||
'%=',
|
|
||||||
'&=',
|
|
||||||
'&&',
|
|
||||||
'/=',
|
|
||||||
'*=',
|
|
||||||
'**',
|
|
||||||
'..',
|
|
||||||
'!=',
|
|
||||||
':=',
|
|
||||||
'::',
|
|
||||||
'->',
|
|
||||||
'-=',
|
|
||||||
'--',
|
|
||||||
'~',
|
|
||||||
'|',
|
|
||||||
'>',
|
|
||||||
'=',
|
|
||||||
'<',
|
|
||||||
'+',
|
|
||||||
'^',
|
|
||||||
'%',
|
|
||||||
'&',
|
|
||||||
'*',
|
|
||||||
'.',
|
|
||||||
'!',
|
|
||||||
':',
|
|
||||||
';',
|
|
||||||
',',
|
|
||||||
'-',
|
|
||||||
];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
...defaultHighlightOptions,
|
|
||||||
characters: true,
|
|
||||||
binNumbers: true,
|
|
||||||
hexNumbers: true,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { Some } from '../option.ts';
|
|
||||||
import {
|
|
||||||
AbstractFileType,
|
|
||||||
defaultHighlightOptions,
|
|
||||||
FileLang,
|
|
||||||
HighlightingOptions,
|
|
||||||
} from './base.ts';
|
|
||||||
|
|
||||||
export class ShellFile extends AbstractFileType {
|
|
||||||
public readonly name: FileLang = FileLang.Shell;
|
|
||||||
public readonly singleLineComment = Some('#');
|
|
||||||
public readonly keywords1 = [
|
|
||||||
'case',
|
|
||||||
'do',
|
|
||||||
'done',
|
|
||||||
'elif',
|
|
||||||
'else',
|
|
||||||
'esac',
|
|
||||||
'fi',
|
|
||||||
'for',
|
|
||||||
'function',
|
|
||||||
'if',
|
|
||||||
'in',
|
|
||||||
'select',
|
|
||||||
'then',
|
|
||||||
'time',
|
|
||||||
'until',
|
|
||||||
'while',
|
|
||||||
'declare',
|
|
||||||
];
|
|
||||||
public readonly keywords2 = ['set'];
|
|
||||||
public readonly operators = ['[[', ']]'];
|
|
||||||
public readonly hlOptions: HighlightingOptions = {
|
|
||||||
...defaultHighlightOptions,
|
|
||||||
numbers: false,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
import Ansi, { KeyCommand } from './ansi.ts';
|
import { KeyCommand } from './ansi.ts';
|
||||||
import { SCROLL_COLOR_SCHEME } from './config.ts';
|
|
||||||
import { HighlightType } from './types.ts';
|
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
@ -13,6 +11,20 @@ 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
|
||||||
@ -30,7 +42,7 @@ export function readKey(raw: Uint8Array): string {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some key codes have multiple potential inputs
|
// Some keycodes have multiple potential inputs
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
case '\x1b[1~':
|
case '\x1b[1~':
|
||||||
case '\x1b[7~':
|
case '\x1b[7~':
|
||||||
@ -59,23 +71,12 @@ export function readKey(raw: Uint8Array): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the configured ANSI formatting escape codes for the
|
|
||||||
* type of syntax specified
|
|
||||||
*
|
|
||||||
* @param type The type of syntax to highlight
|
|
||||||
*/
|
|
||||||
export function highlightToColor(type: HighlightType): string {
|
|
||||||
return SCROLL_COLOR_SCHEME.get(type) ?? Ansi.ResetFormatting;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Array manipulation
|
// Array manipulation
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a value into an array at the specified index
|
* Insert a value into an array at the specified index
|
||||||
*
|
|
||||||
* @param arr - the array
|
* @param arr - the array
|
||||||
* @param at - the index to insert at
|
* @param at - the index to insert at
|
||||||
* @param value - what to add into the array
|
* @param value - what to add into the array
|
||||||
@ -100,7 +101,6 @@ export function arrayInsert<T>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Subtract two numbers, returning a zero if the result is negative
|
* Subtract two numbers, returning a zero if the result is negative
|
||||||
*
|
|
||||||
* @param l
|
* @param l
|
||||||
* @param s
|
* @param s
|
||||||
*/
|
*/
|
||||||
@ -149,10 +149,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 unicode code points
|
* @param s - the string to split into 'characters'
|
||||||
*/
|
*/
|
||||||
export function strChars(s: string): string[] {
|
export function strChars(s: string): string[] {
|
||||||
return [...s];
|
return s.split(/(?:)/u);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,17 +164,6 @@ export function strlen(s: string): number {
|
|||||||
return strChars(s).length;
|
return strChars(s).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a slice of a string
|
|
||||||
*
|
|
||||||
* @param s - the string
|
|
||||||
* @param from - the 'character' index of the start of the slice
|
|
||||||
* @param to - the 'character' index of the last character you want
|
|
||||||
*/
|
|
||||||
export function substr(s: string, from: number, to?: number): string {
|
|
||||||
return strChars(s).slice(from, to).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are all the characters in the string in ASCII range?
|
* Are all the characters in the string in ASCII range?
|
||||||
*
|
*
|
||||||
@ -204,15 +193,6 @@ export function isControl(char: string): boolean {
|
|||||||
return isAscii(char) && (code === 0x7f || code < 0x20);
|
return isAscii(char) && (code === 0x7f || code < 0x20);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the one char string a common separator/operator character
|
|
||||||
*
|
|
||||||
* @param char - a one character string to check
|
|
||||||
*/
|
|
||||||
export function isSeparator(char: string): boolean {
|
|
||||||
return /\s/.test(char) || char === '\0' || ',.()+-/*=~%<>[];'.includes(char);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the key code for a ctrl chord
|
* Get the key code for a ctrl chord
|
||||||
*
|
*
|
||||||
|
20
src/common/highlight.ts
Normal file
20
src/common/highlight.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Ansi from './ansi.ts';
|
||||||
|
|
||||||
|
export enum HighlightType {
|
||||||
|
None,
|
||||||
|
Number,
|
||||||
|
Match,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightToColor(type: HighlightType): string {
|
||||||
|
switch (type) {
|
||||||
|
case HighlightType.Number:
|
||||||
|
return Ansi.color256(196);
|
||||||
|
|
||||||
|
case HighlightType.Match:
|
||||||
|
return Ansi.color256(21);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Ansi.ResetFormatting;
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,9 @@
|
|||||||
|
import process from 'node:process';
|
||||||
import { readKey } from './fns.ts';
|
import { readKey } from './fns.ts';
|
||||||
import {
|
import { getRuntime, logError } from './runtime.ts';
|
||||||
getRuntime,
|
|
||||||
logError,
|
|
||||||
logWarning,
|
|
||||||
node_process as process,
|
|
||||||
} from './runtime.ts';
|
|
||||||
import Editor from './editor.ts';
|
import Editor from './editor.ts';
|
||||||
|
|
||||||
/**
|
export async function main() {
|
||||||
* The main runtime loop
|
|
||||||
*
|
|
||||||
* Only returns on error or quit
|
|
||||||
*/
|
|
||||||
export async function main(): Promise<void> {
|
|
||||||
const rt = await getRuntime();
|
const rt = await getRuntime();
|
||||||
const { term } = rt;
|
const { term } = rt;
|
||||||
|
|
||||||
@ -23,13 +14,15 @@ export async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Setup error handler to log to file
|
// Setup error handler to log to file
|
||||||
rt.onEvent('error', (error: any) => {
|
rt.onEvent('error', (error) => {
|
||||||
process.stdin.setRawMode(false);
|
process.stdin.setRawMode(false);
|
||||||
logError(JSON.stringify(error, null, 2));
|
logError(JSON.stringify(error, null, 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const terminalSize = await term.getTerminalSize();
|
||||||
|
|
||||||
// Create the editor itself
|
// Create the editor itself
|
||||||
const editor = Editor.create(await term.getTerminalSize());
|
const editor = new Editor(terminalSize);
|
||||||
|
|
||||||
// Process cli arguments
|
// Process cli arguments
|
||||||
if (term.argv.length > 0) {
|
if (term.argv.length > 0) {
|
||||||
@ -50,9 +43,7 @@ export async function main(): Promise<void> {
|
|||||||
for await (const char of term.inputLoop()) {
|
for await (const char of term.inputLoop()) {
|
||||||
const parsed = readKey(char);
|
const parsed = readKey(char);
|
||||||
if (char.length === 0 || parsed.length === 0) {
|
if (char.length === 0 || parsed.length === 0) {
|
||||||
logWarning('Empty input returned from runtime input loop');
|
continue;
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process input
|
// Process input
|
||||||
@ -60,9 +51,6 @@ export async function main(): Promise<void> {
|
|||||||
if (!shouldLoop) {
|
if (!shouldLoop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render output
|
|
||||||
await editor.refreshScreen();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,261 +0,0 @@
|
|||||||
/**
|
|
||||||
* The sad, lonely enum that should be more tightly coupled
|
|
||||||
* to the Option type...but this isn't Rust
|
|
||||||
*/
|
|
||||||
enum OptionType {
|
|
||||||
Some = 'Some',
|
|
||||||
None = 'None',
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Typeguards to handle Some/None difference
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const isOption = <T>(v: any): v is Option<T> => v instanceof Option;
|
|
||||||
|
|
||||||
class OptNone {
|
|
||||||
public type: OptionType = OptionType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OptSome<T> {
|
|
||||||
public type: OptionType = OptionType.Some;
|
|
||||||
|
|
||||||
constructor(public value: T) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptType<T> = OptNone | OptSome<T>;
|
|
||||||
|
|
||||||
const isSome = <T>(v: OptType<T>): v is OptSome<T> =>
|
|
||||||
'value' in v && v.type === OptionType.Some;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rust-style optional type
|
|
||||||
*
|
|
||||||
* Based on https://gist.github.com/s-panferov/575da5a7131c285c0539
|
|
||||||
*/
|
|
||||||
export class Option<T> {
|
|
||||||
/**
|
|
||||||
* The placeholder for the 'None' value type
|
|
||||||
*/
|
|
||||||
private static _None: Option<any> = new Option(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this a 'Some' or a 'None'?
|
|
||||||
*/
|
|
||||||
private readonly inner: OptType<T>;
|
|
||||||
|
|
||||||
private constructor(v?: T) {
|
|
||||||
this.inner = (v !== undefined && v !== null)
|
|
||||||
? new OptSome(v)
|
|
||||||
: new OptNone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of the Rust `Option`.`None` enum value
|
|
||||||
*/
|
|
||||||
public static get None(): Option<any> {
|
|
||||||
return Option._None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of the Rust `Option`.`Some` enum value
|
|
||||||
*
|
|
||||||
* If the value passed is null or undefined, this will throw an Error
|
|
||||||
*
|
|
||||||
* @param v The value to wrap
|
|
||||||
*/
|
|
||||||
public static Some<X>(v: any): Option<X> | never {
|
|
||||||
const maybeSome: Option<X> = Option.from(v);
|
|
||||||
|
|
||||||
if (maybeSome.isNone()) {
|
|
||||||
throw new Error('Cannot create Some<T> with an empty value');
|
|
||||||
} else {
|
|
||||||
return maybeSome;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new `Option`
|
|
||||||
*
|
|
||||||
* If the value is null or undefined, the `Option` will have a `None` type
|
|
||||||
*
|
|
||||||
* @param v The value to wrap
|
|
||||||
*/
|
|
||||||
public static from<X>(v?: any): Option<X> {
|
|
||||||
return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wrapped value is not null or undefined
|
|
||||||
*/
|
|
||||||
public isSome(): boolean {
|
|
||||||
return isSome(this.inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wrapped value is null or undefined
|
|
||||||
*/
|
|
||||||
public isNone(): boolean {
|
|
||||||
return !this.isSome();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current `Option` is `Some`. If it is,
|
|
||||||
* return the value of the passed function.
|
|
||||||
*
|
|
||||||
* Otherwise, returns false
|
|
||||||
*
|
|
||||||
* @param fn A boolean check to run on the wrapped value
|
|
||||||
*/
|
|
||||||
public isSomeAnd(fn: (a: T) => boolean): boolean {
|
|
||||||
return isSome(this.inner) ? fn(this.inner.value) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current `Option` is `None`. If it is,
|
|
||||||
* return the value of the passed function.
|
|
||||||
*
|
|
||||||
* Otherwise, return false
|
|
||||||
*
|
|
||||||
* @param fn A function returning a boolean value
|
|
||||||
*/
|
|
||||||
public isNoneAnd(fn: () => boolean): boolean {
|
|
||||||
return this.isNone() ? fn() : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform the inner value of the `Option` with the passed function.
|
|
||||||
* If this `Option` is `Some`, a new `Option` with the transformed value
|
|
||||||
* is returned. Otherwise `None` is returned
|
|
||||||
*
|
|
||||||
* @param fn A function that takes the inner value of the `Option` and returns a new one
|
|
||||||
*/
|
|
||||||
public map<U>(fn: (a: T) => U): Option<U> {
|
|
||||||
return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this `Option` is `Some`, return the transformed inner value via the passed function.
|
|
||||||
*
|
|
||||||
* Otherwise, return the passed default value
|
|
||||||
*
|
|
||||||
* @param def The default value to return if this `Option` is `None`
|
|
||||||
* @param fn A function that takes the inner value of this `Option` and returns a new value
|
|
||||||
*/
|
|
||||||
public mapOr<U>(def: U, fn: (a: T) => U): U {
|
|
||||||
return isSome(this.inner) ? fn(this.inner.value) : def;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this `Option` is `Some`, return the transformed inner value via the passed function (fn).
|
|
||||||
*
|
|
||||||
* Otherwise run the function (def) to return a value
|
|
||||||
*
|
|
||||||
* @param def A function to return a value if this `Option` is `None`
|
|
||||||
* @param fn A function that takes the inner value of this `Option` and returns a new value
|
|
||||||
*/
|
|
||||||
public mapOrElse<U>(def: () => U, fn: (a: T) => U): U {
|
|
||||||
return isSome(this.inner) ? fn(this.inner.value) : def();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the inner value if not `None`.
|
|
||||||
* Otherwise, throw a new exception with the passed message
|
|
||||||
*
|
|
||||||
* @param err
|
|
||||||
*/
|
|
||||||
public assert(err: string): T | never {
|
|
||||||
if (isSome(this.inner)) {
|
|
||||||
return this.inner.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the inner value if is `Some<T>`.
|
|
||||||
*
|
|
||||||
* If `None`, throws an exception.
|
|
||||||
*/
|
|
||||||
public unwrap(): T | never {
|
|
||||||
return this.assert("Called unwrap on a 'None'");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the inner value if is `Some<T>`,
|
|
||||||
* Otherwise, return the passed default value
|
|
||||||
*
|
|
||||||
* @param def Value to return on `None` value
|
|
||||||
*/
|
|
||||||
public unwrapOr(def: T): T {
|
|
||||||
return isSome(this.inner) ? this.inner.value : def;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the inner value if is `Some<T>`,
|
|
||||||
* Otherwise, return the value generated by the passed function
|
|
||||||
*
|
|
||||||
* @param f Function to run on `None` value
|
|
||||||
*/
|
|
||||||
public unwrapOrElse(f: () => T): T {
|
|
||||||
return isSome(this.inner) ? this.inner.value : f();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this `Option` and the passed option are both `Some`,
|
|
||||||
* otherwise return `None`
|
|
||||||
*
|
|
||||||
* @param optb Another `Option` to check
|
|
||||||
*/
|
|
||||||
public and<U>(optb: Option<U>): Option<U> {
|
|
||||||
return isSome(this.inner) ? optb : Option.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this `Option` is `Some`. If it is, run the passed
|
|
||||||
* function with the wrapped value.
|
|
||||||
*
|
|
||||||
* Otherwise, return None
|
|
||||||
*
|
|
||||||
* @param f function to run on the wrapped value
|
|
||||||
*/
|
|
||||||
public andThen<U>(f: (a: T) => Option<U>): Option<U> {
|
|
||||||
return isSome(this.inner) ? f(this.inner.value) : Option.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this `Option` is `None`. If it is, return the passed option.
|
|
||||||
*
|
|
||||||
* @param optb The `Option` to return if this `Option` is `None`
|
|
||||||
*/
|
|
||||||
public or(optb: Option<T>): Option<T> {
|
|
||||||
return this.isNone() ? optb : this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this `Option` is `None`. If it is, return the passed function.
|
|
||||||
*
|
|
||||||
* Otherwise, return this `Option`
|
|
||||||
*
|
|
||||||
* @param f A function to return a different `Option`
|
|
||||||
*/
|
|
||||||
public orElse(f: () => Option<T>): Option<T> {
|
|
||||||
return this.isNone() ? f() : this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a string representation of the `Option`,
|
|
||||||
* mostly for debugging
|
|
||||||
*/
|
|
||||||
public toString(): string {
|
|
||||||
const innerValue = (isSome(this.inner))
|
|
||||||
? JSON.stringify(this.inner.value)
|
|
||||||
: '';
|
|
||||||
const prefix = this.inner.type.valueOf();
|
|
||||||
|
|
||||||
return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { Some, None } = Option;
|
|
||||||
export default Option;
|
|
@ -4,23 +4,14 @@
|
|||||||
export class Position {
|
export class Position {
|
||||||
private constructor(public x: number = 0, public y: number = 0) {}
|
private constructor(public x: number = 0, public y: number = 0) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new `Position` at the specified location
|
|
||||||
*/
|
|
||||||
public static at(x: number, y: number): Position {
|
public static at(x: number, y: number): Position {
|
||||||
return new Position(x, y);
|
return new Position(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new `Position` from an existing one
|
|
||||||
*/
|
|
||||||
public static from(p: Position): Position {
|
public static from(p: Position): Position {
|
||||||
return new Position(p.x, p.y);
|
return new Position(p.x, p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new `Position` at the origin (0, 0)
|
|
||||||
*/
|
|
||||||
public static default(): Position {
|
public static default(): Position {
|
||||||
return new Position();
|
return new Position();
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,7 @@
|
|||||||
import Ansi from './ansi.ts';
|
|
||||||
|
|
||||||
import { SCROLL_TAB_SIZE } from './config.ts';
|
import { SCROLL_TAB_SIZE } from './config.ts';
|
||||||
import {
|
import { arrayInsert, isAsciiDigit, some, strChars } from './fns.ts';
|
||||||
arrayInsert,
|
import { highlightToColor, HighlightType } from './highlight.ts';
|
||||||
highlightToColor,
|
import Ansi from './ansi.ts';
|
||||||
isAsciiDigit,
|
|
||||||
isSeparator,
|
|
||||||
strChars,
|
|
||||||
strlen,
|
|
||||||
substr,
|
|
||||||
} from './fns.ts';
|
|
||||||
import { FileType } from './filetype.ts';
|
|
||||||
import Option, { None, Some } from './option.ts';
|
|
||||||
import { HighlightType, SearchDirection } from './types.ts';
|
|
||||||
|
|
||||||
const SINGLE_QUOTE = "'";
|
|
||||||
const DOUBLE_QUOTE = '"';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One row of text in the current document. In order to handle
|
* One row of text in the current document. In order to handle
|
||||||
@ -39,47 +25,27 @@ export class Row {
|
|||||||
*/
|
*/
|
||||||
public hl: HighlightType[] = [];
|
public hl: HighlightType[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Has the current row been highlighted?
|
|
||||||
*/
|
|
||||||
public isHighlighted: boolean = false;
|
|
||||||
|
|
||||||
private constructor(s: string | string[] = '') {
|
private constructor(s: string | string[] = '') {
|
||||||
this.chars = Array.isArray(s) ? s : strChars(s);
|
this.chars = Array.isArray(s) ? s : strChars(s);
|
||||||
this.rchars = [];
|
this.rchars = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of 'characters' in this row
|
|
||||||
*/
|
|
||||||
public get size(): number {
|
public get size(): number {
|
||||||
return this.chars.length;
|
return this.chars.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of 'characters' in the 'render' array
|
|
||||||
*/
|
|
||||||
public get rsize(): number {
|
public get rsize(): number {
|
||||||
return this.rchars.length;
|
return this.rchars.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the 'render' string
|
|
||||||
*/
|
|
||||||
public rstring(offset: number = 0): string {
|
public rstring(offset: number = 0): string {
|
||||||
return this.rchars.slice(offset).join('');
|
return this.rchars.slice(offset).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new empty Row
|
|
||||||
*/
|
|
||||||
public static default(): Row {
|
public static default(): Row {
|
||||||
return new Row();
|
return new Row();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Row
|
|
||||||
*/
|
|
||||||
public static from(s: string | string[] | Row): Row {
|
public static from(s: string | string[] | Row): Row {
|
||||||
if (s instanceof Row) {
|
if (s instanceof Row) {
|
||||||
return s;
|
return s;
|
||||||
@ -88,17 +54,11 @@ export class Row {
|
|||||||
return new Row(s);
|
return new Row(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public append(s: string): void {
|
||||||
* Add a character to the end of the current row
|
|
||||||
*/
|
|
||||||
public append(s: string, syntax: FileType): void {
|
|
||||||
this.chars = this.chars.concat(strChars(s));
|
this.chars = this.chars.concat(strChars(s));
|
||||||
this.update(None, syntax);
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a character to the current row at the specified location
|
|
||||||
*/
|
|
||||||
public insertChar(at: number, c: string): void {
|
public insertChar(at: number, c: string): void {
|
||||||
const newSlice = strChars(c);
|
const newSlice = strChars(c);
|
||||||
if (at >= this.size) {
|
if (at >= this.size) {
|
||||||
@ -111,10 +71,10 @@ export class Row {
|
|||||||
/**
|
/**
|
||||||
* Truncate the current row, and return a new one at the specified index
|
* Truncate the current row, and return a new one at the specified index
|
||||||
*/
|
*/
|
||||||
public split(at: number, syntax: FileType): 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(None, syntax);
|
this.update();
|
||||||
|
|
||||||
return newRow;
|
return newRow;
|
||||||
}
|
}
|
||||||
@ -132,48 +92,25 @@ export class Row {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the current row for the specified string, and return
|
* Search the current row for the specified string, and return
|
||||||
* the 'character' index of the start of that match
|
* the index of the start of that match
|
||||||
*/
|
*/
|
||||||
public find(
|
public find(s: string, offset: number = 0): number | null {
|
||||||
s: string,
|
const thisStr = this.toString();
|
||||||
at: number = 0,
|
if (!this.toString().includes(s)) {
|
||||||
direction: SearchDirection = SearchDirection.Forward,
|
return null;
|
||||||
): Option<number> {
|
|
||||||
if (at > this.size) {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
const thisStr = this.chars.join('');
|
|
||||||
|
|
||||||
// Look for the search query `s`, starting from the 'character' `offset`
|
const byteCount = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
||||||
const byteIndex = (direction === SearchDirection.Forward)
|
|
||||||
? thisStr.indexOf(s, this.charIndexToByteIndex(at))
|
|
||||||
: thisStr.lastIndexOf(s, this.charIndexToByteIndex(at));
|
|
||||||
|
|
||||||
// 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 Some(byteIndex);
|
return byteCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emoji/Extended Unicode-friendly search
|
// Emoji/Extended Unicode-friendly search
|
||||||
return Some(this.byteIndexToCharIndex(byteIndex));
|
return this.byteIndexToCharIndex(byteCount);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the current Row for the given string, returning the index in
|
|
||||||
* the 'render' version
|
|
||||||
*/
|
|
||||||
public rIndexOf(s: string, offset: number = 0): Option<number> {
|
|
||||||
const rstring = this.rchars.join('');
|
|
||||||
const byteIndex = rstring.indexOf(s, this.charIndexToByteIndex(offset));
|
|
||||||
|
|
||||||
return (byteIndex >= 0) ? Some(this.byteIndexToCharIndex(byteIndex)) : None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,447 +179,44 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Output the contents of the row
|
|
||||||
*/
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.chars.join('');
|
return this.chars.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public update(searchMatch?: string): void {
|
||||||
* Setup up the row by converting tabs to spaces for rendering,
|
|
||||||
* then setup syntax highlighting
|
|
||||||
*/
|
|
||||||
public update(
|
|
||||||
word: Option<string>,
|
|
||||||
syntax: FileType,
|
|
||||||
startWithComment: boolean = false,
|
|
||||||
): boolean {
|
|
||||||
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);
|
||||||
return this.highlight(word, syntax, startWithComment);
|
this.highlight(searchMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public highlight(searchMatch?: string): void {
|
||||||
* Calculate the syntax types of the current Row
|
const highlighting = [];
|
||||||
*/
|
|
||||||
public highlight(
|
if (some(searchMatch)) {
|
||||||
word: Option<string>,
|
// TODO: highlight search here
|
||||||
syntax: FileType,
|
|
||||||
startWithComment: boolean,
|
|
||||||
): boolean {
|
|
||||||
// When the highlighting is already up-to-date
|
|
||||||
if (this.isHighlighted && word.isNone()) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hl = [];
|
for (const ch of this.rchars) {
|
||||||
let i = 0;
|
if (isAsciiDigit(ch)) {
|
||||||
|
highlighting.push(HighlightType.Number);
|
||||||
// Handle the case where we are in a multi-line
|
|
||||||
// comment from a previous row
|
|
||||||
let inMlComment = startWithComment;
|
|
||||||
if (inMlComment && syntax.hasMultilineComments()) {
|
|
||||||
const maybeEnd = this.rIndexOf(syntax.multiLineCommentEnd.unwrap(), i);
|
|
||||||
const closingIndex = (maybeEnd.isSome())
|
|
||||||
? maybeEnd.unwrap() + 2
|
|
||||||
: this.rsize;
|
|
||||||
|
|
||||||
for (; i < closingIndex; i++) {
|
|
||||||
this.hl.push(HighlightType.MultiLineComment);
|
|
||||||
}
|
|
||||||
i = closingIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < this.rsize;) {
|
|
||||||
const maybeMultiline = this.highlightMultilineComment(i, syntax);
|
|
||||||
if (maybeMultiline.isSome()) {
|
|
||||||
inMlComment = true;
|
|
||||||
i = maybeMultiline.unwrap();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
inMlComment = false;
|
|
||||||
|
|
||||||
// Go through the syntax highlighting types in order:
|
|
||||||
// If there is a match, we end the chain of syntax types
|
|
||||||
// and 'consume' the number of characters that matched
|
|
||||||
const maybeNext = this.highlightComment(i, syntax)
|
|
||||||
.orElse(() => this.highlightPrimaryKeywords(i, syntax))
|
|
||||||
.orElse(() => this.highlightSecondaryKeywords(i, syntax))
|
|
||||||
.orElse(() => this.highlightString(i, syntax))
|
|
||||||
.orElse(() => this.highlightCharacter(i, syntax))
|
|
||||||
.orElse(() => this.highlightNumber(i, syntax))
|
|
||||||
.orElse(() => this.highlightOperators(i, syntax));
|
|
||||||
|
|
||||||
if (maybeNext.isNone()) {
|
|
||||||
this.hl.push(HighlightType.None);
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = maybeNext.unwrap();
|
|
||||||
if (next >= this.rsize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
i = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.highlightMatch(word);
|
|
||||||
if (inMlComment && syntax.hasMultilineComments()) {
|
|
||||||
if (
|
|
||||||
substr(this.toString(), this.size - 2) !==
|
|
||||||
syntax.multiLineCommentEnd.unwrap()
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.isHighlighted = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightMatch(word: Option<string>): void {
|
|
||||||
let searchIndex = 0;
|
|
||||||
|
|
||||||
// Find matches for the current search
|
|
||||||
if (word.isSome()) {
|
|
||||||
const query = word.unwrap();
|
|
||||||
while (true) {
|
|
||||||
const match = this.find(
|
|
||||||
query,
|
|
||||||
searchIndex,
|
|
||||||
SearchDirection.Forward,
|
|
||||||
);
|
|
||||||
if (match.isNone()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = match.unwrap();
|
|
||||||
const matchSize = strlen(query);
|
|
||||||
const nextPossible = index + matchSize;
|
|
||||||
if (nextPossible < this.rsize) {
|
|
||||||
let i = index;
|
|
||||||
for (const _ in strChars(word.unwrap())) {
|
|
||||||
this.hl[i] = HighlightType.Match;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchIndex = nextPossible;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
highlighting.push(HighlightType.None);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected highlightComment(
|
this.hl = highlighting;
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
// Highlight single-line comments
|
|
||||||
if (syntax.singleLineComment.isSome()) {
|
|
||||||
const commentStart = syntax.singleLineComment.unwrap();
|
|
||||||
const hasCommentStart = this.rIndexOf(commentStart).isSome();
|
|
||||||
if (
|
|
||||||
hasCommentStart && this.rIndexOf(commentStart).unwrap() === i
|
|
||||||
) {
|
|
||||||
for (; i < this.rsize; i++) {
|
|
||||||
this.hl.push(HighlightType.SingleLineComment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private highlightStr(
|
|
||||||
i: number,
|
|
||||||
substring: string,
|
|
||||||
hl_type: HighlightType,
|
|
||||||
): Option<number> {
|
|
||||||
if (strlen(substring) === 0) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
const substringChars = strChars(substring);
|
|
||||||
for (const [j, ch] of substringChars.entries()) {
|
|
||||||
const nextChar = this.rchars[i + j];
|
|
||||||
if (nextChar !== ch) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const _ of substringChars) {
|
|
||||||
this.hl.push(hl_type);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
private highlightKeywords(
|
|
||||||
i: number,
|
|
||||||
keywords: string[],
|
|
||||||
hl_type: HighlightType,
|
|
||||||
): Option<number> {
|
|
||||||
if (i > 0) {
|
|
||||||
const prevChar = this.rchars[i - 1];
|
|
||||||
if (!isSeparator(prevChar)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const keyword of keywords) {
|
|
||||||
// Skip keywords that can't fit in the current line
|
|
||||||
if (i + strlen(keyword) >= this.rsize) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the character after the keyword
|
|
||||||
// if it is not a 'separator' character,
|
|
||||||
// we must be highlighting the middle of something else
|
|
||||||
const nextChar = this.rchars[i + strlen(keyword)];
|
|
||||||
if (!isSeparator(nextChar)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybeHighlight = this.highlightStr(i, keyword, hl_type);
|
|
||||||
if (maybeHighlight.isSome()) {
|
|
||||||
return maybeHighlight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightPrimaryKeywords(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
return this.highlightKeywords(
|
|
||||||
i,
|
|
||||||
syntax.primaryKeywords,
|
|
||||||
HighlightType.Keyword1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightSecondaryKeywords(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
return this.highlightKeywords(
|
|
||||||
i,
|
|
||||||
syntax.secondaryKeywords,
|
|
||||||
HighlightType.Keyword2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightOperators(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
// Search the list of operators
|
|
||||||
outer: for (const op of syntax.operators) {
|
|
||||||
const chars = strChars(op);
|
|
||||||
|
|
||||||
// See if this operator (chars[j]) exists at this index
|
|
||||||
for (const [j, ch] of chars.entries()) {
|
|
||||||
// Make sure the next character of this operator matches too
|
|
||||||
const nextChar = this.rchars[i + j];
|
|
||||||
if (nextChar !== ch) {
|
|
||||||
continue outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This operator matches, highlight it
|
|
||||||
for (const _ of chars) {
|
|
||||||
this.hl.push(HighlightType.Operator);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightCharacter(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
if (!syntax.flags.characters) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight character literals
|
|
||||||
const ch = this.rchars[i];
|
|
||||||
if (ch === SINGLE_QUOTE && this.rIndexOf(SINGLE_QUOTE, i + 1).isSome()) {
|
|
||||||
while (true) {
|
|
||||||
this.hl.push(HighlightType.Character);
|
|
||||||
i += 1;
|
|
||||||
if (i === this.rsize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextChar = this.rchars[i];
|
|
||||||
// Make sure to continue highlighting if
|
|
||||||
// you have an escaped character delimeter
|
|
||||||
if (nextChar === '\\') {
|
|
||||||
this.hl.push(HighlightType.Character);
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (nextChar === ch) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.hl.push(HighlightType.Character);
|
|
||||||
i += 1;
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightString(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
if (!syntax.flags.strings) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight strings
|
|
||||||
const ch = this.rchars[i];
|
|
||||||
if (
|
|
||||||
ch === DOUBLE_QUOTE ||
|
|
||||||
((!syntax.flags.characters) && ch === SINGLE_QUOTE)
|
|
||||||
) {
|
|
||||||
while (true) {
|
|
||||||
this.hl.push(HighlightType.String);
|
|
||||||
i += 1;
|
|
||||||
if (i === this.rsize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextChar = this.rchars[i];
|
|
||||||
if (nextChar === ch) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.hl.push(HighlightType.String);
|
|
||||||
i += 1;
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightMultilineComment(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
if (!syntax.hasMultilineComments()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ch = this.rchars[i];
|
|
||||||
|
|
||||||
const startChars = syntax.multiLineCommentStart.unwrap();
|
|
||||||
const endChars = syntax.multiLineCommentEnd.unwrap();
|
|
||||||
if (ch === startChars[0] && this.rchars[i + 1] == startChars[1]) {
|
|
||||||
const maybeEnd = this.rIndexOf(endChars, i);
|
|
||||||
const end = (maybeEnd.isSome())
|
|
||||||
? maybeEnd.unwrap() + strlen(endChars) + 2
|
|
||||||
: this.rsize;
|
|
||||||
|
|
||||||
for (; i <= end; i++) {
|
|
||||||
this.hl.push(HighlightType.MultiLineComment);
|
|
||||||
}
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected highlightNumber(
|
|
||||||
i: number,
|
|
||||||
syntax: FileType,
|
|
||||||
): Option<number> {
|
|
||||||
// Exit early
|
|
||||||
const ch = this.rchars[i];
|
|
||||||
if (!(syntax.flags.numbers && isAsciiDigit(ch))) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure which characters are valid
|
|
||||||
// for numbers in the current FileType
|
|
||||||
let validChars = ['.'];
|
|
||||||
if (syntax.flags.binNumbers) {
|
|
||||||
validChars = validChars.concat(['b', 'B']);
|
|
||||||
}
|
|
||||||
if (syntax.flags.octalNumbers) {
|
|
||||||
validChars = validChars.concat(['o', 'O']);
|
|
||||||
}
|
|
||||||
if (syntax.flags.hexNumbers) {
|
|
||||||
// deno-fmt-ignore
|
|
||||||
validChars = validChars.concat([
|
|
||||||
'a','A',
|
|
||||||
'b','B',
|
|
||||||
'c','C',
|
|
||||||
'd','D',
|
|
||||||
'e','E',
|
|
||||||
'f','F',
|
|
||||||
'x','X',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (syntax.flags.jsBigInt) {
|
|
||||||
validChars.push('n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number literals are not attached to other syntax
|
|
||||||
if (i > 0 && !isSeparator(this.rchars[i - 1])) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match until the end of the number literal
|
|
||||||
while (true) {
|
|
||||||
this.hl.push(HighlightType.Number);
|
|
||||||
i += 1;
|
|
||||||
if (i >= this.rsize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextChar = this.rchars[i];
|
|
||||||
if (
|
|
||||||
!(validChars.includes(nextChar) || isAsciiDigit(nextChar))
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a terminal-formatted version of the current row
|
|
||||||
*/
|
|
||||||
public render(offset: number, len: number): string {
|
public render(offset: number, len: number): string {
|
||||||
const end = Math.min(len, this.rsize);
|
const end = Math.min(len, this.rsize);
|
||||||
const start = Math.min(offset, len);
|
const start = Math.min(offset, len);
|
||||||
|
@ -1,11 +1,141 @@
|
|||||||
export * from './runtime/file_io.ts';
|
import process from 'node:process';
|
||||||
export * from './runtime/helpers.ts';
|
import { IRuntime, ITestBase } from './types.ts';
|
||||||
export * from './runtime/log.ts';
|
import { noop } from './fns.ts';
|
||||||
export * from './runtime/node.ts';
|
import { SCROLL_ERR_FILE, SCROLL_LOG_FILE } from './config.ts';
|
||||||
export * from './runtime/runtime.ts';
|
|
||||||
export * from './runtime/terminal_io.ts';
|
|
||||||
export * from './runtime/test_base.ts';
|
|
||||||
export { RunTimeType } from './types.ts';
|
|
||||||
|
|
||||||
import { CommonRuntime } from './runtime/runtime.ts';
|
export type { IFileIO, IRuntime, ITerminal } from './types.ts';
|
||||||
export default CommonRuntime;
|
|
||||||
|
/**
|
||||||
|
* Which Typescript runtime is currently being used
|
||||||
|
*/
|
||||||
|
export enum RunTimeType {
|
||||||
|
Bun = 'bun',
|
||||||
|
Deno = 'deno',
|
||||||
|
Unknown = 'common',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
Debug = 'Debug',
|
||||||
|
Info = 'Info',
|
||||||
|
Notice = 'Notice',
|
||||||
|
Warning = 'Warning',
|
||||||
|
Error = 'Error',
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollRuntime: IRuntime | null = null;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Misc runtime functions
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function log(s: unknown, level: LogLevel = LogLevel.Notice): void {
|
||||||
|
getRuntime().then(({ file }) => {
|
||||||
|
const raw = JSON.stringify(s, null, 2);
|
||||||
|
const output = `${level}: ${raw}\n`;
|
||||||
|
|
||||||
|
const outputFile = (level === LogLevel.Error)
|
||||||
|
? SCROLL_ERR_FILE
|
||||||
|
: SCROLL_LOG_FILE;
|
||||||
|
file.appendFile(outputFile, output).then(noop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append information to the scroll.err logfile
|
||||||
|
*/
|
||||||
|
export function logError(s: unknown): void {
|
||||||
|
log(s, LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill program, displaying an error message
|
||||||
|
* @param s
|
||||||
|
*/
|
||||||
|
export function die(s: string | Error): void {
|
||||||
|
logError(s);
|
||||||
|
process.stdin.setRawMode(false);
|
||||||
|
console.error(s);
|
||||||
|
getRuntime().then((r) => r.exit());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which Typescript runtime we are operating under
|
||||||
|
*/
|
||||||
|
export function runtimeType(): RunTimeType {
|
||||||
|
let runtime = RunTimeType.Unknown;
|
||||||
|
|
||||||
|
if ('Deno' in globalThis) {
|
||||||
|
runtime = RunTimeType.Deno;
|
||||||
|
}
|
||||||
|
if ('Bun' in globalThis) {
|
||||||
|
runtime = RunTimeType.Bun;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the adapter object for the current Runtime
|
||||||
|
*/
|
||||||
|
export async function getRuntime(): Promise<IRuntime> {
|
||||||
|
if (scrollRuntime === null) {
|
||||||
|
const runtime = runtimeType();
|
||||||
|
const path = `../${runtime}/mod.ts`;
|
||||||
|
|
||||||
|
const pkg = await import(path);
|
||||||
|
if ('default' in pkg) {
|
||||||
|
scrollRuntime = pkg.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollRuntime !== null) {
|
||||||
|
return Promise.resolve(scrollRuntime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('Missing default import');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(scrollRuntime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the common test interface object
|
||||||
|
*/
|
||||||
|
export async function getTestRunner(): Promise<ITestBase> {
|
||||||
|
const runtime = runtimeType();
|
||||||
|
const path = `../${runtime}/test_base.ts`;
|
||||||
|
const pkg = await import(path);
|
||||||
|
if ('default' in pkg) {
|
||||||
|
return pkg.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a runtime-specific module
|
||||||
|
*
|
||||||
|
* e.g. to load "src/bun/mod.ts", if the runtime is bun,
|
||||||
|
* you can use like so `await importForRuntime('index')`;
|
||||||
|
*
|
||||||
|
* @param path - the path within the runtime module
|
||||||
|
*/
|
||||||
|
export const importForRuntime = async (path: string) => {
|
||||||
|
const runtime = runtimeType();
|
||||||
|
const suffix = '.ts';
|
||||||
|
const base = `../${runtime}/`;
|
||||||
|
|
||||||
|
const pathParts = path
|
||||||
|
.split('/')
|
||||||
|
.filter((part) => part !== '' && part !== '.' && part !== suffix)
|
||||||
|
.map((part) => part.replace(suffix, ''));
|
||||||
|
|
||||||
|
const cleanedPath = pathParts.join('/');
|
||||||
|
const importPath = base + cleanedPath + suffix;
|
||||||
|
|
||||||
|
const pkg = await import(importPath);
|
||||||
|
if ('default' in pkg) {
|
||||||
|
return pkg.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg;
|
||||||
|
};
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { appendFile, readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
|
|
||||||
import { IFileIO } from './runtime.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File IO implementation using shared node APIs
|
|
||||||
*/
|
|
||||||
export const CommonFileIO: IFileIO = {
|
|
||||||
openFile: async function (path: string): Promise<string> {
|
|
||||||
const filePath = resolve(path);
|
|
||||||
const contents = await readFile(filePath, { encoding: 'utf8' });
|
|
||||||
|
|
||||||
return contents;
|
|
||||||
},
|
|
||||||
appendFile: async function (path: string, contents: string): Promise<void> {
|
|
||||||
const filePath = resolve(path);
|
|
||||||
|
|
||||||
await appendFile(filePath, contents);
|
|
||||||
},
|
|
||||||
saveFile: async function (path: string, contents: string): Promise<void> {
|
|
||||||
const filePath = resolve(path);
|
|
||||||
|
|
||||||
await writeFile(filePath, contents);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommonFileIO;
|
|
@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Functions/Methods that depend on the current runtime to function
|
|
||||||
*/
|
|
||||||
import { logError, logWarning } from './log.ts';
|
|
||||||
import { node_path as path, node_process as process } from './node.ts';
|
|
||||||
import { IRuntime } from './runtime.ts';
|
|
||||||
import { ITestBase } from './test_base.ts';
|
|
||||||
import { RunTimeType } from '../types.ts';
|
|
||||||
|
|
||||||
let scrollRuntime: IRuntime | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kill program, displaying an error message
|
|
||||||
* @param s
|
|
||||||
*/
|
|
||||||
export function die(s: string | Error): void {
|
|
||||||
logError(s);
|
|
||||||
process.stdin.setRawMode(false);
|
|
||||||
console.error(s);
|
|
||||||
getRuntime().then((r) => r.exit());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine which Typescript runtime we are operating under
|
|
||||||
*/
|
|
||||||
export function runtimeType(): RunTimeType {
|
|
||||||
const cmd = path.basename(process.argv[0]);
|
|
||||||
switch (cmd) {
|
|
||||||
case 'deno':
|
|
||||||
return RunTimeType.Deno;
|
|
||||||
|
|
||||||
case 'bun':
|
|
||||||
return RunTimeType.Bun;
|
|
||||||
|
|
||||||
case 'node':
|
|
||||||
return RunTimeType.Tsx;
|
|
||||||
|
|
||||||
default:
|
|
||||||
logWarning('Fallback runtime detection', { cmd });
|
|
||||||
if ('bun' in globalThis) {
|
|
||||||
return RunTimeType.Bun;
|
|
||||||
}
|
|
||||||
return RunTimeType.Tsx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the adapter object for the current Runtime
|
|
||||||
*/
|
|
||||||
export async function getRuntime(): Promise<IRuntime> {
|
|
||||||
if (scrollRuntime === null) {
|
|
||||||
const runtime = runtimeType();
|
|
||||||
const path = `../../${runtime}/mod.ts`;
|
|
||||||
|
|
||||||
const pkg = await import(path);
|
|
||||||
if ('default' in pkg) {
|
|
||||||
scrollRuntime = pkg.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollRuntime !== null) {
|
|
||||||
return Promise.resolve(scrollRuntime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject('Missing default import');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(scrollRuntime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the common test interface object
|
|
||||||
*/
|
|
||||||
export async function getTestRunner(): Promise<ITestBase> {
|
|
||||||
const runtime = runtimeType();
|
|
||||||
const path = `../../${runtime}/test_base.ts`;
|
|
||||||
const pkg = await import(path);
|
|
||||||
if ('default' in pkg) {
|
|
||||||
return pkg.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkg;
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
import { noop } from '../fns.ts';
|
|
||||||
import { SCROLL_LOG_FILE_PREFIX, SCROLL_LOG_FILE_SUFFIX } from '../config.ts';
|
|
||||||
import { getRuntime } from './helpers.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The label for type/severity of the log entry
|
|
||||||
*/
|
|
||||||
export enum LogLevel {
|
|
||||||
Debug = 'Debug',
|
|
||||||
Info = 'Info',
|
|
||||||
Notice = 'Notice',
|
|
||||||
Warning = 'Warning',
|
|
||||||
Error = 'Error',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic logging
|
|
||||||
*
|
|
||||||
* @param s The string or data to display first
|
|
||||||
* @param level The log severity
|
|
||||||
* @param data Any additional data to display with the log entry
|
|
||||||
*/
|
|
||||||
export function log(
|
|
||||||
s: unknown,
|
|
||||||
level: LogLevel = LogLevel.Notice,
|
|
||||||
data?: any,
|
|
||||||
): void {
|
|
||||||
getRuntime().then(({ file }) => {
|
|
||||||
const rawS = JSON.stringify(s, null, 2);
|
|
||||||
const rawData = JSON.stringify(data, null, 2);
|
|
||||||
const output = (typeof data !== 'undefined')
|
|
||||||
? `${rawS}\n${rawData}\n\n`
|
|
||||||
: `${rawS}\n`;
|
|
||||||
|
|
||||||
const outputFile =
|
|
||||||
`${SCROLL_LOG_FILE_PREFIX}-${level.toLowerCase()}${SCROLL_LOG_FILE_SUFFIX}`;
|
|
||||||
file.appendFile(outputFile, output).then(noop);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a log entry of `LogLevel.Debug` severity
|
|
||||||
*/
|
|
||||||
export const logDebug = (s: unknown, data?: any) =>
|
|
||||||
log(s, LogLevel.Debug, data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a log entry of `LogLevel.Info` severity
|
|
||||||
*/
|
|
||||||
export const logInfo = (s: unknown, data?: any) => log(s, LogLevel.Info, data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a log entry of `LogLevel.Notice` severity
|
|
||||||
*/
|
|
||||||
export const logNotice = (s: unknown, data?: any) =>
|
|
||||||
log(s, LogLevel.Notice, data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a log entry of `LogLevel.Warning` severity
|
|
||||||
*/
|
|
||||||
export const logWarning = (s: unknown, data?: any) =>
|
|
||||||
log(s, LogLevel.Warning, data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a log entry of `LogLevel.Error` severity
|
|
||||||
*/
|
|
||||||
export const logError = (s: unknown, data?: any) =>
|
|
||||||
log(s, LogLevel.Warning, data);
|
|
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Re-export of node apis shared by runtimes
|
|
||||||
*/
|
|
||||||
import node_assert from 'node:assert';
|
|
||||||
import node_path from 'node:path';
|
|
||||||
import node_process from 'node:process';
|
|
||||||
import node_tty from 'node:tty';
|
|
||||||
|
|
||||||
export { node_assert, node_path, node_process, node_tty };
|
|
@ -1,139 +0,0 @@
|
|||||||
import { node_process as process } from './node.ts';
|
|
||||||
import CommonFileIO from './file_io.ts';
|
|
||||||
import CommonTerminalIO from './terminal_io.ts';
|
|
||||||
import { RunTimeType } from '../types.ts';
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Runtime adapter interfaces
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size of terminal in rows and columns
|
|
||||||
*/
|
|
||||||
export interface ITerminalSize {
|
|
||||||
rows: number;
|
|
||||||
cols: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime-specific terminal functionality
|
|
||||||
*/
|
|
||||||
export interface ITerminal {
|
|
||||||
/**
|
|
||||||
* The arguments passed to the program on launch
|
|
||||||
*/
|
|
||||||
argv: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The generator function returning chunks of input from the stdin stream
|
|
||||||
*/
|
|
||||||
inputLoop(): AsyncGenerator<Uint8Array, null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the size of the terminal
|
|
||||||
*/
|
|
||||||
getTerminalSize(): Promise<ITerminalSize>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current chunk of input, if it exists
|
|
||||||
*/
|
|
||||||
readStdin(): Promise<string | null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the raw chunk of input
|
|
||||||
*/
|
|
||||||
readStdinRaw(): Promise<Uint8Array | null>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipe a string to stdout
|
|
||||||
*/
|
|
||||||
writeStdout(s: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime-specific file system io
|
|
||||||
*/
|
|
||||||
export interface IFileIO {
|
|
||||||
/**
|
|
||||||
* Open an entire file
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
*/
|
|
||||||
openFile(path: string): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append to a file, or create it if it doesn't exist
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* @param contents
|
|
||||||
*/
|
|
||||||
appendFile(path: string, contents: string): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a string into a file
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* @param contents
|
|
||||||
*/
|
|
||||||
saveFile(path: string, contents: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The common interface for runtime adapters
|
|
||||||
*/
|
|
||||||
export interface IRuntime {
|
|
||||||
/**
|
|
||||||
* The name of the runtime
|
|
||||||
*/
|
|
||||||
name: RunTimeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime-specific terminal functionality
|
|
||||||
*/
|
|
||||||
term: ITerminal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime-specific file system io
|
|
||||||
*/
|
|
||||||
file: IFileIO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up an event handler
|
|
||||||
*
|
|
||||||
* @param eventName - The event to listen for
|
|
||||||
* @param handler - The event handler
|
|
||||||
*/
|
|
||||||
onEvent: (
|
|
||||||
eventName: string,
|
|
||||||
handler: (e: Event | ErrorEvent) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a beforeExit/beforeUnload event handler for the runtime
|
|
||||||
* @param cb - The event handler
|
|
||||||
*/
|
|
||||||
onExit(cb: () => void): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop execution
|
|
||||||
*
|
|
||||||
* @param code
|
|
||||||
*/
|
|
||||||
exit(code?: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base runtime using shared Node APIs
|
|
||||||
*/
|
|
||||||
export const CommonRuntime: IRuntime = {
|
|
||||||
name: RunTimeType.Unknown,
|
|
||||||
term: CommonTerminalIO,
|
|
||||||
file: CommonFileIO,
|
|
||||||
onEvent: (eventName: string, handler) => process.on(eventName, handler),
|
|
||||||
onExit: (cb: () => void): void => {
|
|
||||||
process.on('beforeExit', cb);
|
|
||||||
process.on('exit', cb);
|
|
||||||
process.on('SIGINT', cb);
|
|
||||||
},
|
|
||||||
exit: (code?: number) => process.exit(code),
|
|
||||||
};
|
|
@ -1,44 +0,0 @@
|
|||||||
import { node_process as process } from './node.ts';
|
|
||||||
import { readKey } from '../fns.ts';
|
|
||||||
import { ITerminal, ITerminalSize } from '../types.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Terminal IO using shared Node APIs
|
|
||||||
*/
|
|
||||||
export const CommonTerminalIO: ITerminal = {
|
|
||||||
argv: (process.argv.length > 2) ? process.argv.slice(2) : [],
|
|
||||||
inputLoop: async function* (): AsyncGenerator<Uint8Array, null> {
|
|
||||||
yield (await CommonTerminalIO.readStdinRaw()) ?? new Uint8Array(0);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
getTerminalSize: function getTerminalSize(): Promise<ITerminalSize> {
|
|
||||||
const [cols, rows] = process.stdout.getWindowSize();
|
|
||||||
|
|
||||||
return Promise.resolve({
|
|
||||||
rows,
|
|
||||||
cols,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readStdin: async function (): Promise<string | null> {
|
|
||||||
const raw = await CommonTerminalIO.readStdinRaw();
|
|
||||||
return readKey(raw ?? new Uint8Array(0));
|
|
||||||
},
|
|
||||||
readStdinRaw: function (): Promise<Uint8Array | null> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
process.stdin.resume().once(
|
|
||||||
'data',
|
|
||||||
(buffer: Uint8Array) => {
|
|
||||||
process.stdin.pause();
|
|
||||||
resolve(buffer);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
writeStdout: function (s: string): Promise<void> {
|
|
||||||
process.stdout.write(s);
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommonTerminalIO;
|
|
@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* Adapt the node test interface to the shared testing interface
|
|
||||||
*/
|
|
||||||
import { deepStrictEqual, notStrictEqual, strictEqual } from 'node:assert';
|
|
||||||
import Option from '../option.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The shared interface for tests, running on a different test
|
|
||||||
* runner for each runtime
|
|
||||||
*/
|
|
||||||
export interface ITestBase {
|
|
||||||
assertEquivalent(actual: unknown, expected: unknown): void;
|
|
||||||
assertExists(actual: unknown): void;
|
|
||||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
|
||||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
|
||||||
assertEquals(actual: unknown, expected: unknown): void;
|
|
||||||
assertTrue(actual: boolean): void;
|
|
||||||
assertFalse(actual: boolean): void;
|
|
||||||
assertSome<T>(actual: Option<T>): void;
|
|
||||||
assertNone<T>(actual: Option<T>): void;
|
|
||||||
/**
|
|
||||||
* Convert the nested test object into a test suite for the current runtime
|
|
||||||
*/
|
|
||||||
testSuite(testObj: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base testing implementation using Node assert API
|
|
||||||
*/
|
|
||||||
export abstract class AbstractTestBase implements Partial<ITestBase> {
|
|
||||||
/**
|
|
||||||
* The values (often objects) have all the same property values
|
|
||||||
*/
|
|
||||||
public static assertEquivalent(actual: unknown, expected: unknown): void {
|
|
||||||
return deepStrictEqual(actual, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value is not null or undefined
|
|
||||||
*/
|
|
||||||
public static assertExists(actual: unknown): void {
|
|
||||||
return notStrictEqual(actual, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `actual` is an object implementing `expectedType`
|
|
||||||
*/
|
|
||||||
public static assertInstanceOf(actual: unknown, expectedType: any): void {
|
|
||||||
return strictEqual(actual instanceof expectedType, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The values are not exactly equal (Different instance, type, value, etc)
|
|
||||||
*/
|
|
||||||
public static assertNotEquals(actual: unknown, expected: unknown): void {
|
|
||||||
return notStrictEqual(actual, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The values are exactly the same
|
|
||||||
*/
|
|
||||||
public static assertEquals(actual: unknown, expected: unknown): void {
|
|
||||||
return strictEqual(actual, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value is true
|
|
||||||
*/
|
|
||||||
public static assertTrue(actual: boolean): void {
|
|
||||||
return strictEqual(actual, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value is false
|
|
||||||
*/
|
|
||||||
public static assertFalse(actual: boolean): void {
|
|
||||||
return strictEqual(actual, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value is a `Some` type `Option`
|
|
||||||
*/
|
|
||||||
public static assertSome<T>(actual: Option<T>): void {
|
|
||||||
return AbstractTestBase.assertTrue(actual.isSome());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value is a `None` type `Option`
|
|
||||||
*/
|
|
||||||
public static assertNone<T>(actual: Option<T>): void {
|
|
||||||
return AbstractTestBase.assertTrue(actual.isNone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AbstractTestBase;
|
|
76
src/common/search.ts
Normal file
76
src/common/search.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Position } from './types.ts';
|
||||||
|
import { KeyCommand } from './ansi.ts';
|
||||||
|
import Document from './document.ts';
|
||||||
|
|
||||||
|
enum SearchDirection {
|
||||||
|
Forward = 1,
|
||||||
|
Backward = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Search {
|
||||||
|
private lastMatch: number = -1;
|
||||||
|
private current: number = -1;
|
||||||
|
private direction: SearchDirection = SearchDirection.Forward;
|
||||||
|
public parent: Document | null = null;
|
||||||
|
|
||||||
|
private parseInput(key: string) {
|
||||||
|
switch (key) {
|
||||||
|
case KeyCommand.ArrowRight:
|
||||||
|
case KeyCommand.ArrowDown:
|
||||||
|
this.direction = SearchDirection.Forward;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyCommand.ArrowLeft:
|
||||||
|
case KeyCommand.ArrowUp:
|
||||||
|
this.direction = SearchDirection.Backward;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.lastMatch = -1;
|
||||||
|
this.direction = SearchDirection.Forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastMatch === -1) {
|
||||||
|
this.direction = SearchDirection.Forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = this.lastMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNextRow(rowCount: number): number {
|
||||||
|
this.current += this.direction;
|
||||||
|
if (this.current === -1) {
|
||||||
|
this.current = rowCount - 1;
|
||||||
|
} else if (this.current === rowCount) {
|
||||||
|
this.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public search(q: string, key: string): Position | null {
|
||||||
|
if (this.parent === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parseInput(key);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (; i < this.parent.numRows; i++) {
|
||||||
|
const current = this.getNextRow(this.parent.numRows);
|
||||||
|
const row = this.parent.row(current);
|
||||||
|
|
||||||
|
if (row === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const possible = row.find(q);
|
||||||
|
if (possible !== null) {
|
||||||
|
this.lastMatch = current;
|
||||||
|
return Position.at(possible, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +1,122 @@
|
|||||||
|
import { RunTimeType } from './runtime.ts';
|
||||||
|
|
||||||
export { Position } from './position.ts';
|
export { Position } from './position.ts';
|
||||||
export type { ITestBase } from './runtime/test_base.ts';
|
|
||||||
export type { IFileIO, IRuntime, ITerminal, ITerminalSize } from './runtime.ts';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which Typescript runtime is currently being used
|
* The size of terminal in rows and columns
|
||||||
*/
|
*/
|
||||||
export enum RunTimeType {
|
export interface ITerminalSize {
|
||||||
Bun = 'bun',
|
rows: number;
|
||||||
Deno = 'deno',
|
cols: number;
|
||||||
Tsx = 'tsx',
|
}
|
||||||
Unknown = 'unknown',
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Runtime adapter interfaces
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The common interface for runtime adapters
|
||||||
|
*/
|
||||||
|
export interface IRuntime {
|
||||||
|
/**
|
||||||
|
* The name of the runtime
|
||||||
|
*/
|
||||||
|
name: RunTimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-specific terminal functionality
|
||||||
|
*/
|
||||||
|
term: {
|
||||||
|
/**
|
||||||
|
* The arguments passed to the program on launch
|
||||||
|
*/
|
||||||
|
argv: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generator function returning chunks of input from the stdin stream
|
||||||
|
*/
|
||||||
|
inputLoop(): AsyncGenerator<Uint8Array, null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the terminal
|
||||||
|
*/
|
||||||
|
getTerminalSize(): Promise<ITerminalSize>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current chunk of input, if it exists
|
||||||
|
*/
|
||||||
|
readStdin(): Promise<string | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw chunk of input
|
||||||
|
*/
|
||||||
|
readStdinRaw(): Promise<Uint8Array | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe a string to stdout
|
||||||
|
*/
|
||||||
|
writeStdout(s: string): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-specific file system io
|
||||||
|
*/
|
||||||
|
file: {
|
||||||
|
openFile(path: string): Promise<string>;
|
||||||
|
appendFile(path: string, contents: string): Promise<void>;
|
||||||
|
saveFile(path: string, contents: string): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up an event handler
|
||||||
|
*
|
||||||
|
* @param eventName - The event to listen for
|
||||||
|
* @param handler - The event handler
|
||||||
|
*/
|
||||||
|
onEvent: (
|
||||||
|
eventName: string,
|
||||||
|
handler: (e: Event | ErrorEvent) => void,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a beforeExit/beforeUnload event handler for the runtime
|
||||||
|
* @param cb - The event handler
|
||||||
|
*/
|
||||||
|
onExit(cb: () => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop execution
|
||||||
|
*
|
||||||
|
* @param code
|
||||||
|
*/
|
||||||
|
exit(code?: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of Syntax being highlighted
|
* Runtime-specific terminal functionality
|
||||||
*/
|
*/
|
||||||
export enum HighlightType {
|
export type ITerminal = IRuntime['term'];
|
||||||
/** No highlighting */
|
|
||||||
None,
|
|
||||||
/** Number literals */
|
|
||||||
Number,
|
|
||||||
/** Search results */
|
|
||||||
Match,
|
|
||||||
/** Character literals */
|
|
||||||
Character,
|
|
||||||
/** String literals */
|
|
||||||
String,
|
|
||||||
/** Single line comments */
|
|
||||||
SingleLineComment,
|
|
||||||
/** Multi-line comments */
|
|
||||||
MultiLineComment,
|
|
||||||
/** Primary keywords */
|
|
||||||
Keyword1,
|
|
||||||
/** Secondary keywords */
|
|
||||||
Keyword2,
|
|
||||||
/** Math/logic operators */
|
|
||||||
Operator,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which direction to search in the current document
|
* Runtime-specific file handling
|
||||||
*/
|
*/
|
||||||
export enum SearchDirection {
|
export type IFileIO = IRuntime['file'];
|
||||||
Forward = 1,
|
|
||||||
Backward = -1,
|
// ----------------------------------------------------------------------------
|
||||||
|
// Testing
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shared test interface, so tests can be run by both runtimes
|
||||||
|
*/
|
||||||
|
export interface ITestBase {
|
||||||
|
assertEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertExists(actual: unknown): void;
|
||||||
|
assertFalse(actual: boolean): void;
|
||||||
|
assertInstanceOf(actual: unknown, expectedType: any): void;
|
||||||
|
assertNotEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertNull(actual: unknown): void;
|
||||||
|
assertStrictEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertTrue(actual: boolean): void;
|
||||||
|
testSuite(testObj: any): void;
|
||||||
}
|
}
|
||||||
|
1
src/deno/deps.ts
Normal file
1
src/deno/deps.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * as stdAssert from 'https://deno.land/std@0.208.0/assert/mod.ts';
|
@ -1,3 +1,7 @@
|
|||||||
|
if (!('Deno' in globalThis)) {
|
||||||
|
throw new Error('This module requires Deno to run');
|
||||||
|
}
|
||||||
|
|
||||||
import { IFileIO } from '../common/runtime.ts';
|
import { IFileIO } from '../common/runtime.ts';
|
||||||
|
|
||||||
const DenoFileIO: IFileIO = {
|
const DenoFileIO: IFileIO = {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
|
if (!('Deno' in globalThis)) {
|
||||||
|
throw new Error('This module requires Deno to run');
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* The main entrypoint when using Deno as the runtime
|
* The main entrypoint when using Deno as the runtime
|
||||||
*/
|
*/
|
||||||
import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime.ts';
|
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';
|
||||||
* The Deno Runtime implementation
|
|
||||||
*/
|
|
||||||
const DenoRuntime: IRuntime = {
|
const DenoRuntime: IRuntime = {
|
||||||
...CommonRuntime,
|
|
||||||
name: RunTimeType.Deno,
|
name: RunTimeType.Deno,
|
||||||
file: DenoFileIO,
|
file: DenoFileIO,
|
||||||
term: DenoTerminalIO,
|
term: DenoTerminalIO,
|
||||||
@ -19,6 +20,7 @@ const DenoRuntime: IRuntime = {
|
|||||||
globalThis.addEventListener('onbeforeunload', cb);
|
globalThis.addEventListener('onbeforeunload', cb);
|
||||||
globalThis.onbeforeunload = cb;
|
globalThis.onbeforeunload = cb;
|
||||||
},
|
},
|
||||||
|
exit: (code?: number) => node_process.exit(code),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DenoRuntime;
|
export default DenoRuntime;
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
if (!('Deno' in globalThis)) {
|
||||||
|
throw new Error('This module requires Deno to run');
|
||||||
|
}
|
||||||
import { readKey } from '../common/fns.ts';
|
import { readKey } from '../common/fns.ts';
|
||||||
import { ITerminal, ITerminalSize } from '../common/types.ts';
|
import { ITerminal, ITerminalSize } from '../common/types.ts';
|
||||||
|
|
||||||
|
@ -1,15 +1,48 @@
|
|||||||
// @ts-ignore The import exists, but tsc complains
|
if (!('Deno' in globalThis)) {
|
||||||
import { test } from 'node:test';
|
throw new Error('This module requires Deno to run');
|
||||||
import AbstractTestBase from '../common/runtime/test_base.ts';
|
}
|
||||||
class DenoTestBase extends AbstractTestBase {
|
import { ITestBase } from '../common/types.ts';
|
||||||
public static testSuite(testObj: any) {
|
import { stdAssert } from './deps.ts';
|
||||||
|
const {
|
||||||
|
assertEquals,
|
||||||
|
assertExists,
|
||||||
|
assertInstanceOf,
|
||||||
|
AssertionError,
|
||||||
|
assertNotEquals,
|
||||||
|
assertStrictEquals,
|
||||||
|
} = stdAssert;
|
||||||
|
|
||||||
|
export function testSuite(testObj: any) {
|
||||||
Object.keys(testObj).forEach((group) => {
|
Object.keys(testObj).forEach((group) => {
|
||||||
const groupObj = testObj[group];
|
const groupObj = testObj[group];
|
||||||
Object.keys(groupObj).forEach((testName) => {
|
Object.keys(groupObj).forEach((testName) => {
|
||||||
test(testName, groupObj[testName]);
|
Deno.test(testName, groupObj[testName]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DenoTestBase: ITestBase = {
|
||||||
|
assertEquals,
|
||||||
|
assertExists,
|
||||||
|
assertInstanceOf,
|
||||||
|
assertNotEquals,
|
||||||
|
assertStrictEquals,
|
||||||
|
assertTrue: function (actual: boolean): void {
|
||||||
|
if (actual !== true) {
|
||||||
|
throw new AssertionError(`actual: "${actual}" expected to be true"`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertFalse(actual: boolean): void {
|
||||||
|
if (actual !== 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,
|
||||||
|
};
|
||||||
|
|
||||||
export default DenoTestBase;
|
export default DenoTestBase;
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* The main entrypoint when using Tsx as the runtime
|
|
||||||
*/
|
|
||||||
import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime.ts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Tsx Runtime implementation
|
|
||||||
*/
|
|
||||||
const TsxRuntime: IRuntime = {
|
|
||||||
...CommonRuntime,
|
|
||||||
name: RunTimeType.Tsx,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TsxRuntime;
|
|
@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Adapt the node test interface to the shared testing interface
|
|
||||||
*/
|
|
||||||
// @ts-ignore The import exists, but tsc complains
|
|
||||||
import { describe, it } from 'node:test';
|
|
||||||
import AbstractTestBase from '../common/runtime/test_base.ts';
|
|
||||||
|
|
||||||
class TsxTestBase extends AbstractTestBase {
|
|
||||||
public static testSuite(testObj: any): void {
|
|
||||||
Object.keys(testObj).forEach((group) => {
|
|
||||||
describe(group, () => {
|
|
||||||
const groupObj = testObj[group];
|
|
||||||
Object.keys(groupObj).forEach((testName) => {
|
|
||||||
it(testName, groupObj[testName]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TsxTestBase;
|
|
@ -10,14 +10,10 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"esModuleInterop": true,
|
"allowSyntheticDefaultImports": true
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"strictNullChecks": true
|
|
||||||
},
|
},
|
||||||
"exclude": ["src/deno"]
|
"exclude": ["src/deno"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user