@adecore/editor 0.0.1 → 0.17.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +82 -0
- package/dist/attribution.d.ts +28 -0
- package/dist/attribution.js +55 -0
- package/dist/code-block.d.ts +2 -0
- package/dist/code-block.js +89 -0
- package/dist/column-selection.d.ts +9 -0
- package/dist/column-selection.js +52 -0
- package/dist/controller.d.ts +64 -0
- package/dist/controller.js +772 -0
- package/dist/editor.css +813 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +604 -0
- package/dist/fake.d.ts +190 -0
- package/dist/fake.js +658 -0
- package/dist/find.d.ts +30 -0
- package/dist/find.js +108 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/input.d.ts +9 -0
- package/dist/input.js +57 -0
- package/dist/keymap-table.d.ts +102 -0
- package/dist/keymap-table.js +130 -0
- package/dist/keymap.d.ts +36 -0
- package/dist/keymap.js +163 -0
- package/dist/layout.d.ts +167 -0
- package/dist/layout.js +583 -0
- package/dist/lens.d.ts +3 -0
- package/dist/lens.js +29 -0
- package/dist/listeners.d.ts +3 -0
- package/dist/listeners.js +12 -0
- package/dist/metrics.d.ts +8 -0
- package/dist/metrics.js +54 -0
- package/dist/modifier-gesture.d.ts +24 -0
- package/dist/modifier-gesture.js +98 -0
- package/dist/native-input.d.ts +26 -0
- package/dist/native-input.js +147 -0
- package/dist/offsets.d.ts +2 -0
- package/dist/offsets.js +14 -0
- package/dist/outline.d.ts +55 -0
- package/dist/outline.js +181 -0
- package/dist/overview.d.ts +17 -0
- package/dist/overview.js +55 -0
- package/dist/paint.d.ts +86 -0
- package/dist/paint.js +481 -0
- package/dist/php-html.d.ts +9 -0
- package/dist/php-html.js +36 -0
- package/dist/pointer.d.ts +49 -0
- package/dist/pointer.js +243 -0
- package/dist/scroll-animation.d.ts +3 -0
- package/dist/scroll-animation.js +45 -0
- package/dist/scroll.d.ts +28 -0
- package/dist/scroll.js +76 -0
- package/dist/semantic.d.ts +8 -0
- package/dist/semantic.js +47 -0
- package/dist/shiki.d.ts +7 -0
- package/dist/shiki.js +106 -0
- package/dist/smart-keys.d.ts +3 -0
- package/dist/smart-keys.js +14 -0
- package/dist/testing.d.ts +34 -0
- package/dist/testing.js +117 -0
- package/dist/theme-scopes.d.ts +9 -0
- package/dist/theme-scopes.js +61 -0
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.js +152 -0
- package/dist/tracked-range.d.ts +5 -0
- package/dist/tracked-range.js +20 -0
- package/dist/types.d.ts +342 -0
- package/dist/types.js +1 -0
- package/dist/view.d.ts +382 -0
- package/dist/view.js +2134 -0
- package/package.json +63 -3
- package/src/attribution.ts +85 -0
- package/src/code-block.ts +100 -0
- package/src/column-selection.ts +63 -0
- package/src/controller.ts +838 -0
- package/src/editor.css +813 -0
- package/src/engine.ts +757 -0
- package/src/fake.ts +831 -0
- package/src/find.ts +128 -0
- package/src/index.ts +69 -0
- package/src/input.ts +63 -0
- package/src/keymap-table.ts +163 -0
- package/src/keymap.ts +204 -0
- package/src/layout.ts +754 -0
- package/src/lens.ts +32 -0
- package/src/listeners.ts +15 -0
- package/src/metrics.ts +65 -0
- package/src/modifier-gesture.ts +115 -0
- package/src/native-input.ts +179 -0
- package/src/offsets.ts +16 -0
- package/src/outline.ts +248 -0
- package/src/overview.ts +80 -0
- package/src/paint.ts +608 -0
- package/src/php-html.ts +43 -0
- package/src/pointer.ts +326 -0
- package/src/scroll-animation.ts +48 -0
- package/src/scroll.ts +102 -0
- package/src/semantic.ts +54 -0
- package/src/shiki.ts +118 -0
- package/src/smart-keys.ts +17 -0
- package/src/testing.ts +157 -0
- package/src/theme-scopes.ts +75 -0
- package/src/tokens.ts +175 -0
- package/src/tracked-range.ts +22 -0
- package/src/types.ts +656 -0
- package/src/view.ts +2436 -0
package/package.json
CHANGED
|
@@ -1,11 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"
|
|
3
|
+
"version": "0.17.0-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"source": "./src/index.ts",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./editor.css": {
|
|
12
|
+
"source": "./src/editor.css",
|
|
13
|
+
"default": "./dist/editor.css"
|
|
14
|
+
},
|
|
15
|
+
"./fake": {
|
|
16
|
+
"source": "./src/fake.ts",
|
|
17
|
+
"types": "./dist/fake.d.ts",
|
|
18
|
+
"default": "./dist/fake.js"
|
|
19
|
+
},
|
|
20
|
+
"./shiki": {
|
|
21
|
+
"source": "./src/shiki.ts",
|
|
22
|
+
"types": "./dist/shiki.d.ts",
|
|
23
|
+
"default": "./dist/shiki.js"
|
|
24
|
+
},
|
|
25
|
+
"./keymap": {
|
|
26
|
+
"source": "./src/keymap-table.ts",
|
|
27
|
+
"types": "./dist/keymap-table.d.ts",
|
|
28
|
+
"default": "./dist/keymap-table.js"
|
|
29
|
+
},
|
|
30
|
+
"./testing": {
|
|
31
|
+
"source": "./src/testing.ts",
|
|
32
|
+
"types": "./dist/testing.d.ts",
|
|
33
|
+
"default": "./dist/testing.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"build": "bun scripts/build.ts",
|
|
39
|
+
"test": "bun test --conditions=source"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@adecore/editor-core": "0.17.0-beta.1",
|
|
43
|
+
"shiki": "^4.4.3",
|
|
44
|
+
"linkedom": "^0.18.13"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.4.2",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"@types/node": "^24.13.6"
|
|
50
|
+
},
|
|
5
51
|
"license": "FSL-1.1-MIT",
|
|
52
|
+
"author": "Bas Milius",
|
|
6
53
|
"repository": {
|
|
7
54
|
"type": "git",
|
|
8
55
|
"url": "git+https://github.com/basmilius/adecore.git",
|
|
9
56
|
"directory": "packages/editor"
|
|
10
|
-
}
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/basmilius/adecore/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://adecore.dev/editor/",
|
|
62
|
+
"sideEffects": [
|
|
63
|
+
"**/*.css"
|
|
64
|
+
],
|
|
65
|
+
"files": [
|
|
66
|
+
"dist",
|
|
67
|
+
"src",
|
|
68
|
+
"!src/**/*.test.ts",
|
|
69
|
+
"LICENSE"
|
|
70
|
+
]
|
|
11
71
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DocumentChange } from '@adecore/editor-core';
|
|
2
|
+
import { mapOffset } from './offsets.ts';
|
|
3
|
+
|
|
4
|
+
/* The custom properties of the agent palette, defined in `editor.css` for both themes. */
|
|
5
|
+
export const AGENT_COLORS = ['--agent-1', '--agent-2', '--agent-3', '--agent-4', '--agent-5', '--agent-6'] as const;
|
|
6
|
+
|
|
7
|
+
export interface AttributedLines {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly color: string;
|
|
10
|
+
readonly sign?: string;
|
|
11
|
+
readonly fill?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Run {
|
|
15
|
+
id: string;
|
|
16
|
+
color: string;
|
|
17
|
+
sign: string | undefined;
|
|
18
|
+
fill: string | undefined;
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* A color washed out to a background that text stays readable on in either theme. */
|
|
24
|
+
export function tintValue(color: string): string {
|
|
25
|
+
return `color-mix(in srgb, ${colorValue(color)} 14%, transparent)`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* The CSS value of a mark's color: `--agent-1` is the page's custom property of that name, anything else is a color as it stands. */
|
|
29
|
+
export function colorValue(color: string): string {
|
|
30
|
+
return color.startsWith('--') ? `var(${color})` : color;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* The runs of lines a host attributed, anchored by offsets so they follow their text through edits. An
|
|
35
|
+
* edit costs one mapping of two offsets per run, and a read works out the lines of only the runs that
|
|
36
|
+
* reach the lines asked for.
|
|
37
|
+
*/
|
|
38
|
+
export class AttributionRuns {
|
|
39
|
+
private runs: Run[] = [];
|
|
40
|
+
|
|
41
|
+
get size(): number {
|
|
42
|
+
return this.runs.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Marks name lines one-based and inclusive; `lineBounds` answers for the document as it is now. */
|
|
46
|
+
set(
|
|
47
|
+
marks: readonly { id: string; startLine: number; endLine: number; color: string; sign?: string; fill?: string }[],
|
|
48
|
+
lineCount: number,
|
|
49
|
+
lineBounds: (line: number) => { start: number; end: number }
|
|
50
|
+
): void {
|
|
51
|
+
const clamp = (line: number): number => Math.min(lineCount, Math.max(1, Math.trunc(line) || 1)) - 1;
|
|
52
|
+
this.runs = marks.map((mark) => {
|
|
53
|
+
const start = clamp(mark.startLine);
|
|
54
|
+
const end = Math.max(start, clamp(mark.endLine));
|
|
55
|
+
return { id: mark.id, color: mark.color, sign: mark.sign, fill: mark.fill, from: lineBounds(start).start, to: lineBounds(end).end };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clear(): void {
|
|
60
|
+
this.runs = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
map(changes: readonly DocumentChange[]): void {
|
|
64
|
+
for (const run of this.runs) {
|
|
65
|
+
run.from = mapOffset(run.from, changes);
|
|
66
|
+
run.to = mapOffset(run.to, changes);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* The run on each line from `first` to `last`, zero-based, where `from` and `to` are the offsets they span; where runs overlap the one set last wins. */
|
|
71
|
+
linesIn(first: number, last: number, from: number, to: number, lineOf: (offset: number) => number): Map<number, AttributedLines> {
|
|
72
|
+
const lines = new Map<number, AttributedLines>();
|
|
73
|
+
for (const run of this.runs) {
|
|
74
|
+
if (run.to < from || run.from > to) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const value = { id: run.id, color: run.color, sign: run.sign, fill: run.fill };
|
|
78
|
+
const end = Math.min(Math.max(lineOf(run.from), lineOf(run.to)), last);
|
|
79
|
+
for (let line = Math.max(lineOf(run.from), first); line <= end; line++) {
|
|
80
|
+
lines.set(line, value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { colorValue, tintValue } from './attribution.ts';
|
|
2
|
+
import { styleSpan } from './paint.ts';
|
|
3
|
+
import type { EditorCodeBlockOptions, LineToken } from './types.ts';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Lines of code that are not part of the document, in the editor's own face, colors and gutter, for a
|
|
7
|
+
* widget row to show text that is not in the file: lines an agent removed, or the other side of a
|
|
8
|
+
* conflict. The gutter column is as wide as the editor's, so the text lines up with the document's.
|
|
9
|
+
*/
|
|
10
|
+
export function renderCodeBlock(
|
|
11
|
+
container: HTMLElement,
|
|
12
|
+
text: string,
|
|
13
|
+
tokens: readonly (readonly LineToken[])[] | null,
|
|
14
|
+
tabSize: number,
|
|
15
|
+
options: EditorCodeBlockOptions
|
|
16
|
+
): void {
|
|
17
|
+
const document = container.ownerDocument;
|
|
18
|
+
const block = document.createElement('div');
|
|
19
|
+
block.className = 'se-code-block';
|
|
20
|
+
block.style.tabSize = String(tabSize);
|
|
21
|
+
const lines = text.split(/\r?\n/);
|
|
22
|
+
lines.forEach((line, index) => {
|
|
23
|
+
const row = document.createElement('div');
|
|
24
|
+
row.className = 'se-code-row';
|
|
25
|
+
if (options.color !== undefined) {
|
|
26
|
+
row.style.background = tintValue(options.color);
|
|
27
|
+
}
|
|
28
|
+
const gutter = document.createElement('div');
|
|
29
|
+
gutter.className = 'se-code-gutter';
|
|
30
|
+
if (options.firstLine !== undefined) {
|
|
31
|
+
const number = document.createElement('span');
|
|
32
|
+
number.textContent = String(options.firstLine + index);
|
|
33
|
+
gutter.append(number);
|
|
34
|
+
}
|
|
35
|
+
if (options.sign) {
|
|
36
|
+
const sign = document.createElement('span');
|
|
37
|
+
sign.className = 'se-line-sign';
|
|
38
|
+
sign.textContent = options.sign;
|
|
39
|
+
if (options.color !== undefined) {
|
|
40
|
+
sign.style.color = colorValue(options.color);
|
|
41
|
+
}
|
|
42
|
+
gutter.append(sign);
|
|
43
|
+
}
|
|
44
|
+
if (options.color !== undefined) {
|
|
45
|
+
const bar = document.createElement('span');
|
|
46
|
+
bar.className = 'se-attribution';
|
|
47
|
+
bar.style.background = colorValue(options.color);
|
|
48
|
+
gutter.append(bar);
|
|
49
|
+
}
|
|
50
|
+
const code = document.createElement('div');
|
|
51
|
+
code.className = 'se-code-text';
|
|
52
|
+
if (options.faded) {
|
|
53
|
+
code.dataset.faded = 'true';
|
|
54
|
+
}
|
|
55
|
+
const emphasis = options.emphasis?.[index] ?? [];
|
|
56
|
+
if (emphasis.length > 0 && options.color !== undefined) {
|
|
57
|
+
row.style.setProperty('--se-emphasis', colorValue(options.color));
|
|
58
|
+
}
|
|
59
|
+
fill(code, line, tokens?.[index] ?? null, emphasis);
|
|
60
|
+
row.append(gutter, code);
|
|
61
|
+
block.append(row);
|
|
62
|
+
});
|
|
63
|
+
container.append(block);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fill(element: HTMLElement, line: string, tokens: readonly LineToken[] | null, emphasis: ReadonlyArray<readonly [number, number]>): void {
|
|
67
|
+
if (tokens === null && emphasis.length === 0) {
|
|
68
|
+
element.textContent = line;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const spans: Array<{ start: number; end: number; token: LineToken | null }> = [];
|
|
72
|
+
let start = 0;
|
|
73
|
+
for (const token of tokens ?? []) {
|
|
74
|
+
const end = Math.min(line.length, start + token.length);
|
|
75
|
+
if (end > start) {
|
|
76
|
+
spans.push({ start, end, token });
|
|
77
|
+
}
|
|
78
|
+
start = end;
|
|
79
|
+
}
|
|
80
|
+
if (start < line.length) {
|
|
81
|
+
spans.push({ start, end: line.length, token: null });
|
|
82
|
+
}
|
|
83
|
+
for (const { start: from, end: to, token } of spans) {
|
|
84
|
+
// A token is cut at the edges of every emphasis that crosses it, so each piece is one color and either on or off.
|
|
85
|
+
const cuts = [...new Set([from, to, ...emphasis.flatMap((range) => range).filter((cut) => cut > from && cut < to)])].sort(
|
|
86
|
+
(left, right) => left - right
|
|
87
|
+
);
|
|
88
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
89
|
+
const piece = element.ownerDocument.createElement('span');
|
|
90
|
+
piece.textContent = line.slice(cuts[i]!, cuts[i + 1]!);
|
|
91
|
+
if (token !== null) {
|
|
92
|
+
styleSpan(piece, token);
|
|
93
|
+
}
|
|
94
|
+
if (emphasis.some(([emphasisStart, emphasisEnd]) => emphasisStart <= cuts[i]! && cuts[i + 1]! <= emphasisEnd)) {
|
|
95
|
+
piece.className = 'se-code-emphasis';
|
|
96
|
+
}
|
|
97
|
+
element.append(piece);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Selection } from '@adecore/editor-core';
|
|
2
|
+
import { type EditorLayout, TEXT_PADDING } from './layout.ts';
|
|
3
|
+
|
|
4
|
+
export interface ContentPoint {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* The cell of a monospaced grid an x falls in, which stands in for the visual column since a tab is a whole number of cells. */
|
|
10
|
+
export function cellAt(layout: EditorLayout, x: number): number {
|
|
11
|
+
return Math.max(0, Math.round((x - TEXT_PADDING) / layout.metrics.charWidth));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* One selection per line between two points, each spanning the same visual columns, the way a column
|
|
16
|
+
* (box) selection works. A line that ends before the box starts has no text in it: it gets a caret at
|
|
17
|
+
* its end when the box is only a column of carets, and none otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export function columnSelections(layout: EditorLayout, from: ContentPoint, to: ContentPoint): Selection[] {
|
|
20
|
+
const { charWidth } = layout.metrics;
|
|
21
|
+
const document = layout.document;
|
|
22
|
+
const fromLine = document.positionAt(layout.hitTest(from.x, from.y)).line;
|
|
23
|
+
const toLine = document.positionAt(layout.hitTest(to.x, to.y)).line;
|
|
24
|
+
const fromCell = cellAt(layout, from.x);
|
|
25
|
+
const toCell = cellAt(layout, to.x);
|
|
26
|
+
const left = Math.min(fromCell, toCell);
|
|
27
|
+
const right = Math.max(fromCell, toCell);
|
|
28
|
+
const reversed = toCell < fromCell;
|
|
29
|
+
const result: Selection[] = [];
|
|
30
|
+
for (let line = Math.min(fromLine, toLine); line <= Math.max(fromLine, toLine); line++) {
|
|
31
|
+
const row = layout.rowForLine(line);
|
|
32
|
+
if (row.kind !== 'text' || layout.isHidden(line)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const geometry = layout.geometry(row);
|
|
36
|
+
const endX = geometry.rowStarts.length > 1 ? geometry.rowEnds[0]! : geometry.before[geometry.before.length - 1]!;
|
|
37
|
+
const offsetAt = (cell: number): number => layout.offsetAtX(geometry, 0, TEXT_PADDING + cell * charWidth).offset;
|
|
38
|
+
if (cellAt(layout, endX + TEXT_PADDING) < left) {
|
|
39
|
+
if (left === right) {
|
|
40
|
+
const end = document.getLine(line).end;
|
|
41
|
+
result.push({ anchor: end, head: end });
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const start = offsetAt(left);
|
|
46
|
+
const end = offsetAt(right);
|
|
47
|
+
result.push(reversed ? { anchor: end, head: start } : { anchor: start, head: end });
|
|
48
|
+
}
|
|
49
|
+
if (result.length === 0) {
|
|
50
|
+
const end = document.getLine(toLine).end;
|
|
51
|
+
result.push({ anchor: end, head: end });
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Whether two points are in the same cell of the same line, which makes a press with alt a click and not yet a box. */
|
|
57
|
+
export function sameCell(layout: EditorLayout, from: ContentPoint, to: ContentPoint): boolean {
|
|
58
|
+
const document = layout.document;
|
|
59
|
+
return (
|
|
60
|
+
cellAt(layout, from.x) === cellAt(layout, to.x) &&
|
|
61
|
+
document.positionAt(layout.hitTest(from.x, from.y)).line === document.positionAt(layout.hitTest(to.x, to.y)).line
|
|
62
|
+
);
|
|
63
|
+
}
|