@adecore/editor 0.17.0-beta.2 → 0.18.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/dist/controller.js +3 -2
- package/dist/editor.css +5 -3
- package/dist/engine.js +12 -5
- 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 +7 -3
- package/dist/view.d.ts +1 -0
- package/dist/view.js +40 -15
- package/package.json +2 -2
- package/src/controller.ts +3 -2
- package/src/editor.css +5 -3
- package/src/engine.ts +13 -5
- package/src/index.ts +1 -0
- package/src/layout.ts +25 -1
- package/src/smart-keys.ts +1 -0
- package/src/types.ts +10 -1
- package/src/view.ts +42 -15
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
|
@@ -537,11 +537,18 @@ class SmartEditor {
|
|
|
537
537
|
to: this.offsetAt(symbol.range.end),
|
|
538
538
|
body: symbol.body
|
|
539
539
|
})),
|
|
540
|
-
ranges: (hints.ranges ?? []).map((range) =>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
+
})
|
|
545
552
|
});
|
|
546
553
|
}
|
|
547
554
|
setSmartKeys(keys) {
|
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
|
@@ -46,17 +46,21 @@ export interface EditorSmartKeys {
|
|
|
46
46
|
readonly smartIndentOnEnter: boolean;
|
|
47
47
|
readonly indentOnPaste: boolean;
|
|
48
48
|
readonly smartSemicolon: boolean;
|
|
49
|
+
readonly smartArrow: boolean;
|
|
49
50
|
readonly camelHumps: boolean;
|
|
50
51
|
}
|
|
51
52
|
export interface EditorFoldSymbol {
|
|
52
53
|
readonly range: EditorRange;
|
|
53
54
|
readonly body: 'function' | 'method' | 'class' | 'value';
|
|
54
55
|
}
|
|
56
|
+
export interface EditorFoldRangeHint extends EditorFoldRange {
|
|
57
|
+
readonly kind?: string;
|
|
58
|
+
readonly startCharacter?: number;
|
|
59
|
+
readonly endCharacter?: number;
|
|
60
|
+
}
|
|
55
61
|
export interface EditorFoldHints {
|
|
56
62
|
readonly symbols?: readonly EditorFoldSymbol[];
|
|
57
|
-
readonly ranges?: readonly
|
|
58
|
-
readonly kind?: string;
|
|
59
|
-
})[];
|
|
63
|
+
readonly ranges?: readonly EditorFoldRangeHint[];
|
|
60
64
|
}
|
|
61
65
|
export interface EditorBlock {
|
|
62
66
|
readonly startLine: number;
|
package/dist/view.d.ts
CHANGED
|
@@ -276,6 +276,7 @@ export declare class EditorView {
|
|
|
276
276
|
private effectiveIndent;
|
|
277
277
|
private activeGuide;
|
|
278
278
|
private scopeGuide;
|
|
279
|
+
private caretOnGuide;
|
|
279
280
|
private guideRects;
|
|
280
281
|
selectionAtPoint(clientX: number, clientY: number): EditorSelection | null;
|
|
281
282
|
setColumnMode(on: boolean): void;
|
package/dist/view.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.18.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.
|
|
42
|
+
"@adecore/editor-core": "0.18.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
|
@@ -676,11 +676,19 @@ class SmartEditor implements Editor {
|
|
|
676
676
|
to: this.offsetAt(symbol.range.end),
|
|
677
677
|
body: symbol.body
|
|
678
678
|
})),
|
|
679
|
-
ranges: (hints.ranges ?? []).map((range) =>
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
+
})
|
|
684
692
|
}
|
|
685
693
|
);
|
|
686
694
|
}
|
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
|
@@ -103,6 +103,8 @@ export interface EditorSmartKeys {
|
|
|
103
103
|
readonly indentOnPaste: boolean;
|
|
104
104
|
/* A `;` typed inside a call goes to the end of the statement. */
|
|
105
105
|
readonly smartSemicolon: boolean;
|
|
106
|
+
/* In PHP a `-` typed after a variable, a member or a `)` or `]` becomes `->`. */
|
|
107
|
+
readonly smartArrow: boolean;
|
|
106
108
|
/* Moving by word also stops inside `camelCase` and `snake_case` words. Off. */
|
|
107
109
|
readonly camelHumps: boolean;
|
|
108
110
|
}
|
|
@@ -114,9 +116,16 @@ export interface EditorFoldSymbol {
|
|
|
114
116
|
readonly body: 'function' | 'method' | 'class' | 'value';
|
|
115
117
|
}
|
|
116
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
|
+
|
|
117
126
|
export interface EditorFoldHints {
|
|
118
127
|
readonly symbols?: readonly EditorFoldSymbol[];
|
|
119
|
-
readonly ranges?: readonly
|
|
128
|
+
readonly ranges?: readonly EditorFoldRangeHint[];
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/* A part of the document with a header line, such as a function, a class or a method. */
|
package/src/view.ts
CHANGED
|
@@ -570,7 +570,14 @@ export class EditorView {
|
|
|
570
570
|
this.foldHints = {
|
|
571
571
|
symbols:
|
|
572
572
|
this.foldHints.symbols?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? [],
|
|
573
|
-
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
|
+
})) ?? []
|
|
574
581
|
};
|
|
575
582
|
}
|
|
576
583
|
this.customFolds = this.customFolds.map((fold) => ({ from: mapOffset(fold.from, changes), to: mapOffset(fold.to, changes) }));
|
|
@@ -723,12 +730,16 @@ export class EditorView {
|
|
|
723
730
|
}
|
|
724
731
|
|
|
725
732
|
private foldStates(): FoldState[] {
|
|
726
|
-
return this.foldRanges.map((range) =>
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
});
|
|
732
743
|
}
|
|
733
744
|
|
|
734
745
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
@@ -1046,14 +1057,14 @@ export class EditorView {
|
|
|
1046
1057
|
this.render();
|
|
1047
1058
|
}
|
|
1048
1059
|
|
|
1049
|
-
/* 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. */
|
|
1050
1061
|
private moveHiddenCarets(): void {
|
|
1051
1062
|
const visible = (offset: number): number => {
|
|
1052
|
-
|
|
1053
|
-
if (!this.layout.isHidden(line)) {
|
|
1063
|
+
if (!this.layout.hides(offset)) {
|
|
1054
1064
|
return offset;
|
|
1055
1065
|
}
|
|
1056
|
-
|
|
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;
|
|
1057
1068
|
};
|
|
1058
1069
|
const selections = this.model.getSelections();
|
|
1059
1070
|
const moved = selections.map((selection) => ({ anchor: visible(selection.anchor), head: visible(selection.head) }));
|
|
@@ -1065,11 +1076,11 @@ export class EditorView {
|
|
|
1065
1076
|
/* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
|
|
1066
1077
|
ensureVisible(offset: number): void {
|
|
1067
1078
|
const line = this.model.positionAt(offset).line;
|
|
1068
|
-
if (!this.layout.
|
|
1079
|
+
if (!this.layout.hides(offset)) {
|
|
1069
1080
|
return;
|
|
1070
1081
|
}
|
|
1071
1082
|
for (const range of this.foldRanges) {
|
|
1072
|
-
if (range.startLine < line && range.endLine >= line) {
|
|
1083
|
+
if ((range.startLine < line && range.endLine >= line) || (range.startLine === line && range.head !== undefined && offset > range.head)) {
|
|
1073
1084
|
this.collapsed.delete(range.from);
|
|
1074
1085
|
this.settled.add(range.from);
|
|
1075
1086
|
}
|
|
@@ -1375,11 +1386,25 @@ export class EditorView {
|
|
|
1375
1386
|
return { column, first, last };
|
|
1376
1387
|
}
|
|
1377
1388
|
|
|
1378
|
-
/*
|
|
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
|
+
*/
|
|
1379
1403
|
private guideRects(rows: readonly LayoutRow[]): { plain: LayoutRect[]; active: LayoutRect[] } {
|
|
1380
1404
|
const size = this.settings.tabSize;
|
|
1381
1405
|
const charWidth = this.layout.metrics.charWidth;
|
|
1382
1406
|
const active = this.activeGuide();
|
|
1407
|
+
const caret = this.caretOnGuide();
|
|
1383
1408
|
const plain: LayoutRect[] = [];
|
|
1384
1409
|
const marked: LayoutRect[] = [];
|
|
1385
1410
|
interface Open {
|
|
@@ -1393,7 +1418,9 @@ export class EditorView {
|
|
|
1393
1418
|
const segment = open.get(column)!;
|
|
1394
1419
|
open.delete(column);
|
|
1395
1420
|
const rect = { x: column * charWidth, y: segment.top, width: 1, height: segment.bottom - segment.top };
|
|
1396
|
-
|
|
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);
|
|
1397
1424
|
};
|
|
1398
1425
|
for (const row of rows) {
|
|
1399
1426
|
if (row.kind !== 'text') {
|