@adecore/editor 0.17.0-beta.2 → 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.
@@ -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
- if (PAIR_CHARACTER.test(text)) {
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) 8%, transparent);
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) 10%, transparent);
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. Its entries stand a text size apart and light up as a link under the pointer.
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
- from: this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1)).start,
542
- to: this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1)).start,
543
- ...(range.kind === undefined ? {} : { kind: range.kind })
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
- /* The closer a fold ends in, clamped to its last line since the fold may be a moment older than an edit. */
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;
@@ -6,6 +6,7 @@ export const DEFAULT_SMART_KEYS = {
6
6
  smartIndentOnEnter: true,
7
7
  indentOnPaste: true,
8
8
  smartSemicolon: true,
9
+ smartArrow: true,
9
10
  camelHumps: false
10
11
  };
11
12
  /* The keys with what a host left out filled in from the defaults. */
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 (EditorFoldRange & {
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) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? []
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
- startLine: range.startLine,
588
- endLine: range.endLine,
589
- collapsed: this.collapsed.has(range.from),
590
- closer: this.collapsed.has(range.from) ? this.closerOf(range) : undefined
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 a line a fold hides would be typing where nobody can see. */
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
- const line = this.model.positionAt(offset).line;
882
- if (!this.layout.isHidden(line)) {
891
+ if (!this.layout.hides(offset)) {
883
892
  return offset;
884
893
  }
885
- return this.model.getLine(this.layout.rowForLine(line).line).end;
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.isHidden(line)) {
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
- /* A line at each indentation level of the rows on screen, joined where the lines go on, and the one of the scope around the caret apart. */
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
- (active !== null && active.column === column && segment.last >= active.first && segment.first <= active.last ? marked : plain).push(rect);
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-beta.2",
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-beta.2",
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
- if (PAIR_CHARACTER.test(text)) {
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) 8%, transparent);
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) 10%, transparent);
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. Its entries stand a text size apart and light up as a link under the pointer.
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
- from: this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1)).start,
681
- to: this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1)).start,
682
- ...(range.kind === undefined ? {} : { kind: range.kind })
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
@@ -20,6 +20,7 @@ export type {
20
20
  EditorEngine,
21
21
  EditorFindQuery,
22
22
  EditorFoldHints,
23
+ EditorFoldRangeHint,
23
24
  EditorFoldOutline,
24
25
  EditorFoldRange,
25
26
  EditorFoldSymbol,
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
- /* The closer a fold ends in, clamped to its last line since the fold may be a moment older than an edit. */
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
@@ -8,6 +8,7 @@ export const DEFAULT_SMART_KEYS: EditorSmartKeys = {
8
8
  smartIndentOnEnter: true,
9
9
  indentOnPaste: true,
10
10
  smartSemicolon: true,
11
+ smartArrow: true,
11
12
  camelHumps: false
12
13
  };
13
14
 
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 (EditorFoldRange & { readonly kind?: string })[];
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: this.foldHints.ranges?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? []
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
- startLine: range.startLine,
728
- endLine: range.endLine,
729
- collapsed: this.collapsed.has(range.from),
730
- closer: this.collapsed.has(range.from) ? this.closerOf(range) : undefined
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 a line a fold hides would be typing where nobody can see. */
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
- const line = this.model.positionAt(offset).line;
1053
- if (!this.layout.isHidden(line)) {
1063
+ if (!this.layout.hides(offset)) {
1054
1064
  return offset;
1055
1065
  }
1056
- return this.model.getLine(this.layout.rowForLine(line).line).end;
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.isHidden(line)) {
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
- /* A line at each indentation level of the rows on screen, joined where the lines go on, and the one of the scope around the caret apart. */
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
- (active !== null && active.column === column && segment.last >= active.first && segment.first <= active.last ? marked : plain).push(rect);
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') {