Compare commits
50 Commits
linear-inp
...
master
Author | SHA1 | Date | |
---|---|---|---|
db3e5dd685 | |||
b01212b76e | |||
269322e5b2 | |||
57e96db4ff | |||
ec3d9c3179 | |||
7aa71d4dae | |||
8b44e250e2 | |||
501f5e10d5 | |||
46a0314ce6 | |||
d59900a895 | |||
a7fcc982fe | |||
c31933ed9b | |||
58490f1c51 | |||
d90d685ef2 | |||
6101132a15 | |||
21d26ede6c | |||
5b40d16999 | |||
0c13942aae | |||
ea00f76a62 | |||
2b3be61933 | |||
1a8d9f5469 | |||
65ff7e5b79 | |||
6d9d1113f2 | |||
e84dfa9ba9 | |||
359e739fe8 | |||
32e3676b02 | |||
b5856f063a | |||
01b8535c5e | |||
8c54ceb104 | |||
1951425508 | |||
21ff71cc94 | |||
2c21bf0c9b | |||
b2169cf54b | |||
d7bf8801c9 | |||
4be7be09a7 | |||
4436a8a783 | |||
b3bddbb601 | |||
e0e7849fe4 | |||
88bf3da4e7 | |||
0148561240 | |||
8d2ba868b0 | |||
e656ad3112 | |||
1cfdeece60 | |||
090d6262c3 | |||
4313b923bf | |||
1b3e9d9796 | |||
76eacd835f | |||
cf80dce335 | |||
82bcc72d21 | |||
32f9ef3bba |
7
.gitignore
vendored
7
.gitignore
vendored
@ -335,8 +335,13 @@ $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.err
|
scroll*.log
|
||||||
|
docs
|
||||||
deno.lock
|
deno.lock
|
||||||
cov_profile/
|
cov_profile/
|
||||||
coverage/
|
coverage/
|
||||||
|
20
README.md
20
README.md
@ -1,16 +1,30 @@
|
|||||||
# 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 [Bun](https://bun.sh/) (v1.0 or later) and [Deno](https://deno.com/)
|
runs on
|
||||||
(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` and `src/bun` folders
|
- Runtime implementations are in the `src/deno`, `src/bun`, `src/tsx` folders
|
||||||
- The main implementation is in `src/common`
|
- The main implementation is in `src/common`
|
||||||
|
5
bin/bun.sh
Executable file
5
bin/bun.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
PARENT_DIR="$(dirname "$(realpath "$0")")"
|
||||||
|
SCROLL="$(realpath "${PARENT_DIR}/../src/scroll.ts")"
|
||||||
|
bun run "${SCROLL}" "$@"
|
5
bin/deno.sh
Executable file
5
bin/deno.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/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}" "$@"
|
6
bin/tsx.sh
Executable file
6
bin/tsx.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/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,6 +4,3 @@ 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
Normal file
155
demo/colors.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
1686
demo/editor.rs
Normal file
File diff suppressed because it is too large
Load Diff
1468
demo/kilo.c
Normal file
1468
demo/kilo.c
Normal file
File diff suppressed because it is too large
Load Diff
271
demo/test.css
Normal file
271
demo/test.css
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
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,5 +13,6 @@
|
|||||||
"semiColons": true,
|
"semiColons": true,
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"nodeModulesDir": true
|
"nodeModulesDir": true,
|
||||||
|
"exclude": ["src/bun/"]
|
||||||
}
|
}
|
||||||
|
58
justfile
58
justfile
@ -3,31 +3,41 @@ default:
|
|||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Test coverage
|
# Test coverage
|
||||||
coverage: bun-test deno-coverage
|
coverage: deno-coverage bun-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 --unstable-ffi --name="Scroll" ./src/scroll.ts ./src/common/mod.ts ./src/deno/mod.ts ./src/bun/mod.ts
|
deno doc --html --name="Scroll" ./src/common/*.ts ./src/common/**/*.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 bun-test
|
test: deno-test tsx-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 scroll.err
|
rm -f test.file
|
||||||
rm -f tsconfig.tsbuildinfo
|
rm -f tsconfig.tsbuildinfo
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
@ -36,15 +46,19 @@ clean:
|
|||||||
|
|
||||||
# Check code with actual Typescript compiler
|
# Check code with actual Typescript compiler
|
||||||
bun-check:
|
bun-check:
|
||||||
bunx tsc
|
bun run bun-check
|
||||||
|
|
||||||
# Test with bun
|
# Test with bun
|
||||||
bun-test:
|
bun-test:
|
||||||
bun test --coverage
|
bun run bun-test
|
||||||
|
|
||||||
|
# CLI test coverage report
|
||||||
|
bun-coverage:
|
||||||
|
bun run bun-coverage
|
||||||
|
|
||||||
# Run with bun
|
# Run with bun
|
||||||
bun-run file="":
|
bun-run file="":
|
||||||
bun run ./src/scroll.ts {{file}}
|
bun run bun-run {{file}}
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
# Deno-specific commands
|
# Deno-specific commands
|
||||||
@ -52,17 +66,35 @@ bun-run file="":
|
|||||||
|
|
||||||
# Lint code and check types
|
# Lint code and check types
|
||||||
deno-check:
|
deno-check:
|
||||||
deno lint
|
deno task deno-lint
|
||||||
deno check --unstable-ffi --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
deno task deno-check
|
||||||
|
|
||||||
# Test with deno
|
# Test with deno
|
||||||
deno-test:
|
deno-test:
|
||||||
deno test --allow-all --unstable-ffi
|
deno task deno-test
|
||||||
|
|
||||||
# Create test coverage report with deno
|
# Create test coverage report with deno
|
||||||
deno-coverage:
|
deno-coverage:
|
||||||
./coverage.sh
|
deno task deno-coverage
|
||||||
|
|
||||||
# Run with deno
|
# Run with deno
|
||||||
deno-run file="":
|
deno-run file="":
|
||||||
deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts {{file}}
|
deno task deno-run {{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,6 +1,24 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "^1.0.11"
|
"@types/node": "*",
|
||||||
}
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
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,25 +1,14 @@
|
|||||||
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';
|
/**
|
||||||
import BunTerminalIO from './terminal_io.ts';
|
* The Bun Runtime implementation
|
||||||
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;
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
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,11 +1,10 @@
|
|||||||
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, expect, test } from 'bun:test';
|
import { describe, test } from 'bun:test';
|
||||||
import { ITestBase } from '../common/types.ts';
|
import AbstractTestBase from '../common/runtime/test_base.ts';
|
||||||
|
class BunTestBase extends AbstractTestBase {
|
||||||
export function testSuite(testObj: any) {
|
public static testSuite(testObj: any): void {
|
||||||
Object.keys(testObj).forEach((group) => {
|
Object.keys(testObj).forEach((group) => {
|
||||||
describe(group, () => {
|
describe(group, () => {
|
||||||
const groupObj = testObj[group];
|
const groupObj = testObj[group];
|
||||||
@ -15,21 +14,6 @@ export function testSuite(testObj: any) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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,270 +2,41 @@ 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,
|
||||||
@ -279,47 +50,49 @@ 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', '😸', '😹'];
|
||||||
assertEquals(b, c);
|
assertEquivalent(b, c);
|
||||||
|
|
||||||
const d = arrayInsert(c, 17, 'y');
|
const d = arrayInsert(c, 17, 'y');
|
||||||
const e = ['😺', 'x', '😸', '😹', 'y'];
|
const e = ['😺', 'x', '😸', '😹', 'y'];
|
||||||
assertEquals(d, e);
|
assertEquivalent(d, e);
|
||||||
|
|
||||||
assertEquals(arrayInsert([], 0, 'foo'), ['foo']);
|
assertEquivalent(arrayInsert([], 0, 'foo'), ['foo']);
|
||||||
},
|
},
|
||||||
'arrayInsert() numbers': () => {
|
'arrayInsert() numbers': () => {
|
||||||
const a = [1, 3, 5];
|
const a = [1, 3, 5];
|
||||||
const b = [1, 3, 4, 5];
|
const b = [1, 3, 4, 5];
|
||||||
assertEquals(arrayInsert(a, 2, 4), b);
|
assertEquivalent(arrayInsert(a, 2, 4), b);
|
||||||
|
|
||||||
const c = [1, 2, 3, 4, 5];
|
const c = [1, 2, 3, 4, 5];
|
||||||
assertEquals(arrayInsert(b, 1, 2), c);
|
assertEquivalent(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);
|
||||||
@ -340,7 +113,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': () => {
|
||||||
assertEquals(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
assertEquivalent(strChars('😺😸😹'), ['😺', '😸', '😹']);
|
||||||
},
|
},
|
||||||
'ctrlKey()': () => {
|
'ctrlKey()': () => {
|
||||||
const ctrl_a = ctrlKey('a');
|
const ctrl_a = ctrlKey('a');
|
||||||
@ -391,8 +164,6 @@ const fnTest = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const readKeyTest = () => {
|
const readKeyTest = () => {
|
||||||
const { KeyCommand } = _Ansi;
|
const { KeyCommand } = _Ansi;
|
||||||
const { readKey, ctrlKey } = Fn;
|
const { readKey, ctrlKey } = Fn;
|
||||||
@ -445,17 +216,502 @@ 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,6 +42,7 @@ export enum AnsiColor {
|
|||||||
FgMagenta,
|
FgMagenta,
|
||||||
FgCyan,
|
FgCyan,
|
||||||
FgWhite,
|
FgWhite,
|
||||||
|
ForegroundColor,
|
||||||
FgDefault,
|
FgDefault,
|
||||||
|
|
||||||
// Background Colors
|
// Background Colors
|
||||||
@ -53,6 +54,7 @@ export enum AnsiColor {
|
|||||||
BgMagenta,
|
BgMagenta,
|
||||||
BgCyan,
|
BgCyan,
|
||||||
BgWhite,
|
BgWhite,
|
||||||
|
BackgroundColor,
|
||||||
BgDefault,
|
BgDefault,
|
||||||
|
|
||||||
// Bright Foreground Colors
|
// Bright Foreground Colors
|
||||||
@ -77,8 +79,8 @@ export enum AnsiColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Ground {
|
export enum Ground {
|
||||||
Fore = AnsiColor.FgDefault,
|
Fore = AnsiColor.ForegroundColor,
|
||||||
Back = AnsiColor.BgDefault,
|
Back = AnsiColor.BackgroundColor,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@ -106,7 +108,6 @@ 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 = (
|
||||||
@ -116,6 +117,9 @@ 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'),
|
||||||
@ -129,7 +133,46 @@ 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,10 +1,17 @@
|
|||||||
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 = '';
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static default(): Buffer {
|
||||||
|
return new Buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public append(s: string, maxLen?: number): void {
|
public append(s: string, maxLen?: number): void {
|
||||||
|
@ -1,13 +1,27 @@
|
|||||||
import { ITerminalSize } from './types.ts';
|
import Ansi from './ansi.ts';
|
||||||
|
import { HighlightType, ITerminalSize } from './types.ts';
|
||||||
|
|
||||||
export const SCROLL_VERSION = '0.0.1';
|
export const SCROLL_VERSION = '0.1.0';
|
||||||
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 = './scroll.log';
|
export const SCROLL_LOG_FILE_PREFIX = './scroll';
|
||||||
export const SCROLL_ERR_FILE = './scroll.err';
|
export const SCROLL_LOG_FILE_SUFFIX = '.log';
|
||||||
|
|
||||||
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,34 +1,36 @@
|
|||||||
import Row from './row.ts';
|
import Row from './row.ts';
|
||||||
import { arrayInsert, some, strlen } from './fns.ts';
|
import { FileType } from './filetype.ts';
|
||||||
import { HighlightType } from './highlight.ts';
|
import { arrayInsert } from './fns.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import Option, { None, Some } from './option.ts';
|
||||||
import { Position } from './types.ts';
|
import { getRuntime, logWarning } from './runtime.ts';
|
||||||
import { Search } from './search.ts';
|
import { Position, SearchDirection } from './types.ts';
|
||||||
|
|
||||||
export class Document {
|
export class Document {
|
||||||
#rows: Row[];
|
/**
|
||||||
#search: Search;
|
* Each line of the current document
|
||||||
|
*/
|
||||||
|
#rows: Row[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has the document been modified?
|
* @param dirty - Has the document been modified?
|
||||||
|
* @param type - The meta-data for the file type of the current document
|
||||||
*/
|
*/
|
||||||
public dirty: boolean;
|
private constructor(
|
||||||
|
public dirty: boolean = false,
|
||||||
private constructor() {
|
public type: FileType = FileType.default(),
|
||||||
this.#rows = [];
|
) {
|
||||||
this.#search = new Search();
|
|
||||||
this.dirty = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get numRows(): number {
|
public get fileType(): string {
|
||||||
|
return this.type.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get numRows(): number {
|
||||||
return this.#rows.length;
|
return this.#rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static default(): Document {
|
public static default(): Document {
|
||||||
const self = new Document();
|
return new Document();
|
||||||
self.#search.parent = self;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
@ -46,9 +48,13 @@ 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) => this.insertRow(this.numRows, row));
|
.forEach((row: string) => {
|
||||||
|
this.#rows.push(Row.from(row));
|
||||||
|
});
|
||||||
|
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
|
||||||
@ -58,71 +64,104 @@ export class Document {
|
|||||||
/**
|
/**
|
||||||
* Save the current document
|
* Save the current document
|
||||||
*/
|
*/
|
||||||
public async save(filename: string) {
|
public async save(filename: string): Promise<void> {
|
||||||
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() {
|
/**
|
||||||
this.#search = new Search();
|
* Find the cursor position of the query, if it exists
|
||||||
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,
|
||||||
key: string,
|
at: Position,
|
||||||
): Position | null {
|
direction: SearchDirection,
|
||||||
const potential = this.#search.search(q, key);
|
): Option<Position> {
|
||||||
if (some(potential) && potential instanceof Position) {
|
if (at.y >= this.numRows) {
|
||||||
// Update highlight of search match
|
logWarning('Trying to search beyond the end of the current file', {
|
||||||
const row = this.#rows[potential.y];
|
at,
|
||||||
|
document: this,
|
||||||
|
});
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Okay, we have to take the Javascript string index (potential.x), convert
|
const position = Position.from(at);
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Just to be safe with unicode searches, take the number of 'characters'
|
for (let y = at.y; y >= 0 && y < this.numRows; y += direction) {
|
||||||
// as the search query length, not the JS string length.
|
const maybeMatch = this.#rows[y].find(q, position.x, direction);
|
||||||
const end = start + strlen(q);
|
if (maybeMatch.isSome()) {
|
||||||
|
position.x = maybeMatch.unwrap();
|
||||||
|
return Some(position);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
if (direction === SearchDirection.Forward) {
|
||||||
row.hl[i] = HighlightType.Match;
|
position.y += 1;
|
||||||
|
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 potential;
|
return None;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRow = this.#rows[at.y].split(at.x);
|
// Split the current row, and insert a new
|
||||||
newRow.update();
|
// row with the leftovers
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,65 +174,71 @@ export class Document {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = this.row(at.y)!;
|
this.dirty = true;
|
||||||
const mergeNextRow = at.x === row.size && at.y + 1 < len;
|
|
||||||
const mergeIntoPrevRow = at.x === 0 && at.y > 0;
|
const maybeRow = this.row(at.y);
|
||||||
|
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
|
// the merged row object (This also works for pressing
|
||||||
|
// 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 on
|
// the next line into the current one
|
||||||
const rowToAppend = this.#rows.at(at.y + 1)!.toString();
|
const rowToAppend = this.#rows[at.y + 1].toString();
|
||||||
row.append(rowToAppend);
|
row.append(rowToAppend, this.type);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.update();
|
this.unHighlightRows(at.y);
|
||||||
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public row(i: number): Row | null {
|
public row(i: number): Option<Row> {
|
||||||
return this.#rows[i] ?? null;
|
if (i >= this.numRows || i < 0) {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertRow(at: number = this.numRows, s: string = ''): void {
|
return Option.from(this.#rows.at(i));
|
||||||
this.#rows = arrayInsert(this.#rows, at, Row.from(s));
|
|
||||||
this.#rows[at].update();
|
|
||||||
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch?: string): void {
|
public highlight(searchMatch: Option<string>, limit: Option<number>): void {
|
||||||
this.#rows.forEach((row) => {
|
let startWithComment = false;
|
||||||
row.update(searchMatch);
|
let until = this.numRows;
|
||||||
});
|
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
|
|
||||||
*/
|
*/
|
||||||
private deleteRow(at: number): void {
|
protected 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
|
|
||||||
*/
|
*/
|
||||||
private rowsToString(): string {
|
protected 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
5
src/common/filetype.ts
Normal file
5
src/common/filetype.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './filetype/base.ts';
|
||||||
|
export * from './filetype/filetype.ts';
|
||||||
|
|
||||||
|
import FileType from './filetype/filetype.ts';
|
||||||
|
export default FileType;
|
91
src/common/filetype/base.ts
Normal file
91
src/common/filetype/base.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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,
|
||||||
|
};
|
151
src/common/filetype/c.ts
Normal file
151
src/common/filetype/c.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
393
src/common/filetype/css.ts
Normal file
393
src/common/filetype/css.ts
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
41
src/common/filetype/filetype.ts
Normal file
41
src/common/filetype/filetype.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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;
|
152
src/common/filetype/javascript.ts
Normal file
152
src/common/filetype/javascript.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
169
src/common/filetype/rust.ts
Normal file
169
src/common/filetype/rust.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
37
src/common/filetype/shell.ts
Normal file
37
src/common/filetype/shell.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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,4 +1,6 @@
|
|||||||
import { KeyCommand } from './ansi.ts';
|
import Ansi, { KeyCommand } from './ansi.ts';
|
||||||
|
import { SCROLL_COLOR_SCHEME } from './config.ts';
|
||||||
|
import { HighlightType } from './types.ts';
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
@ -11,20 +13,6 @@ const decoder = new TextDecoder();
|
|||||||
*/
|
*/
|
||||||
export const noop = () => {};
|
export const noop = () => {};
|
||||||
|
|
||||||
/**
|
|
||||||
* Does a value exist? (not null or undefined)
|
|
||||||
*/
|
|
||||||
export function some(v: unknown): boolean {
|
|
||||||
return v !== null && typeof v !== 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the value null or undefined?
|
|
||||||
*/
|
|
||||||
export function none(v: unknown): boolean {
|
|
||||||
return v === null || typeof v === 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert input from ANSI escape sequences into a form
|
* Convert input from ANSI escape sequences into a form
|
||||||
* that can be more easily mapped to editor commands
|
* that can be more easily mapped to editor commands
|
||||||
@ -71,12 +59,23 @@ 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
|
||||||
@ -101,6 +100,7 @@ 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 'characters'
|
* @param s - the string to split into unicode code points
|
||||||
*/
|
*/
|
||||||
export function strChars(s: string): string[] {
|
export function strChars(s: string): string[] {
|
||||||
return s.split(/(?:)/u);
|
return [...s];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,6 +164,17 @@ 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?
|
||||||
*
|
*
|
||||||
@ -193,6 +204,15 @@ 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
|
||||||
*
|
*
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
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,9 +1,18 @@
|
|||||||
import process from 'node:process';
|
|
||||||
import { readKey } from './fns.ts';
|
import { readKey } from './fns.ts';
|
||||||
import { getRuntime, logError } from './runtime.ts';
|
import {
|
||||||
|
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;
|
||||||
|
|
||||||
@ -14,15 +23,13 @@ export async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Setup error handler to log to file
|
// Setup error handler to log to file
|
||||||
rt.onEvent('error', (error) => {
|
rt.onEvent('error', (error: any) => {
|
||||||
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 = new Editor(terminalSize);
|
const editor = Editor.create(await term.getTerminalSize());
|
||||||
|
|
||||||
// Process cli arguments
|
// Process cli arguments
|
||||||
if (term.argv.length > 0) {
|
if (term.argv.length > 0) {
|
||||||
@ -43,7 +50,9 @@ export async function main() {
|
|||||||
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) {
|
||||||
continue;
|
logWarning('Empty input returned from runtime input loop');
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process input
|
// Process input
|
||||||
@ -51,6 +60,9 @@ export async function main() {
|
|||||||
if (!shouldLoop) {
|
if (!shouldLoop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render output
|
||||||
|
await editor.refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
261
src/common/option.ts
Normal file
261
src/common/option.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* 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,14 +4,23 @@
|
|||||||
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,8 +1,22 @@
|
|||||||
import { SCROLL_TAB_SIZE } from './config.ts';
|
|
||||||
import { arrayInsert, isAsciiDigit, some, strChars } from './fns.ts';
|
|
||||||
import { highlightToColor, HighlightType } from './highlight.ts';
|
|
||||||
import Ansi from './ansi.ts';
|
import Ansi from './ansi.ts';
|
||||||
|
|
||||||
|
import { SCROLL_TAB_SIZE } from './config.ts';
|
||||||
|
import {
|
||||||
|
arrayInsert,
|
||||||
|
highlightToColor,
|
||||||
|
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
|
||||||
* multi-byte graphemes, all operations are done on an
|
* multi-byte graphemes, all operations are done on an
|
||||||
@ -25,27 +39,47 @@ 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;
|
||||||
@ -54,11 +88,17 @@ 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();
|
this.update(None, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
@ -71,10 +111,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): Row {
|
public split(at: number, syntax: FileType): Row {
|
||||||
const newRow = new Row(this.chars.slice(at));
|
const newRow = new Row(this.chars.slice(at));
|
||||||
this.chars = this.chars.slice(0, at);
|
this.chars = this.chars.slice(0, at);
|
||||||
this.update();
|
this.update(None, syntax);
|
||||||
|
|
||||||
return newRow;
|
return newRow;
|
||||||
}
|
}
|
||||||
@ -92,25 +132,48 @@ export class Row {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the current row for the specified string, and return
|
* Search the current row for the specified string, and return
|
||||||
* the index of the start of that match
|
* the 'character' index of the start of that match
|
||||||
*/
|
*/
|
||||||
public find(s: string, offset: number = 0): number | null {
|
public find(
|
||||||
const thisStr = this.toString();
|
s: string,
|
||||||
if (!this.toString().includes(s)) {
|
at: number = 0,
|
||||||
return null;
|
direction: SearchDirection = SearchDirection.Forward,
|
||||||
|
): Option<number> {
|
||||||
|
if (at > this.size) {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
const thisStr = this.chars.join('');
|
||||||
|
|
||||||
const byteCount = thisStr.indexOf(s, this.charIndexToByteIndex(offset));
|
// Look for the search query `s`, starting from the 'character' `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 byteCount;
|
return Some(byteIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emoji/Extended Unicode-friendly search
|
// Emoji/Extended Unicode-friendly search
|
||||||
return this.byteIndexToCharIndex(byteCount);
|
return Some(this.byteIndexToCharIndex(byteIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,44 +242,447 @@ 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);
|
||||||
this.highlight(searchMatch);
|
return this.highlight(word, syntax, startWithComment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public highlight(searchMatch?: string): void {
|
/**
|
||||||
const highlighting = [];
|
* Calculate the syntax types of the current Row
|
||||||
|
*/
|
||||||
if (some(searchMatch)) {
|
public highlight(
|
||||||
// TODO: highlight search here
|
word: Option<string>,
|
||||||
|
syntax: FileType,
|
||||||
|
startWithComment: boolean,
|
||||||
|
): boolean {
|
||||||
|
// When the highlighting is already up-to-date
|
||||||
|
if (this.isHighlighted && word.isNone()) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ch of this.rchars) {
|
this.hl = [];
|
||||||
if (isAsciiDigit(ch)) {
|
let i = 0;
|
||||||
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 {
|
||||||
highlighting.push(HighlightType.None);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hl = highlighting;
|
protected highlightComment(
|
||||||
|
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,141 +1,11 @@
|
|||||||
import process from 'node:process';
|
export * from './runtime/file_io.ts';
|
||||||
import { IRuntime, ITestBase } from './types.ts';
|
export * from './runtime/helpers.ts';
|
||||||
import { noop } from './fns.ts';
|
export * from './runtime/log.ts';
|
||||||
import { SCROLL_ERR_FILE, SCROLL_LOG_FILE } from './config.ts';
|
export * from './runtime/node.ts';
|
||||||
|
export * from './runtime/runtime.ts';
|
||||||
|
export * from './runtime/terminal_io.ts';
|
||||||
|
export * from './runtime/test_base.ts';
|
||||||
|
export { RunTimeType } from './types.ts';
|
||||||
|
|
||||||
export type { IFileIO, IRuntime, ITerminal } from './types.ts';
|
import { CommonRuntime } from './runtime/runtime.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;
|
|
||||||
};
|
|
||||||
|
28
src/common/runtime/file_io.ts
Normal file
28
src/common/runtime/file_io.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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;
|
82
src/common/runtime/helpers.ts
Normal file
82
src/common/runtime/helpers.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
68
src/common/runtime/log.ts
Normal file
68
src/common/runtime/log.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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);
|
9
src/common/runtime/node.ts
Normal file
9
src/common/runtime/node.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 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 };
|
139
src/common/runtime/runtime.ts
Normal file
139
src/common/runtime/runtime.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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),
|
||||||
|
};
|
44
src/common/runtime/terminal_io.ts
Normal file
44
src/common/runtime/terminal_io.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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;
|
95
src/common/runtime/test_base.ts
Normal file
95
src/common/runtime/test_base.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
@ -1,76 +0,0 @@
|
|||||||
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,122 +1,47 @@
|
|||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of terminal in rows and columns
|
* Which Typescript runtime is currently being used
|
||||||
*/
|
*/
|
||||||
export interface ITerminalSize {
|
export enum RunTimeType {
|
||||||
rows: number;
|
Bun = 'bun',
|
||||||
cols: number;
|
Deno = 'deno',
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime-specific terminal functionality
|
* The type of Syntax being highlighted
|
||||||
*/
|
*/
|
||||||
export type ITerminal = IRuntime['term'];
|
export enum HighlightType {
|
||||||
|
/** No highlighting */
|
||||||
/**
|
None,
|
||||||
* Runtime-specific file handling
|
/** Number literals */
|
||||||
*/
|
Number,
|
||||||
export type IFileIO = IRuntime['file'];
|
/** Search results */
|
||||||
|
Match,
|
||||||
// ----------------------------------------------------------------------------
|
/** Character literals */
|
||||||
// Testing
|
Character,
|
||||||
// ----------------------------------------------------------------------------
|
/** String literals */
|
||||||
|
String,
|
||||||
/**
|
/** Single line comments */
|
||||||
* The shared test interface, so tests can be run by both runtimes
|
SingleLineComment,
|
||||||
*/
|
/** Multi-line comments */
|
||||||
export interface ITestBase {
|
MultiLineComment,
|
||||||
assertEquals(actual: unknown, expected: unknown): void;
|
/** Primary keywords */
|
||||||
assertExists(actual: unknown): void;
|
Keyword1,
|
||||||
assertFalse(actual: boolean): void;
|
/** Secondary keywords */
|
||||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
Keyword2,
|
||||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
/** Math/logic operators */
|
||||||
assertNull(actual: unknown): void;
|
Operator,
|
||||||
assertStrictEquals(actual: unknown, expected: unknown): void;
|
}
|
||||||
assertTrue(actual: boolean): void;
|
|
||||||
testSuite(testObj: any): void;
|
/**
|
||||||
|
* Which direction to search in the current document
|
||||||
|
*/
|
||||||
|
export enum SearchDirection {
|
||||||
|
Forward = 1,
|
||||||
|
Backward = -1,
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * as stdAssert from 'https://deno.land/std@0.208.0/assert/mod.ts';
|
|
@ -1,7 +1,3 @@
|
|||||||
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,16 +1,15 @@
|
|||||||
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 { IRuntime, RunTimeType } from '../common/runtime.ts';
|
import { CommonRuntime, 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,
|
||||||
@ -20,7 +19,6 @@ 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,6 +1,3 @@
|
|||||||
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,48 +1,15 @@
|
|||||||
if (!('Deno' in globalThis)) {
|
// @ts-ignore The import exists, but tsc complains
|
||||||
throw new Error('This module requires Deno to run');
|
import { test } from 'node:test';
|
||||||
}
|
import AbstractTestBase from '../common/runtime/test_base.ts';
|
||||||
import { ITestBase } from '../common/types.ts';
|
class DenoTestBase extends AbstractTestBase {
|
||||||
import { stdAssert } from './deps.ts';
|
public static testSuite(testObj: any) {
|
||||||
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) => {
|
||||||
Deno.test(testName, groupObj[testName]);
|
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;
|
||||||
|
14
src/tsx/mod.ts
Normal file
14
src/tsx/mod.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
21
src/tsx/test_base.ts
Normal file
21
src/tsx/test_base.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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,10 +10,14 @@
|
|||||||
"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,
|
||||||
"allowSyntheticDefaultImports": true
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"exclude": ["src/deno"]
|
"exclude": ["src/deno"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user