@adecore/editor 0.17.0-beta.1 → 0.17.0
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 +1 -1
- package/README.md +40 -65
- package/dist/controller.js +3 -2
- package/dist/editor.css +5 -3
- package/dist/engine.js +18 -5
- package/dist/fake.d.ts +2 -0
- package/dist/fake.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/layout.d.ts +3 -0
- package/dist/layout.js +15 -1
- package/dist/smart-keys.js +1 -0
- package/dist/types.d.ts +9 -3
- package/dist/view.d.ts +2 -0
- package/dist/view.js +41 -16
- package/package.json +2 -2
- package/src/controller.ts +3 -2
- package/src/editor.css +5 -3
- package/src/engine.ts +20 -5
- package/src/fake.ts +7 -0
- package/src/index.ts +1 -0
- package/src/layout.ts +25 -1
- package/src/smart-keys.ts +1 -0
- package/src/types.ts +13 -1
- package/src/view.ts +44 -16
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,82 +1,57 @@
|
|
|
1
1
|
# @adecore/editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@adecore/editor)
|
|
4
|
+
[](https://adecore.dev/editor/)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
A code editor for the DOM. It draws a [`DocumentModel`](https://adecore.dev/editor-core/) with a layout of its own, renders only the rows in view, and takes the decorations, rows and marks a host hands it: problems, inlay hints, semantic colors, change marks, widgets between lines, code vision, agent attribution and ghost text. It knows no language server; [`@adecore/editor-react`](https://adecore.dev/editor-react/) adds one.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const engine = createSmartEditorEngine({
|
|
9
|
-
tokenizer: shikiTokenizers(() => highlighter),
|
|
10
|
-
handBack: shellShortcuts,
|
|
11
|
-
apple
|
|
12
|
-
});
|
|
13
|
-
const editor = engine.mount(element, { text, language: 'typescript', theme: 'github-dark' });
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Import `@adecore/ui/theme.css`, then `@adecore/editor/editor.css`. The element needs a positioned box sized by its parent. Editor, find, change and attribution colors have defaults on Adecore theme tokens. The code face uses `--font-mono`, `--code-font-size` and `--code-line-height`; the latter two default to `--text-code` and `--text-code--line-height`. Override them after the editor CSS, and call `refreshFont()` when they change.
|
|
17
|
-
|
|
18
|
-
## How it is put together
|
|
19
|
-
|
|
20
|
-
- `layout.ts`: the rows of the document, free of the DOM. A row is a line, a widget or a fold's first line. It owns wrapping into visual lines, tab stops, inlays, folded lines, caret and selection boxes, hit testing and moving by visual line. Only the rows on screen are measured; the others stand on an estimate that the first draw corrects.
|
|
21
|
-
- `view.ts`: the DOM, the scroll (native: the gutter and the pinned headers stick to the corner of the one scroll container, and the scroll event draws the rows, a viewport of them on each side, in the frame it shows in), the folds and what is drawn behind and above the text. `paint.ts` holds the drawing of rows, the gutter, the selection and the carets. Rows are absolutely placed runs of text, so the layout decides where each character is. A draw reads the viewport's scroll position and size once at its start, since a read after the first write of a frame makes the browser lay the page out again, and a host setter whose value did not change draws nothing. The horizontal scroll extent is the widest line measured since the text last changed, inlays included, plus three columns as on the platform; a host row follows the scroll and clips what it draws past its width, since an overflow there would grow the extent with every scroll and carry the sticky gutter out of view.
|
|
22
|
-
- `controller.ts`: what a person does. Keys come from one table (`keymap-table.ts`, which the client prints in its menus as well) and are mapped in `keymap.ts` to the model's commands, typing goes through `DocumentModel.typeText`, a paste through `DocumentModel.paste`, the mouse through `pointer.ts`. A copy or cut of a bare caret remembers its text for every editor of the page, and pasting that same text again, in any of them, lands as whole lines. Adding a caret above or below is the double tap of a modifier and an arrow (`modifier-gesture.ts`). The smart keys (`EditorSmartKeys`: pairing, surrounding, tabbing out, smart Enter, indent on paste, the smart semicolon and the camel humps) are options of the editor, set at mounting and changed with `setSmartKeys`; all are on except the camel humps. The textarea (`native-input.ts`) is only the sink for the platform's typing, composition and paste, and carries the text around the caret for assistive technology.
|
|
23
|
-
- `tokens.ts` and `shiki.ts`: coloring. A grammar needs the state the previous line ended in, so lines are colored from a frontier down, a slice at a time, on the screen plus a margin. An edit moves the frontier to the changed line and stops as soon as a line ends in the state it had before. The tokenizer is injected (`TokenizerSource`); `shikiTokenizers` adapts the viewer's Shiki highlighter. It loads the languages a grammar embeds only on demand (a style block's `lang="scss"`, a Markdown fence) when the document names them, since Shiki rebuilds the host grammar when one loads later, and a tokenizer says so with `stale` so the colors start over from the top. A language added to a document after it opened (a new fence) is colored once the file opens again. `php-html.ts` is the grammar for a PHP file as a document (HTML with PHP between its tags), which Shiki does not bundle; PHP inside an HTML attribute is not colored.
|
|
24
|
-
- `outline.ts`: the blocks of the document with a header line, read from braces and indentation until the host hands over better ones with `setBlocks` (a language server's symbols). Sticky scroll pins the headers of the blocks scrolled out of sight, and `onScope` reports the named blocks around the caret for a breadcrumb.
|
|
25
|
-
- `overview.ts`: the ticks in the scroll track for the host's change marks (`setChangeMarks`, also drawn in the gutter) and the find matches.
|
|
26
|
-
- `find.ts`: the matches of the host's find bar. They are marked in the editor and never touch the selection until `endFind`.
|
|
27
|
-
- `semantic.ts` and `theme-scopes.ts`: what a language server classified, drawn over the grammar's colors. The host hands scopes (`setSemanticTokens`), and the theme's own rules decide the color (`scopeColors`, `shikiScopeColors`), so a class is the theme's class color. A scope the theme says nothing about keeps what the grammar made.
|
|
28
|
-
- `scroll.ts` and `scroll-animation.ts`: where a scroll goes (the platform's scrolling model: margins, and a jump a third from the top) and how it gets there, over up to a tenth of a second and never for a line or less.
|
|
29
|
-
- `keymap-table.ts`: the keys of every editor and language command for macOS and the other platforms, which the client prints in its menus too (`@adecore/editor/keymap`).
|
|
30
|
-
- `engine.ts`: the `Editor` contract over a model and a view.
|
|
31
|
-
|
|
32
|
-
## What a language feature gets
|
|
33
|
-
|
|
34
|
-
The editor knows no language server. It reports every change of the text as LSP content changes (`onTextChange`, with its source), says where the caret is (`getCaret`, `onCaret`) and where a character is on the screen (`rectAt`, in the page's pixels so a popup is right at any canvas zoom), and takes what a host draws or does: `setMarkers` (squiggles, faded and struck text, a lane in the scroll track), `setInlayHints`, `setSemanticTokens`, `setHighlights`, `setBlocks`, `setWidgets` (rows of the host's own DOM next to lines, drawn in the layout's block rows; `placement: 'above'` puts one over its line, and each owner has its own set, see below), `setCodeVision` (a quiet row above a declaration, see below), `setGhostText` (a suggestion after the caret, see below), `setGutterAction` (one button in the gutter, such as a lightbulb, which `onGutterAction` reports), `setGutterMarkers` (small buttons in the gutter per owner, such as the mark of a saved inline edit, which `onGutterMarker` reports by id; a marker stays on its line until its owner sets them again), `setLink` (a name underlined as a link while a modifier is held) `trackRange` (a range that follows the text through edits and reads null once an edit touched it, inside or across an end; text inserted exactly at an end does not count, so a range that was a selection still holds the same text) and `applyEdits`. A marker's `message` is what the tick of the problem says in the scroll track, where a press on the tick goes to the problem. `getSelection`, `getSelections`, `setSelections` (the last range is the primary one), `getVisibleRange` and `getIndentation` say what a feature asks about, and `setSelection` selects a range, such as the stop of a snippet. `onHover` reports the character under the pointer, `onClick` offers a press on a character first (Mod+click follows a name) and `onContextMenu` asks the host for its menu, which the editor does not draw, `onKeyDown` lets a host take a key before the editor does, and `onViewChange` says when the screen position of a character moved.
|
|
35
|
-
|
|
36
|
-
`setText` applies the difference as a line diff in one batch (`changedSpans`), one undo step reported as an external change, so a reload from disk or a sync between two editors on a file keeps the scroll, carets, folds, markers, inlays and code vision rows wherever the text around them stayed. A language server hears it as the one stretch from the first change to the last.
|
|
37
|
-
|
|
38
|
-
`runCommand` runs an editing command by name, which is how a menu or the palette reaches the editor; the folding commands (collapse and expand a region, all, recursively, a selection, documentation comments and "expand all to level 1 through 5") and column mode are the view's own.
|
|
39
|
-
|
|
40
|
-
A collapsed fold is one row for the line commands, as the platform has it: delete, duplicate, comment, move and a copy or cut with no selection take the whole fold (a moved fold stays collapsed on its new lines), a word move steps over it and add caret above and below skips it. The view hands the commands its rows through `lineSpan`.
|
|
8
|
+
**[Documentation with live demos](https://adecore.dev/editor/)**
|
|
41
9
|
|
|
42
|
-
|
|
10
|
+
## Install
|
|
43
11
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`setAttributionMarks` draws a three pixel bar in the gutter, where the change marks are, for each run of lines (`id`, one-based `startLine` and `endLine`, `color`). The color is a custom property of the page (`--agent-1` to `--agent-6`, `AGENT_COLORS`, defined in `editor.css` for both themes, with `--agent-ink` for text on them) or any CSS color. On a line that has both, the bar stands in the place of the git change mark, which the scroll track still shows; a removal triangle between lines stays. A run follows its text through edits until the host sets the marks again, and an edit costs one mapping of two offsets per run, so thousands of runs stay smooth. `onAttributionHover` reports the id and the bar's box on the line under the pointer (a few pixels either side of the bar count), and null when it leaves.
|
|
49
|
-
|
|
50
|
-
`setRemoteCursors` draws the caret of an agent (`id`, `position`, `name`, `color`, the same colors) over the text, two pixels wide with its name in a label above it, the label's lower left corner square. Where there is no room above the caret (the first line, or under the pinned headers) the name goes under it. It follows its text through edits until set again, is not interactive, and is left out on a line a fold hides. The editor's own caret and selection never move for it.
|
|
51
|
-
|
|
52
|
-
## Review and conflict rows
|
|
53
|
-
|
|
54
|
-
`setLineHighlights` tints lines behind the text (`startLine` and `endLine` one-based, a `color`, an optional `sign` such as `+` in the gutter beside the number, an optional `fill` that paints the rows as it stands instead of the 14% tint of the color), one box per row so a widget row between two tinted lines stays clear. It follows its text through edits like the marks do. `renderCode(container, text, options)` fills a widget row's element with lines that are not in the document, such as the lines an agent removed or the other side of a conflict, in the editor's face and token colors, with a gutter column as wide as the editor's, so the text lines up with the document's (`firstLine` for numbers, `sign`, `color` for the tint and the bar, `emphasis` for character ranges per line drawn in a stronger tint, such as the words a change replaced, and `faded` for text that is gone). Both take the same colors as the attribution marks. A widget has `placement: 'above'` for a row over its line. `setLineActions(actions, owner?)` puts the host's own DOM after the last character of a line (`id`, zero-based `line`, `render(container)` once per action), such as the buttons of a change under review. It is no row: it takes no height, follows its line through edits, is hidden where a fold hides the line, and a press on it never moves the caret. A long line with no wrap pushes it past the edge of the view.
|
|
55
|
-
|
|
56
|
-
## Widgets per owner
|
|
12
|
+
```sh
|
|
13
|
+
bun add @adecore/editor
|
|
14
|
+
```
|
|
57
15
|
|
|
58
|
-
|
|
16
|
+
## Set up
|
|
59
17
|
|
|
60
|
-
|
|
18
|
+
```css
|
|
19
|
+
@import '@adecore/ui/theme.css';
|
|
20
|
+
@import '@adecore/editor/editor.css';
|
|
21
|
+
```
|
|
61
22
|
|
|
62
|
-
|
|
23
|
+
```ts
|
|
24
|
+
import { createSmartEditorEngine, shikiTokenizers } from '@adecore/editor';
|
|
63
25
|
|
|
64
|
-
|
|
26
|
+
const engine = createSmartEditorEngine({ tokenizer: shikiTokenizers(getHighlighter) });
|
|
27
|
+
const editor = engine.mount(element, { text, language: 'typescript', theme: 'github-light' });
|
|
28
|
+
```
|
|
65
29
|
|
|
66
|
-
|
|
30
|
+
The element needs a position and a size from its parent.
|
|
67
31
|
|
|
68
|
-
##
|
|
32
|
+
## Entry points
|
|
69
33
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
34
|
+
| Import | What it holds |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `@adecore/editor` | `createSmartEditorEngine`, the `Editor` contract and its types, the Shiki adapters |
|
|
37
|
+
| `@adecore/editor/editor.css` | The styles |
|
|
38
|
+
| `@adecore/editor/shiki` | The Shiki adapters and grammar loading |
|
|
39
|
+
| `@adecore/editor/keymap` | The key table, `resolveKeymap`, `chordOf` and `parseChord` |
|
|
40
|
+
| `@adecore/editor/fake` | `FakeEditorEngine`, for tests of host code |
|
|
41
|
+
| `@adecore/editor/testing` | `mountEditor` on a LinkeDOM page |
|
|
75
42
|
|
|
76
|
-
##
|
|
43
|
+
## Documentation
|
|
77
44
|
|
|
78
|
-
|
|
45
|
+
| Page | What it covers |
|
|
46
|
+
|---|---|
|
|
47
|
+
| [Getting started](https://adecore.dev/editor/getting-started) | Install, styles, mounting and the engine options |
|
|
48
|
+
| [Options and keymaps](https://adecore.dev/editor/options) | Every option and setter, smart keys and the key table |
|
|
49
|
+
| [Text and events](https://adecore.dev/editor/editing) | Positions, edits, selections, events and tracked ranges |
|
|
50
|
+
| [Decorations](https://adecore.dev/editor/decorations), [Rows and widgets](https://adecore.dev/editor/rows), [Agents](https://adecore.dev/editor/agents) | What a host draws into the editor |
|
|
51
|
+
| [Find and folding](https://adecore.dev/editor/find-folding) | The find matcher, folds and sticky scroll |
|
|
52
|
+
| [Theme and syntax](https://adecore.dev/editor/theme) | CSS tokens, Shiki and a tokenizer of your own |
|
|
53
|
+
| [Testing](https://adecore.dev/editor/testing) | `mountEditor` and `FakeEditorEngine` |
|
|
79
54
|
|
|
80
|
-
|
|
55
|
+
## License
|
|
81
56
|
|
|
82
|
-
|
|
57
|
+
FSL-1.1-MIT
|
package/dist/controller.js
CHANGED
|
@@ -51,7 +51,6 @@ const VIEW_COMMANDS = new Set([
|
|
|
51
51
|
let lineClip = null;
|
|
52
52
|
/* Typing in the same stretch is one undo step until a pause this long. */
|
|
53
53
|
const TYPING_PAUSE_MS = 750;
|
|
54
|
-
const PAIR_CHARACTER = /^[()[\]{}'"`;<]$/;
|
|
55
54
|
/*
|
|
56
55
|
* What a person does to the editor: keys, typing, the clipboard, an input method, the mouse. It turns
|
|
57
56
|
* each into a command on the model and leaves the drawing to the view.
|
|
@@ -256,6 +255,7 @@ export class InputController {
|
|
|
256
255
|
smartEnter: keys.smartIndentOnEnter,
|
|
257
256
|
indentOnPaste: keys.indentOnPaste,
|
|
258
257
|
smartSemicolon: keys.smartSemicolon,
|
|
258
|
+
smartArrow: keys.smartArrow,
|
|
259
259
|
language: settings.language ?? 'plaintext',
|
|
260
260
|
camelCase: keys.camelHumps,
|
|
261
261
|
visualLine: (offset) => this.view.rowOf(offset),
|
|
@@ -369,7 +369,8 @@ export class InputController {
|
|
|
369
369
|
for (const selection of this.view.model.getSelections()) {
|
|
370
370
|
this.view.ensureVisible(selection.head);
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
// Every key goes through the model, which decides by the key before it too, as for the arrow of PHP.
|
|
373
|
+
if (text.length === 1) {
|
|
373
374
|
this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
|
|
374
375
|
this.view.revealCaret();
|
|
375
376
|
}
|
package/dist/editor.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--find-current-text: var(--text);
|
|
9
9
|
--editor-selection: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
10
10
|
--editor-occurrence: color-mix(in srgb, var(--text) 7%, transparent);
|
|
11
|
-
--editor-guide: color-mix(in srgb, var(--text)
|
|
11
|
+
--editor-guide: color-mix(in srgb, var(--text) 5%, transparent);
|
|
12
12
|
--editor-guide-active: color-mix(in srgb, var(--text) 24%, transparent);
|
|
13
13
|
--editor-margin: color-mix(in srgb, var(--text) 14%, transparent);
|
|
14
14
|
--editor-whitespace: color-mix(in srgb, var(--text) 30%, transparent);
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
--find-match: color-mix(in srgb, var(--status-needs-you) 24%, transparent);
|
|
30
30
|
--editor-selection: color-mix(in srgb, var(--accent) 28%, transparent);
|
|
31
31
|
--editor-occurrence: color-mix(in srgb, var(--text) 9%, transparent);
|
|
32
|
-
--editor-guide: color-mix(in srgb, var(--text)
|
|
32
|
+
--editor-guide: color-mix(in srgb, var(--text) 6%, transparent);
|
|
33
33
|
--editor-guide-active: color-mix(in srgb, var(--text) 28%, transparent);
|
|
34
34
|
--editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
|
|
35
35
|
--editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
|
|
@@ -728,13 +728,15 @@
|
|
|
728
728
|
|
|
729
729
|
/*
|
|
730
730
|
* A code vision row is quiet text one code line high above a declaration, in the declaration's
|
|
731
|
-
* indentation
|
|
731
|
+
* indentation, a step below the code as the platform draws it. Its entries stand a text size apart
|
|
732
|
+
* and light up as a link under the pointer.
|
|
732
733
|
*/
|
|
733
734
|
.se-block.se-lens {
|
|
734
735
|
display: flex;
|
|
735
736
|
align-items: center;
|
|
736
737
|
gap: var(--text-xs);
|
|
737
738
|
padding: 0;
|
|
739
|
+
font-size: max(12px, calc(var(--code-font-size, 13px) - 2px));
|
|
738
740
|
white-space: nowrap;
|
|
739
741
|
}
|
|
740
742
|
.se-lens-entry {
|
package/dist/engine.js
CHANGED
|
@@ -5,6 +5,7 @@ import { emit, subscribe } from './listeners.js';
|
|
|
5
5
|
import { fillLens, lensSignature } from './lens.js';
|
|
6
6
|
import { EditorView } from './view.js';
|
|
7
7
|
const DEFAULT_TAB_SIZE = 4;
|
|
8
|
+
const DEFAULT_LABEL = 'Code editor';
|
|
8
9
|
const DEFAULT_WIDGET_OWNER = 'default';
|
|
9
10
|
class SmartEditor {
|
|
10
11
|
model;
|
|
@@ -41,6 +42,7 @@ class SmartEditor {
|
|
|
41
42
|
wrap: options.wrap ?? false,
|
|
42
43
|
smartKeys: resolveSmartKeys(options.smartKeys),
|
|
43
44
|
messages: options.messages ?? {},
|
|
45
|
+
label: options.label ?? DEFAULT_LABEL,
|
|
44
46
|
guides: options.guides !== false,
|
|
45
47
|
whitespace: options.whitespace === true,
|
|
46
48
|
rightMargin: options.rightMargin ?? null,
|
|
@@ -522,6 +524,10 @@ class SmartEditor {
|
|
|
522
524
|
this.settings.foldOutline = outline;
|
|
523
525
|
this.view.applySettings();
|
|
524
526
|
}
|
|
527
|
+
setLabel(label) {
|
|
528
|
+
this.settings.label = label;
|
|
529
|
+
this.view.applySettings();
|
|
530
|
+
}
|
|
525
531
|
setFoldHints(hints) {
|
|
526
532
|
this.view.setFoldHints(hints === null
|
|
527
533
|
? null
|
|
@@ -531,11 +537,18 @@ class SmartEditor {
|
|
|
531
537
|
to: this.offsetAt(symbol.range.end),
|
|
532
538
|
body: symbol.body
|
|
533
539
|
})),
|
|
534
|
-
ranges: (hints.ranges ?? []).map((range) =>
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
540
|
+
ranges: (hints.ranges ?? []).map((range) => {
|
|
541
|
+
const first = this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1));
|
|
542
|
+
const last = this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1));
|
|
543
|
+
const within = (line, character) => Math.min(line.end, line.start + Math.max(0, character));
|
|
544
|
+
return {
|
|
545
|
+
from: first.start,
|
|
546
|
+
to: last.start,
|
|
547
|
+
...(range.kind === undefined ? {} : { kind: range.kind }),
|
|
548
|
+
...(range.startCharacter === undefined ? {} : { head: within(first, range.startCharacter) }),
|
|
549
|
+
...(range.endCharacter === undefined ? {} : { tail: within(last, range.endCharacter) })
|
|
550
|
+
};
|
|
551
|
+
})
|
|
539
552
|
});
|
|
540
553
|
}
|
|
541
554
|
setSmartKeys(keys) {
|
package/dist/fake.d.ts
CHANGED
|
@@ -111,6 +111,8 @@ export declare class FakeEditor implements Editor {
|
|
|
111
111
|
foldOutline: EditorFoldOutline;
|
|
112
112
|
foldHints: EditorFoldHints | null;
|
|
113
113
|
setFoldOutline(outline: EditorFoldOutline): void;
|
|
114
|
+
label: string;
|
|
115
|
+
setLabel(label: string): void;
|
|
114
116
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
115
117
|
setSmartKeys(keys: Partial<EditorSmartKeys>): void;
|
|
116
118
|
setIndentation(indentation: EditorIndentation): void;
|
package/dist/fake.js
CHANGED
|
@@ -43,6 +43,7 @@ export class FakeEditor {
|
|
|
43
43
|
this.theme = options.theme;
|
|
44
44
|
this.readOnly = options.readOnly ?? false;
|
|
45
45
|
this.readOnlyReason = options.readOnlyReason;
|
|
46
|
+
this.label = options.label ?? 'Code editor';
|
|
46
47
|
this.revealedLine = options.line ?? null;
|
|
47
48
|
this.column = options.column ?? null;
|
|
48
49
|
this.scrollTop = options.scrollTop ?? null;
|
|
@@ -372,6 +373,10 @@ export class FakeEditor {
|
|
|
372
373
|
setFoldOutline(outline) {
|
|
373
374
|
this.foldOutline = outline;
|
|
374
375
|
}
|
|
376
|
+
label;
|
|
377
|
+
setLabel(label) {
|
|
378
|
+
this.label = label;
|
|
379
|
+
}
|
|
375
380
|
setFoldHints(hints) {
|
|
376
381
|
this.foldHints = hints;
|
|
377
382
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { AGENT_COLORS } from './attribution.ts';
|
|
|
2
2
|
export { createSmartEditorEngine } from './engine.ts';
|
|
3
3
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
4
4
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
5
|
-
export type { Editor, EditorAttributionHover, EditorAttributionMark, EditorMarkColor, EditorBlock, EditorChangeKind, EditorChangeMark, EditorCodeVision, EditorCodeVisionEntry, EditorClick, EditorCodeBlockOptions, EditorClickHandler, EditorCommand, EditorContextMenu, EditorEngine, EditorFindQuery, EditorFoldHints, EditorFoldOutline, EditorFoldRange, EditorFoldSymbol, EditorFolds, EditorFindState, EditorReplaceOptions, EditorGutterAction, EditorGutterMarker, EditorIndentation, EditorContentChange, EditorHighlight, EditorHighlightKind, EditorHover, EditorGhostText, EditorInlayHint, EditorKeyHandler, EditorLineAction, EditorLineHighlight, EditorMarker, EditorMessages, EditorMarkerSeverity, EditorReveal, EditorRect, EditorSmartKeys, EditorOptions, EditorPosition, EditorRemoteCursor, EditorRange, EditorTextChange, EditorTrackedRange, EditorRunCommand, EditorSelectionRanges, EditorTheme, EditorViewCommand, EditorWidget, FoldRole, KeyChord, LineToken, ScopeColorSource, ScopeColors, ScopeStyle, EditorSemanticToken, LineTokenizer, SmartEditorEngineOptions, TokenizedLine, TokenizerSource } from './types.ts';
|
|
5
|
+
export type { Editor, EditorAttributionHover, EditorAttributionMark, EditorMarkColor, EditorBlock, EditorChangeKind, EditorChangeMark, EditorCodeVision, EditorCodeVisionEntry, EditorClick, EditorCodeBlockOptions, EditorClickHandler, EditorCommand, EditorContextMenu, EditorEngine, EditorFindQuery, EditorFoldHints, EditorFoldRangeHint, EditorFoldOutline, EditorFoldRange, EditorFoldSymbol, EditorFolds, EditorFindState, EditorReplaceOptions, EditorGutterAction, EditorGutterMarker, EditorIndentation, EditorContentChange, EditorHighlight, EditorHighlightKind, EditorHover, EditorGhostText, EditorInlayHint, EditorKeyHandler, EditorLineAction, EditorLineHighlight, EditorMarker, EditorMessages, EditorMarkerSeverity, EditorReveal, EditorRect, EditorSmartKeys, EditorOptions, EditorPosition, EditorRemoteCursor, EditorRange, EditorTextChange, EditorTrackedRange, EditorRunCommand, EditorSelectionRanges, EditorTheme, EditorViewCommand, EditorWidget, FoldRole, KeyChord, LineToken, ScopeColorSource, ScopeColors, ScopeStyle, EditorSemanticToken, LineTokenizer, SmartEditorEngineOptions, TokenizedLine, TokenizerSource } from './types.ts';
|
package/dist/layout.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface FoldState {
|
|
|
40
40
|
endLine: number;
|
|
41
41
|
collapsed: boolean;
|
|
42
42
|
closer?: number;
|
|
43
|
+
head?: number;
|
|
43
44
|
}
|
|
44
45
|
interface RowBase {
|
|
45
46
|
key: string;
|
|
@@ -52,6 +53,7 @@ export interface TextRow extends RowBase {
|
|
|
52
53
|
kind: 'text';
|
|
53
54
|
lastLine: number;
|
|
54
55
|
tail: number | null;
|
|
56
|
+
head: number | null;
|
|
55
57
|
subRows: number;
|
|
56
58
|
}
|
|
57
59
|
export interface BlockRow extends RowBase {
|
|
@@ -139,6 +141,7 @@ export declare class EditorLayout {
|
|
|
139
141
|
indentX(line: number): number;
|
|
140
142
|
rowForLine(line: number): LayoutRow;
|
|
141
143
|
isHidden(line: number): boolean;
|
|
144
|
+
hides(offset: number): boolean;
|
|
142
145
|
visibleRows(top: number, height: number, overscan?: number): LayoutRow[];
|
|
143
146
|
geometry(row: TextRow): LineGeometry;
|
|
144
147
|
private stopOf;
|
package/dist/layout.js
CHANGED
|
@@ -278,6 +278,7 @@ export class EditorLayout {
|
|
|
278
278
|
const fold = folds.get(line);
|
|
279
279
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
280
280
|
const tail = fold?.closer !== undefined && last > line ? this.tailOf(fold.closer, last) : null;
|
|
281
|
+
const head = fold?.head !== undefined && last > line ? this.tailOf(fold.head, line) : null;
|
|
281
282
|
const index = this.rows.length;
|
|
282
283
|
const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
|
|
283
284
|
const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
|
|
@@ -288,6 +289,7 @@ export class EditorLayout {
|
|
|
288
289
|
line,
|
|
289
290
|
lastLine: last,
|
|
290
291
|
tail,
|
|
292
|
+
head,
|
|
291
293
|
subRows,
|
|
292
294
|
top: 0,
|
|
293
295
|
height: subRows * this.metrics.lineHeight
|
|
@@ -302,7 +304,7 @@ export class EditorLayout {
|
|
|
302
304
|
}
|
|
303
305
|
this.reflow();
|
|
304
306
|
}
|
|
305
|
-
/*
|
|
307
|
+
/* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
|
|
306
308
|
tailOf(closer, line) {
|
|
307
309
|
const bounds = this.document.getLine(line);
|
|
308
310
|
return Math.min(bounds.end, Math.max(bounds.start, closer));
|
|
@@ -382,6 +384,12 @@ export class EditorLayout {
|
|
|
382
384
|
const row = this.rowForLine(line);
|
|
383
385
|
return row.kind === 'text' && line > row.line && line <= row.lastLine;
|
|
384
386
|
}
|
|
387
|
+
/* Whether a fold hides the offset: on a line it hides, or past where it starts on its first line. */
|
|
388
|
+
hides(offset) {
|
|
389
|
+
const line = this.document.positionAt(offset).line;
|
|
390
|
+
const row = this.rowForLine(line);
|
|
391
|
+
return row.kind === 'text' && (this.isHidden(line) || (row.head !== null && line === row.line && offset > row.head));
|
|
392
|
+
}
|
|
385
393
|
visibleRows(top, height, overscan = 120) {
|
|
386
394
|
const result = [];
|
|
387
395
|
for (let index = this.rowAt(top - overscan).index; index < this.rows.length && this.rows[index].top < top + height + overscan; index++) {
|
|
@@ -390,6 +398,12 @@ export class EditorLayout {
|
|
|
390
398
|
return result;
|
|
391
399
|
}
|
|
392
400
|
geometry(row) {
|
|
401
|
+
if (row.head !== null) {
|
|
402
|
+
// The first line of a fold that starts within it, laid out up to there; seldom drawn, so never kept.
|
|
403
|
+
const line = this.document.getLine(row.line);
|
|
404
|
+
const shown = { ...line, end: row.head, text: line.text.slice(0, row.head - line.start) };
|
|
405
|
+
return scanLine(shown, this.metrics, this.wrapWidth, (this.inlaysByLine.get(row.line) ?? []).filter((inlay) => inlay.at <= row.head));
|
|
406
|
+
}
|
|
393
407
|
const cached = this.geometryCache.get(row.line);
|
|
394
408
|
if (cached) {
|
|
395
409
|
return cached;
|
package/dist/smart-keys.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface EditorOptions {
|
|
|
28
28
|
readonly rightMargin?: number | null;
|
|
29
29
|
readonly foldOutline?: EditorFoldOutline;
|
|
30
30
|
readonly messages?: Partial<EditorMessages>;
|
|
31
|
+
readonly label?: string;
|
|
31
32
|
readonly folds?: EditorFolds;
|
|
32
33
|
readonly foldDefaults?: readonly FoldRole[];
|
|
33
34
|
readonly line?: number;
|
|
@@ -45,17 +46,21 @@ export interface EditorSmartKeys {
|
|
|
45
46
|
readonly smartIndentOnEnter: boolean;
|
|
46
47
|
readonly indentOnPaste: boolean;
|
|
47
48
|
readonly smartSemicolon: boolean;
|
|
49
|
+
readonly smartArrow: boolean;
|
|
48
50
|
readonly camelHumps: boolean;
|
|
49
51
|
}
|
|
50
52
|
export interface EditorFoldSymbol {
|
|
51
53
|
readonly range: EditorRange;
|
|
52
54
|
readonly body: 'function' | 'method' | 'class' | 'value';
|
|
53
55
|
}
|
|
56
|
+
export interface EditorFoldRangeHint extends EditorFoldRange {
|
|
57
|
+
readonly kind?: string;
|
|
58
|
+
readonly startCharacter?: number;
|
|
59
|
+
readonly endCharacter?: number;
|
|
60
|
+
}
|
|
54
61
|
export interface EditorFoldHints {
|
|
55
62
|
readonly symbols?: readonly EditorFoldSymbol[];
|
|
56
|
-
readonly ranges?: readonly
|
|
57
|
-
readonly kind?: string;
|
|
58
|
-
})[];
|
|
63
|
+
readonly ranges?: readonly EditorFoldRangeHint[];
|
|
59
64
|
}
|
|
60
65
|
export interface EditorBlock {
|
|
61
66
|
readonly startLine: number;
|
|
@@ -258,6 +263,7 @@ export interface Editor {
|
|
|
258
263
|
setWhitespace(whitespace: boolean): void;
|
|
259
264
|
setRightMargin(column: number | null): void;
|
|
260
265
|
setFoldOutline(outline: EditorFoldOutline): void;
|
|
266
|
+
setLabel(label: string): void;
|
|
261
267
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
262
268
|
setChangeMarks(marks: readonly EditorChangeMark[]): void;
|
|
263
269
|
setBlocks(blocks: readonly EditorBlock[] | null): void;
|
package/dist/view.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ViewSettings {
|
|
|
13
13
|
wrap: boolean;
|
|
14
14
|
smartKeys: EditorSmartKeys;
|
|
15
15
|
messages: Partial<EditorMessages>;
|
|
16
|
+
label: string;
|
|
16
17
|
guides: boolean;
|
|
17
18
|
whitespace: boolean;
|
|
18
19
|
rightMargin: number | null;
|
|
@@ -275,6 +276,7 @@ export declare class EditorView {
|
|
|
275
276
|
private effectiveIndent;
|
|
276
277
|
private activeGuide;
|
|
277
278
|
private scopeGuide;
|
|
279
|
+
private caretOnGuide;
|
|
278
280
|
private guideRects;
|
|
279
281
|
selectionAtPoint(clientX: number, clientY: number): EditorSelection | null;
|
|
280
282
|
setColumnMode(on: boolean): void;
|
package/dist/view.js
CHANGED
|
@@ -208,7 +208,6 @@ export class EditorView {
|
|
|
208
208
|
this.input.setAttribute(name, 'off');
|
|
209
209
|
}
|
|
210
210
|
this.input.setAttribute('aria-multiline', 'true');
|
|
211
|
-
this.input.setAttribute('aria-label', 'Code editor');
|
|
212
211
|
this.sticky.setAttribute('aria-hidden', 'true');
|
|
213
212
|
this.sticky.hidden = true;
|
|
214
213
|
this.overview = make('div', 'se-overview');
|
|
@@ -252,6 +251,7 @@ export class EditorView {
|
|
|
252
251
|
/* Settings that are read off the page or off `settings` are applied again: after a change to either. */
|
|
253
252
|
applySettings() {
|
|
254
253
|
this.input.readOnly = this.settings.readOnly;
|
|
254
|
+
this.input.setAttribute('aria-label', this.settings.label);
|
|
255
255
|
this.root.dataset.readonly = String(this.settings.readOnly);
|
|
256
256
|
this.root.dataset.wrap = String(this.settings.wrap);
|
|
257
257
|
this.root.dataset.foldOutline = this.settings.foldOutline;
|
|
@@ -446,7 +446,13 @@ export class EditorView {
|
|
|
446
446
|
if (this.foldHints !== null) {
|
|
447
447
|
this.foldHints = {
|
|
448
448
|
symbols: this.foldHints.symbols?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? [],
|
|
449
|
-
ranges: this.foldHints.ranges?.map((hint) => ({
|
|
449
|
+
ranges: this.foldHints.ranges?.map((hint) => ({
|
|
450
|
+
...hint,
|
|
451
|
+
from: mapOffset(hint.from, changes),
|
|
452
|
+
to: mapOffset(hint.to, changes),
|
|
453
|
+
...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
|
|
454
|
+
...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
|
|
455
|
+
})) ?? []
|
|
450
456
|
};
|
|
451
457
|
}
|
|
452
458
|
this.customFolds = this.customFolds.map((fold) => ({ from: mapOffset(fold.from, changes), to: mapOffset(fold.to, changes) }));
|
|
@@ -583,12 +589,16 @@ export class EditorView {
|
|
|
583
589
|
this.render();
|
|
584
590
|
}
|
|
585
591
|
foldStates() {
|
|
586
|
-
return this.foldRanges.map((range) =>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
+
return this.foldRanges.map((range) => {
|
|
593
|
+
const collapsed = this.collapsed.has(range.from);
|
|
594
|
+
return {
|
|
595
|
+
startLine: range.startLine,
|
|
596
|
+
endLine: range.endLine,
|
|
597
|
+
collapsed,
|
|
598
|
+
closer: collapsed ? (range.tail ?? this.closerOf(range)) : undefined,
|
|
599
|
+
...(collapsed && range.head !== undefined ? { head: range.head } : {})
|
|
600
|
+
};
|
|
601
|
+
});
|
|
592
602
|
}
|
|
593
603
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
594
604
|
scheduleFolds() {
|
|
@@ -875,14 +885,14 @@ export class EditorView {
|
|
|
875
885
|
this.moveHiddenCarets();
|
|
876
886
|
this.render();
|
|
877
887
|
}
|
|
878
|
-
/* A caret in
|
|
888
|
+
/* A caret in what a fold hides would be typing where nobody can see; it goes to where the fold's first line stops. */
|
|
879
889
|
moveHiddenCarets() {
|
|
880
890
|
const visible = (offset) => {
|
|
881
|
-
|
|
882
|
-
if (!this.layout.isHidden(line)) {
|
|
891
|
+
if (!this.layout.hides(offset)) {
|
|
883
892
|
return offset;
|
|
884
893
|
}
|
|
885
|
-
|
|
894
|
+
const row = this.layout.rowForLine(this.model.positionAt(offset).line);
|
|
895
|
+
return row.kind === 'text' && row.head !== null ? row.head : this.model.getLine(row.line).end;
|
|
886
896
|
};
|
|
887
897
|
const selections = this.model.getSelections();
|
|
888
898
|
const moved = selections.map((selection) => ({ anchor: visible(selection.anchor), head: visible(selection.head) }));
|
|
@@ -893,11 +903,11 @@ export class EditorView {
|
|
|
893
903
|
/* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
|
|
894
904
|
ensureVisible(offset) {
|
|
895
905
|
const line = this.model.positionAt(offset).line;
|
|
896
|
-
if (!this.layout.
|
|
906
|
+
if (!this.layout.hides(offset)) {
|
|
897
907
|
return;
|
|
898
908
|
}
|
|
899
909
|
for (const range of this.foldRanges) {
|
|
900
|
-
if (range.startLine < line && range.endLine >= line) {
|
|
910
|
+
if ((range.startLine < line && range.endLine >= line) || (range.startLine === line && range.head !== undefined && offset > range.head)) {
|
|
901
911
|
this.collapsed.delete(range.from);
|
|
902
912
|
this.settled.add(range.from);
|
|
903
913
|
}
|
|
@@ -1178,11 +1188,24 @@ export class EditorView {
|
|
|
1178
1188
|
const last = inside ? reaches(line, 1) : next > indent ? reaches(line + 1, 1) : line - 1;
|
|
1179
1189
|
return { column, first, last };
|
|
1180
1190
|
}
|
|
1181
|
-
/*
|
|
1191
|
+
/* The line and the indentation column of a caret in the whitespace that leads its line, past the first column, where it can stand on a guide. */
|
|
1192
|
+
caretOnGuide() {
|
|
1193
|
+
const position = this.model.positionAt(this.model.getPrimary().head);
|
|
1194
|
+
const lead = this.model.getLine(position.line).text.slice(0, position.column);
|
|
1195
|
+
if (position.column === 0 || !/^[\t ]*$/.test(lead)) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
return { line: position.line, column: indentationColumn(lead, this.settings.tabSize) };
|
|
1199
|
+
}
|
|
1200
|
+
/*
|
|
1201
|
+
* A line at each indentation level of the rows on screen, joined where the lines go on. Apart from them stand the one
|
|
1202
|
+
* of the scope around the caret, and as the platform does, the one the caret stands on.
|
|
1203
|
+
*/
|
|
1182
1204
|
guideRects(rows) {
|
|
1183
1205
|
const size = this.settings.tabSize;
|
|
1184
1206
|
const charWidth = this.layout.metrics.charWidth;
|
|
1185
1207
|
const active = this.activeGuide();
|
|
1208
|
+
const caret = this.caretOnGuide();
|
|
1186
1209
|
const plain = [];
|
|
1187
1210
|
const marked = [];
|
|
1188
1211
|
const open = new Map();
|
|
@@ -1190,7 +1213,9 @@ export class EditorView {
|
|
|
1190
1213
|
const segment = open.get(column);
|
|
1191
1214
|
open.delete(column);
|
|
1192
1215
|
const rect = { x: column * charWidth, y: segment.top, width: 1, height: segment.bottom - segment.top };
|
|
1193
|
-
|
|
1216
|
+
const scope = active !== null && active.column === column && segment.last >= active.first && segment.first <= active.last;
|
|
1217
|
+
const under = caret !== null && caret.column === column && segment.first <= caret.line && caret.line <= segment.last;
|
|
1218
|
+
(scope || under ? marked : plain).push(rect);
|
|
1194
1219
|
};
|
|
1195
1220
|
for (const row of rows) {
|
|
1196
1221
|
if (row.kind !== 'text') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor",
|
|
3
|
-
"version": "0.17.0
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"test": "bun test --conditions=source"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@adecore/editor-core": "0.17.0
|
|
42
|
+
"@adecore/editor-core": "0.17.0",
|
|
43
43
|
"shiki": "^4.4.3",
|
|
44
44
|
"linkedom": "^0.18.13"
|
|
45
45
|
},
|
package/src/controller.ts
CHANGED
|
@@ -77,7 +77,6 @@ let lineClip: string | null = null;
|
|
|
77
77
|
|
|
78
78
|
/* Typing in the same stretch is one undo step until a pause this long. */
|
|
79
79
|
const TYPING_PAUSE_MS = 750;
|
|
80
|
-
const PAIR_CHARACTER = /^[()[\]{}'"`;<]$/;
|
|
81
80
|
|
|
82
81
|
/*
|
|
83
82
|
* What a person does to the editor: keys, typing, the clipboard, an input method, the mouse. It turns
|
|
@@ -293,6 +292,7 @@ export class InputController {
|
|
|
293
292
|
smartEnter: keys.smartIndentOnEnter,
|
|
294
293
|
indentOnPaste: keys.indentOnPaste,
|
|
295
294
|
smartSemicolon: keys.smartSemicolon,
|
|
295
|
+
smartArrow: keys.smartArrow,
|
|
296
296
|
language: settings.language ?? 'plaintext',
|
|
297
297
|
camelCase: keys.camelHumps,
|
|
298
298
|
visualLine: (offset) => this.view.rowOf(offset),
|
|
@@ -415,7 +415,8 @@ export class InputController {
|
|
|
415
415
|
for (const selection of this.view.model.getSelections()) {
|
|
416
416
|
this.view.ensureVisible(selection.head);
|
|
417
417
|
}
|
|
418
|
-
|
|
418
|
+
// Every key goes through the model, which decides by the key before it too, as for the arrow of PHP.
|
|
419
|
+
if (text.length === 1) {
|
|
419
420
|
this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
|
|
420
421
|
this.view.revealCaret();
|
|
421
422
|
} else {
|
package/src/editor.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--find-current-text: var(--text);
|
|
9
9
|
--editor-selection: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
10
10
|
--editor-occurrence: color-mix(in srgb, var(--text) 7%, transparent);
|
|
11
|
-
--editor-guide: color-mix(in srgb, var(--text)
|
|
11
|
+
--editor-guide: color-mix(in srgb, var(--text) 5%, transparent);
|
|
12
12
|
--editor-guide-active: color-mix(in srgb, var(--text) 24%, transparent);
|
|
13
13
|
--editor-margin: color-mix(in srgb, var(--text) 14%, transparent);
|
|
14
14
|
--editor-whitespace: color-mix(in srgb, var(--text) 30%, transparent);
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
--find-match: color-mix(in srgb, var(--status-needs-you) 24%, transparent);
|
|
30
30
|
--editor-selection: color-mix(in srgb, var(--accent) 28%, transparent);
|
|
31
31
|
--editor-occurrence: color-mix(in srgb, var(--text) 9%, transparent);
|
|
32
|
-
--editor-guide: color-mix(in srgb, var(--text)
|
|
32
|
+
--editor-guide: color-mix(in srgb, var(--text) 6%, transparent);
|
|
33
33
|
--editor-guide-active: color-mix(in srgb, var(--text) 28%, transparent);
|
|
34
34
|
--editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
|
|
35
35
|
--editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
|
|
@@ -728,13 +728,15 @@
|
|
|
728
728
|
|
|
729
729
|
/*
|
|
730
730
|
* A code vision row is quiet text one code line high above a declaration, in the declaration's
|
|
731
|
-
* indentation
|
|
731
|
+
* indentation, a step below the code as the platform draws it. Its entries stand a text size apart
|
|
732
|
+
* and light up as a link under the pointer.
|
|
732
733
|
*/
|
|
733
734
|
.se-block.se-lens {
|
|
734
735
|
display: flex;
|
|
735
736
|
align-items: center;
|
|
736
737
|
gap: var(--text-xs);
|
|
737
738
|
padding: 0;
|
|
739
|
+
font-size: max(12px, calc(var(--code-font-size, 13px) - 2px));
|
|
738
740
|
white-space: nowrap;
|
|
739
741
|
}
|
|
740
742
|
.se-lens-entry {
|
package/src/engine.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { fillLens, lensSignature } from './lens.ts';
|
|
|
51
51
|
import { EditorView, type ViewSettings } from './view.ts';
|
|
52
52
|
|
|
53
53
|
const DEFAULT_TAB_SIZE = 4;
|
|
54
|
+
const DEFAULT_LABEL = 'Code editor';
|
|
54
55
|
const DEFAULT_WIDGET_OWNER = 'default';
|
|
55
56
|
|
|
56
57
|
class SmartEditor implements Editor {
|
|
@@ -90,6 +91,7 @@ class SmartEditor implements Editor {
|
|
|
90
91
|
wrap: options.wrap ?? false,
|
|
91
92
|
smartKeys: resolveSmartKeys(options.smartKeys),
|
|
92
93
|
messages: options.messages ?? {},
|
|
94
|
+
label: options.label ?? DEFAULT_LABEL,
|
|
93
95
|
guides: options.guides !== false,
|
|
94
96
|
whitespace: options.whitespace === true,
|
|
95
97
|
rightMargin: options.rightMargin ?? null,
|
|
@@ -659,6 +661,11 @@ class SmartEditor implements Editor {
|
|
|
659
661
|
this.view.applySettings();
|
|
660
662
|
}
|
|
661
663
|
|
|
664
|
+
setLabel(label: string): void {
|
|
665
|
+
this.settings.label = label;
|
|
666
|
+
this.view.applySettings();
|
|
667
|
+
}
|
|
668
|
+
|
|
662
669
|
setFoldHints(hints: EditorFoldHints | null): void {
|
|
663
670
|
this.view.setFoldHints(
|
|
664
671
|
hints === null
|
|
@@ -669,11 +676,19 @@ class SmartEditor implements Editor {
|
|
|
669
676
|
to: this.offsetAt(symbol.range.end),
|
|
670
677
|
body: symbol.body
|
|
671
678
|
})),
|
|
672
|
-
ranges: (hints.ranges ?? []).map((range) =>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
679
|
+
ranges: (hints.ranges ?? []).map((range) => {
|
|
680
|
+
const first = this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1));
|
|
681
|
+
const last = this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1));
|
|
682
|
+
const within = (line: { start: number; end: number }, character: number): number =>
|
|
683
|
+
Math.min(line.end, line.start + Math.max(0, character));
|
|
684
|
+
return {
|
|
685
|
+
from: first.start,
|
|
686
|
+
to: last.start,
|
|
687
|
+
...(range.kind === undefined ? {} : { kind: range.kind }),
|
|
688
|
+
...(range.startCharacter === undefined ? {} : { head: within(first, range.startCharacter) }),
|
|
689
|
+
...(range.endCharacter === undefined ? {} : { tail: within(last, range.endCharacter) })
|
|
690
|
+
};
|
|
691
|
+
})
|
|
677
692
|
}
|
|
678
693
|
);
|
|
679
694
|
}
|
package/src/fake.ts
CHANGED
|
@@ -91,6 +91,7 @@ export class FakeEditor implements Editor {
|
|
|
91
91
|
this.theme = options.theme;
|
|
92
92
|
this.readOnly = options.readOnly ?? false;
|
|
93
93
|
this.readOnlyReason = options.readOnlyReason;
|
|
94
|
+
this.label = options.label ?? 'Code editor';
|
|
94
95
|
this.revealedLine = options.line ?? null;
|
|
95
96
|
this.column = options.column ?? null;
|
|
96
97
|
this.scrollTop = options.scrollTop ?? null;
|
|
@@ -487,6 +488,12 @@ export class FakeEditor implements Editor {
|
|
|
487
488
|
this.foldOutline = outline;
|
|
488
489
|
}
|
|
489
490
|
|
|
491
|
+
label: string;
|
|
492
|
+
|
|
493
|
+
setLabel(label: string): void {
|
|
494
|
+
this.label = label;
|
|
495
|
+
}
|
|
496
|
+
|
|
490
497
|
setFoldHints(hints: EditorFoldHints | null): void {
|
|
491
498
|
this.foldHints = hints;
|
|
492
499
|
}
|
package/src/index.ts
CHANGED
package/src/layout.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface FoldState {
|
|
|
49
49
|
collapsed: boolean;
|
|
50
50
|
/* Where the closing delimiter starts on `endLine`; a collapsed fold keeps it drawn after its placeholder. */
|
|
51
51
|
closer?: number;
|
|
52
|
+
/* Where a collapsed fold starts hiding on `startLine`; without it the whole first line stays drawn. */
|
|
53
|
+
head?: number;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
interface RowBase {
|
|
@@ -66,6 +68,8 @@ export interface TextRow extends RowBase {
|
|
|
66
68
|
lastLine: number;
|
|
67
69
|
/* The offset on `lastLine` from which a collapsed fold draws what stays visible after its placeholder. */
|
|
68
70
|
tail: number | null;
|
|
71
|
+
/* The offset on `line` up to which a collapsed fold draws its first line; null when it draws the whole line. */
|
|
72
|
+
head: number | null;
|
|
69
73
|
/* Visual lines, which is more than one for a wrapped line. */
|
|
70
74
|
subRows: number;
|
|
71
75
|
}
|
|
@@ -429,6 +433,7 @@ export class EditorLayout {
|
|
|
429
433
|
const fold = folds.get(line);
|
|
430
434
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
431
435
|
const tail = fold?.closer !== undefined && last > line ? this.tailOf(fold.closer, last) : null;
|
|
436
|
+
const head = fold?.head !== undefined && last > line ? this.tailOf(fold.head, line) : null;
|
|
432
437
|
const index = this.rows.length;
|
|
433
438
|
const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
|
|
434
439
|
const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
|
|
@@ -439,6 +444,7 @@ export class EditorLayout {
|
|
|
439
444
|
line,
|
|
440
445
|
lastLine: last,
|
|
441
446
|
tail,
|
|
447
|
+
head,
|
|
442
448
|
subRows,
|
|
443
449
|
top: 0,
|
|
444
450
|
height: subRows * this.metrics.lineHeight
|
|
@@ -454,7 +460,7 @@ export class EditorLayout {
|
|
|
454
460
|
this.reflow();
|
|
455
461
|
}
|
|
456
462
|
|
|
457
|
-
/*
|
|
463
|
+
/* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
|
|
458
464
|
private tailOf(closer: number, line: number): number {
|
|
459
465
|
const bounds = this.document.getLine(line);
|
|
460
466
|
return Math.min(bounds.end, Math.max(bounds.start, closer));
|
|
@@ -541,6 +547,13 @@ export class EditorLayout {
|
|
|
541
547
|
return row.kind === 'text' && line > row.line && line <= row.lastLine;
|
|
542
548
|
}
|
|
543
549
|
|
|
550
|
+
/* Whether a fold hides the offset: on a line it hides, or past where it starts on its first line. */
|
|
551
|
+
hides(offset: number): boolean {
|
|
552
|
+
const line = this.document.positionAt(offset).line;
|
|
553
|
+
const row = this.rowForLine(line);
|
|
554
|
+
return row.kind === 'text' && (this.isHidden(line) || (row.head !== null && line === row.line && offset > row.head));
|
|
555
|
+
}
|
|
556
|
+
|
|
544
557
|
visibleRows(top: number, height: number, overscan = 120): LayoutRow[] {
|
|
545
558
|
const result: LayoutRow[] = [];
|
|
546
559
|
for (let index = this.rowAt(top - overscan).index; index < this.rows.length && this.rows[index]!.top < top + height + overscan; index++) {
|
|
@@ -550,6 +563,17 @@ export class EditorLayout {
|
|
|
550
563
|
}
|
|
551
564
|
|
|
552
565
|
geometry(row: TextRow): LineGeometry {
|
|
566
|
+
if (row.head !== null) {
|
|
567
|
+
// The first line of a fold that starts within it, laid out up to there; seldom drawn, so never kept.
|
|
568
|
+
const line = this.document.getLine(row.line);
|
|
569
|
+
const shown = { ...line, end: row.head, text: line.text.slice(0, row.head - line.start) };
|
|
570
|
+
return scanLine(
|
|
571
|
+
shown,
|
|
572
|
+
this.metrics,
|
|
573
|
+
this.wrapWidth,
|
|
574
|
+
(this.inlaysByLine.get(row.line) ?? []).filter((inlay) => inlay.at <= row.head!)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
553
577
|
const cached = this.geometryCache.get(row.line);
|
|
554
578
|
if (cached) {
|
|
555
579
|
return cached;
|
package/src/smart-keys.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface EditorOptions {
|
|
|
68
68
|
readonly foldOutline?: EditorFoldOutline;
|
|
69
69
|
/* What the editor says to a person itself, in the host's words. */
|
|
70
70
|
readonly messages?: Partial<EditorMessages>;
|
|
71
|
+
/* The accessible name of the editor, such as "Commit message", in the host's language; `Code editor` without one. */
|
|
72
|
+
readonly label?: string;
|
|
71
73
|
/* The folds a host kept when the file was last open. */
|
|
72
74
|
readonly folds?: EditorFolds;
|
|
73
75
|
/* Without remembered folds, the folds of these roles fold, as they do the first time a file opens, and again as a language server names more of them. */
|
|
@@ -101,6 +103,8 @@ export interface EditorSmartKeys {
|
|
|
101
103
|
readonly indentOnPaste: boolean;
|
|
102
104
|
/* A `;` typed inside a call goes to the end of the statement. */
|
|
103
105
|
readonly smartSemicolon: boolean;
|
|
106
|
+
/* In PHP a `-` typed after a variable, a member or a `)` or `]` becomes `->`. */
|
|
107
|
+
readonly smartArrow: boolean;
|
|
104
108
|
/* Moving by word also stops inside `camelCase` and `snake_case` words. Off. */
|
|
105
109
|
readonly camelHumps: boolean;
|
|
106
110
|
}
|
|
@@ -112,9 +116,16 @@ export interface EditorFoldSymbol {
|
|
|
112
116
|
readonly body: 'function' | 'method' | 'class' | 'value';
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
/* A range a server folds; with characters, a collapsed fold hides from `startCharacter` on its first line up to `endCharacter` on its last, as LSP says. */
|
|
120
|
+
export interface EditorFoldRangeHint extends EditorFoldRange {
|
|
121
|
+
readonly kind?: string;
|
|
122
|
+
readonly startCharacter?: number;
|
|
123
|
+
readonly endCharacter?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
export interface EditorFoldHints {
|
|
116
127
|
readonly symbols?: readonly EditorFoldSymbol[];
|
|
117
|
-
readonly ranges?: readonly
|
|
128
|
+
readonly ranges?: readonly EditorFoldRangeHint[];
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
/* A part of the document with a header line, such as a function, a class or a method. */
|
|
@@ -491,6 +502,7 @@ export interface Editor {
|
|
|
491
502
|
/* The column to draw a line at; null takes it away. */
|
|
492
503
|
setRightMargin(column: number | null): void;
|
|
493
504
|
setFoldOutline(outline: EditorFoldOutline): void;
|
|
505
|
+
setLabel(label: string): void;
|
|
494
506
|
/* What a language server knows about the folds: the bodies of symbols and the ranges it folds. They follow their text through edits until set again; null forgets them. */
|
|
495
507
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
496
508
|
/* Marks that follow their lines through edits until the host sets them again. */
|
package/src/view.ts
CHANGED
|
@@ -67,6 +67,7 @@ export interface ViewSettings {
|
|
|
67
67
|
wrap: boolean;
|
|
68
68
|
smartKeys: EditorSmartKeys;
|
|
69
69
|
messages: Partial<EditorMessages>;
|
|
70
|
+
label: string;
|
|
70
71
|
/* A line at each indentation level, and the one of the scope around the caret stronger. */
|
|
71
72
|
guides: boolean;
|
|
72
73
|
/* Spaces drawn as dots and tabs as arrows. */
|
|
@@ -299,7 +300,6 @@ export class EditorView {
|
|
|
299
300
|
this.input.setAttribute(name, 'off');
|
|
300
301
|
}
|
|
301
302
|
this.input.setAttribute('aria-multiline', 'true');
|
|
302
|
-
this.input.setAttribute('aria-label', 'Code editor');
|
|
303
303
|
this.sticky.setAttribute('aria-hidden', 'true');
|
|
304
304
|
this.sticky.hidden = true;
|
|
305
305
|
this.overview = make('div', 'se-overview');
|
|
@@ -345,6 +345,7 @@ export class EditorView {
|
|
|
345
345
|
/* Settings that are read off the page or off `settings` are applied again: after a change to either. */
|
|
346
346
|
applySettings(): void {
|
|
347
347
|
this.input.readOnly = this.settings.readOnly;
|
|
348
|
+
this.input.setAttribute('aria-label', this.settings.label);
|
|
348
349
|
this.root.dataset.readonly = String(this.settings.readOnly);
|
|
349
350
|
this.root.dataset.wrap = String(this.settings.wrap);
|
|
350
351
|
this.root.dataset.foldOutline = this.settings.foldOutline;
|
|
@@ -569,7 +570,14 @@ export class EditorView {
|
|
|
569
570
|
this.foldHints = {
|
|
570
571
|
symbols:
|
|
571
572
|
this.foldHints.symbols?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? [],
|
|
572
|
-
ranges:
|
|
573
|
+
ranges:
|
|
574
|
+
this.foldHints.ranges?.map((hint) => ({
|
|
575
|
+
...hint,
|
|
576
|
+
from: mapOffset(hint.from, changes),
|
|
577
|
+
to: mapOffset(hint.to, changes),
|
|
578
|
+
...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
|
|
579
|
+
...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
|
|
580
|
+
})) ?? []
|
|
573
581
|
};
|
|
574
582
|
}
|
|
575
583
|
this.customFolds = this.customFolds.map((fold) => ({ from: mapOffset(fold.from, changes), to: mapOffset(fold.to, changes) }));
|
|
@@ -722,12 +730,16 @@ export class EditorView {
|
|
|
722
730
|
}
|
|
723
731
|
|
|
724
732
|
private foldStates(): FoldState[] {
|
|
725
|
-
return this.foldRanges.map((range) =>
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
733
|
+
return this.foldRanges.map((range) => {
|
|
734
|
+
const collapsed = this.collapsed.has(range.from);
|
|
735
|
+
return {
|
|
736
|
+
startLine: range.startLine,
|
|
737
|
+
endLine: range.endLine,
|
|
738
|
+
collapsed,
|
|
739
|
+
closer: collapsed ? (range.tail ?? this.closerOf(range)) : undefined,
|
|
740
|
+
...(collapsed && range.head !== undefined ? { head: range.head } : {})
|
|
741
|
+
};
|
|
742
|
+
});
|
|
731
743
|
}
|
|
732
744
|
|
|
733
745
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
@@ -1045,14 +1057,14 @@ export class EditorView {
|
|
|
1045
1057
|
this.render();
|
|
1046
1058
|
}
|
|
1047
1059
|
|
|
1048
|
-
/* A caret in
|
|
1060
|
+
/* A caret in what a fold hides would be typing where nobody can see; it goes to where the fold's first line stops. */
|
|
1049
1061
|
private moveHiddenCarets(): void {
|
|
1050
1062
|
const visible = (offset: number): number => {
|
|
1051
|
-
|
|
1052
|
-
if (!this.layout.isHidden(line)) {
|
|
1063
|
+
if (!this.layout.hides(offset)) {
|
|
1053
1064
|
return offset;
|
|
1054
1065
|
}
|
|
1055
|
-
|
|
1066
|
+
const row = this.layout.rowForLine(this.model.positionAt(offset).line);
|
|
1067
|
+
return row.kind === 'text' && row.head !== null ? row.head : this.model.getLine(row.line).end;
|
|
1056
1068
|
};
|
|
1057
1069
|
const selections = this.model.getSelections();
|
|
1058
1070
|
const moved = selections.map((selection) => ({ anchor: visible(selection.anchor), head: visible(selection.head) }));
|
|
@@ -1064,11 +1076,11 @@ export class EditorView {
|
|
|
1064
1076
|
/* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
|
|
1065
1077
|
ensureVisible(offset: number): void {
|
|
1066
1078
|
const line = this.model.positionAt(offset).line;
|
|
1067
|
-
if (!this.layout.
|
|
1079
|
+
if (!this.layout.hides(offset)) {
|
|
1068
1080
|
return;
|
|
1069
1081
|
}
|
|
1070
1082
|
for (const range of this.foldRanges) {
|
|
1071
|
-
if (range.startLine < line && range.endLine >= line) {
|
|
1083
|
+
if ((range.startLine < line && range.endLine >= line) || (range.startLine === line && range.head !== undefined && offset > range.head)) {
|
|
1072
1084
|
this.collapsed.delete(range.from);
|
|
1073
1085
|
this.settled.add(range.from);
|
|
1074
1086
|
}
|
|
@@ -1374,11 +1386,25 @@ export class EditorView {
|
|
|
1374
1386
|
return { column, first, last };
|
|
1375
1387
|
}
|
|
1376
1388
|
|
|
1377
|
-
/*
|
|
1389
|
+
/* The line and the indentation column of a caret in the whitespace that leads its line, past the first column, where it can stand on a guide. */
|
|
1390
|
+
private caretOnGuide(): { line: number; column: number } | null {
|
|
1391
|
+
const position = this.model.positionAt(this.model.getPrimary().head);
|
|
1392
|
+
const lead = this.model.getLine(position.line).text.slice(0, position.column);
|
|
1393
|
+
if (position.column === 0 || !/^[\t ]*$/.test(lead)) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
return { line: position.line, column: indentationColumn(lead, this.settings.tabSize) };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/*
|
|
1400
|
+
* A line at each indentation level of the rows on screen, joined where the lines go on. Apart from them stand the one
|
|
1401
|
+
* of the scope around the caret, and as the platform does, the one the caret stands on.
|
|
1402
|
+
*/
|
|
1378
1403
|
private guideRects(rows: readonly LayoutRow[]): { plain: LayoutRect[]; active: LayoutRect[] } {
|
|
1379
1404
|
const size = this.settings.tabSize;
|
|
1380
1405
|
const charWidth = this.layout.metrics.charWidth;
|
|
1381
1406
|
const active = this.activeGuide();
|
|
1407
|
+
const caret = this.caretOnGuide();
|
|
1382
1408
|
const plain: LayoutRect[] = [];
|
|
1383
1409
|
const marked: LayoutRect[] = [];
|
|
1384
1410
|
interface Open {
|
|
@@ -1392,7 +1418,9 @@ export class EditorView {
|
|
|
1392
1418
|
const segment = open.get(column)!;
|
|
1393
1419
|
open.delete(column);
|
|
1394
1420
|
const rect = { x: column * charWidth, y: segment.top, width: 1, height: segment.bottom - segment.top };
|
|
1395
|
-
|
|
1421
|
+
const scope = active !== null && active.column === column && segment.last >= active.first && segment.first <= active.last;
|
|
1422
|
+
const under = caret !== null && caret.column === column && segment.first <= caret.line && caret.line <= segment.last;
|
|
1423
|
+
(scope || under ? marked : plain).push(rect);
|
|
1396
1424
|
};
|
|
1397
1425
|
for (const row of rows) {
|
|
1398
1426
|
if (row.kind !== 'text') {
|