@adecore/editor-react 0.39.0 → 0.40.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.
Files changed (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
@@ -31,6 +31,7 @@ export function planConflict(base: string, ours: string, theirs: string): Confli
31
31
  let oursAt = 0;
32
32
  let theirsAt = 0;
33
33
  let mergedAt = 0;
34
+
34
35
  for (const block of blocks) {
35
36
  if (block.kind === 'theirs') {
36
37
  merges.push({ from: oursAt, to: oursAt + block.ours.length, lines: block.theirs });
@@ -47,10 +48,13 @@ export function planConflict(base: string, ours: string, theirs: string): Confli
47
48
  theirsEnd: theirsAt + block.theirs.length
48
49
  });
49
50
  }
51
+
50
52
  mergedAt += block.ours.length;
51
53
  }
54
+
52
55
  oursAt += block.ours.length;
53
56
  theirsAt += block.theirs.length;
54
57
  }
58
+
55
59
  return { merges, conflicts };
56
60
  }
@@ -36,5 +36,6 @@ export interface MenuGroups {
36
36
  /* The language rows of the context menu in their groups, each holding only the rows a server answers. */
37
37
  export function menuGroupsOf(supports: (method: string) => boolean): MenuGroups {
38
38
  const supported = (rows: readonly MenuRowSpec[]): MenuRowSpec[] => rows.filter((row) => supports(row.method));
39
+
39
40
  return { goTo: supported(GO_TO), references: supported(REFERENCES), changes: supported(CHANGES) };
40
41
  }
@@ -12,13 +12,16 @@ export class ContextMenuFeature {
12
12
 
13
13
  constructor(language: EditorLanguage) {
14
14
  this.language = language;
15
+
15
16
  const off = language.editor.onContextMenu((menu) => {
16
17
  // A right click outside the selection moves the caret first, so the menu is about what was clicked.
17
18
  if (!menu.inSelection) {
18
19
  language.editor.setCaret(menu.position);
19
20
  }
21
+
20
22
  this.open(menu.x, menu.y);
21
23
  });
24
+
22
25
  language.onDispose(() => {
23
26
  off();
24
27
  this.close();
@@ -27,6 +30,7 @@ export class ContextMenuFeature {
27
30
 
28
31
  close(): void {
29
32
  this.request++;
33
+
30
34
  if (this.language.popups.getState().menu !== null) {
31
35
  this.language.popups.setState({ menu: null });
32
36
  }
@@ -37,6 +41,7 @@ export class ContextMenuFeature {
37
41
  const { editor } = this.language;
38
42
  editor.focus();
39
43
  const done = document.execCommand(action);
44
+
40
45
  if (!done && action === 'paste') {
41
46
  void navigator.clipboard
42
47
  ?.readText()
@@ -52,8 +57,10 @@ export class ContextMenuFeature {
52
57
  const request = ++this.request;
53
58
  const { popups, codeActions } = this.language;
54
59
  popups.setState({ menu: { x, y, refactors: [] } });
60
+
55
61
  void codeActions.list(['refactor']).then((entries) => {
56
62
  const shown = popups.getState().menu;
63
+
57
64
  if (request === this.request && shown !== null && entries !== null) {
58
65
  popups.setState({ menu: { ...shown, refactors: entries } });
59
66
  }
@@ -5,6 +5,33 @@ import { locationsOf } from './hover-content.ts';
5
5
  import { lineTextOf } from './line-text.ts';
6
6
  import { wordRangeAt } from './rename-model.ts';
7
7
 
8
+ /* Reports the Mod key going down and up, from the window since the editor only says where the pointer is. */
9
+ export function followModKey(apple: boolean, onHeld: (held: boolean) => void): () => void {
10
+ if (typeof window === 'undefined') {
11
+ return () => {};
12
+ }
13
+
14
+ const modKey = apple ? 'Meta' : 'Control';
15
+
16
+ const keys = (event: KeyboardEvent): void => {
17
+ if (event.key === modKey) {
18
+ onHeld(event.type === 'keydown');
19
+ }
20
+ };
21
+
22
+ const lost = (): void => onHeld(false);
23
+
24
+ window.addEventListener('keydown', keys);
25
+ window.addEventListener('keyup', keys);
26
+ window.addEventListener('blur', lost);
27
+
28
+ return () => {
29
+ window.removeEventListener('keydown', keys);
30
+ window.removeEventListener('keyup', keys);
31
+ window.removeEventListener('blur', lost);
32
+ };
33
+ }
34
+
8
35
  /*
9
36
  * The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
10
37
  * is defined, which is what a Mod+click on it follows. The editor only says where the pointer is, so the
@@ -22,35 +49,22 @@ export class DefinitionLinkFeature {
22
49
  constructor(language: EditorLanguage) {
23
50
  this.language = language;
24
51
  const { editor } = language;
52
+
25
53
  const offs = [
26
54
  editor.onHover((hover) => {
27
55
  this.pointer = hover?.position ?? null;
28
56
  this.update();
29
57
  }),
30
58
  editor.onTextChange(() => this.clear()),
31
- editor.onViewChange(() => this.clear())
59
+ editor.onViewChange(() => this.clear()),
60
+ followModKey(language.apple, (held) => this.setModHeld(held))
32
61
  ];
33
- if (typeof window !== 'undefined') {
34
- const modKey = language.apple ? 'Meta' : 'Control';
35
- const keys = (event: KeyboardEvent): void => {
36
- if (event.key === modKey) {
37
- this.setModHeld(event.type === 'keydown');
38
- }
39
- };
40
- const lost = (): void => this.setModHeld(false);
41
- window.addEventListener('keydown', keys);
42
- window.addEventListener('keyup', keys);
43
- window.addEventListener('blur', lost);
44
- offs.push(() => {
45
- window.removeEventListener('keydown', keys);
46
- window.removeEventListener('keyup', keys);
47
- window.removeEventListener('blur', lost);
48
- });
49
- }
62
+
50
63
  language.onDispose(() => {
51
64
  for (const off of offs) {
52
65
  off();
53
66
  }
67
+
54
68
  this.clear();
55
69
  });
56
70
  }
@@ -66,6 +80,7 @@ export class DefinitionLinkFeature {
66
80
  this.request++;
67
81
  this.lookup?.abort();
68
82
  this.shownKey = '';
83
+
69
84
  if (this.drawn) {
70
85
  this.drawn = false;
71
86
  this.language.editor.setLink(null);
@@ -74,26 +89,35 @@ export class DefinitionLinkFeature {
74
89
 
75
90
  private update(): void {
76
91
  const { pointer } = this;
92
+
77
93
  if (!this.held || pointer === null || !this.language.project.service.supports('textDocument/definition', this.language.uri)) {
78
94
  this.clear();
95
+
79
96
  return;
80
97
  }
98
+
81
99
  const { editor, project, uri } = this.language;
82
100
  const line = lineTextOf(editor, pointer.line);
83
101
  const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
102
+
84
103
  if (word === null) {
85
104
  this.clear();
105
+
86
106
  return;
87
107
  }
108
+
88
109
  const key = `${word.start.line}:${word.start.character}`;
110
+
89
111
  if (key === this.shownKey) {
90
112
  return;
91
113
  }
114
+
92
115
  this.clear();
93
116
  this.shownKey = key;
94
117
  const request = ++this.request;
95
118
  const controller = new AbortController();
96
119
  this.lookup = controller;
120
+
97
121
  project.service
98
122
  .definition(uri, pointer, { signal: controller.signal })
99
123
  .then((result) => {
@@ -12,6 +12,7 @@ const SEVERITIES: Record<number, EditorMarkerSeverity> = { 1: 'error', 2: 'warni
12
12
  const HINT = 4;
13
13
  const UNNECESSARY = 1;
14
14
  const DEPRECATED = 2;
15
+
15
16
  /* The scroll track's tip is a line or two, so a long message is cut where it stops being a tip. */
16
17
  const TICK_MESSAGE_LENGTH = 300;
17
18
 
@@ -30,9 +31,9 @@ export function markerOf(diagnostic: Diagnostic): EditorMarker {
30
31
  };
31
32
  }
32
33
 
33
- /* A diagnostic that names no severity counts as an error. */
34
34
  export function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts {
35
35
  const count = (severity: number): number => diagnostics.filter((diagnostic) => (diagnostic.severity ?? 1) === severity).length;
36
+
36
37
  return { error: count(1), warning: count(2), info: count(3) };
37
38
  }
38
39
 
@@ -40,9 +41,14 @@ export function comparePositions(left: EditorPosition, right: EditorPosition): n
40
41
  return left.line === right.line ? left.character - right.character : left.line - right.line;
41
42
  }
42
43
 
44
+ export function sameRange(left: EditorRange, right: EditorRange): boolean {
45
+ return comparePositions(left.start, right.start) === 0 && comparePositions(left.end, right.end) === 0;
46
+ }
47
+
43
48
  /* Where an insertion of `text` at `start` ends, counting a line break of any kind once. */
44
49
  export function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
45
50
  const lines = text.split(/\r\n|\r|\n/);
51
+
46
52
  return lines.length === 1
47
53
  ? { line: start.line, character: start.character + text.length }
48
54
  : { line: start.line + lines.length - 1, character: lines[lines.length - 1]!.length };
@@ -51,13 +57,17 @@ export function endOfInsertion(start: EditorPosition, text: string): EditorPosit
51
57
  /* Where a position is once one change is applied to the text it was in. One inside the replaced range lands after the new text. */
52
58
  export function shiftPosition(position: EditorPosition, change: EditorContentChange): EditorPosition {
53
59
  const { start, end } = change.range;
60
+
54
61
  if (comparePositions(position, start) < 0) {
55
62
  return position;
56
63
  }
64
+
57
65
  const inserted = endOfInsertion(start, change.text);
66
+
58
67
  if (comparePositions(position, end) < 0) {
59
68
  return inserted;
60
69
  }
70
+
61
71
  return position.line === end.line
62
72
  ? { line: inserted.line, character: inserted.character + (position.character - end.character) }
63
73
  : { line: position.line + (inserted.line - end.line), character: position.character };
@@ -66,10 +76,12 @@ export function shiftPosition(position: EditorPosition, change: EditorContentCha
66
76
  export function shiftRange(range: EditorRange, changes: readonly EditorContentChange[]): EditorRange {
67
77
  let start = range.start;
68
78
  let end = range.end;
79
+
69
80
  for (const change of changes) {
70
81
  start = shiftPosition(start, change);
71
82
  end = shiftPosition(end, change);
72
83
  }
84
+
73
85
  return { start, end };
74
86
  }
75
87
 
@@ -81,6 +93,7 @@ export function rangeHolds(range: EditorRange, position: EditorPosition): boolea
81
93
  /* The problems under a position, worst first. A hint is not a problem, so it is left to the colors and the faded text. */
82
94
  export function problemsAt(problems: readonly Problem[], position: EditorPosition): Problem[] {
83
95
  const rank = (problem: Problem): number => problem.diagnostic.severity ?? 1;
96
+
84
97
  return problems
85
98
  .filter((problem) => (problem.diagnostic.severity ?? 1) < HINT && rangeHolds(problem.diagnostic.range, position))
86
99
  .sort((left, right) => rank(left) - rank(right));
@@ -91,20 +104,25 @@ export function neighborProblem(problems: readonly Problem[], from: EditorPositi
91
104
  const candidates = problems
92
105
  .filter((problem) => (problem.diagnostic.severity ?? 1) < HINT)
93
106
  .sort((left, right) => comparePositions(left.diagnostic.range.start, right.diagnostic.range.start));
107
+
94
108
  if (candidates.length === 0) {
95
109
  return null;
96
110
  }
111
+
97
112
  if (direction === 1) {
98
113
  return candidates.find((problem) => comparePositions(problem.diagnostic.range.start, from) > 0) ?? candidates[0]!;
99
114
  }
115
+
100
116
  return [...candidates].reverse().find((problem) => comparePositions(problem.diagnostic.range.start, from) < 0) ?? candidates[candidates.length - 1]!;
101
117
  }
102
118
 
103
119
  /* `ts(2551)`, or the code alone, or the source alone, whichever the server gave. */
104
120
  export function codeLabelOf(diagnostic: Diagnostic): string {
105
121
  const code = diagnostic.code === undefined ? '' : String(diagnostic.code);
122
+
106
123
  if (diagnostic.source !== undefined && code !== '') {
107
124
  return `${diagnostic.source}(${code})`;
108
125
  }
126
+
109
127
  return code !== '' ? code : (diagnostic.source ?? '');
110
128
  }
@@ -2,9 +2,18 @@ import type { Diagnostic } from '@adecore/lsp';
2
2
  import { KEY_PRIORITY } from './key-priority.ts';
3
3
  import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
4
4
  import type { EditorLanguage } from './editor-language.ts';
5
- import { sameRange } from './hover-content.ts';
6
5
  import { isShortcut } from './shortcut-keys.ts';
7
- import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange, type Problem } from './diagnostics-model.ts';
6
+ import {
7
+ comparePositions,
8
+ markerOf,
9
+ neighborProblem,
10
+ problemCountsOf,
11
+ problemsAt,
12
+ sameRange,
13
+ severityOf,
14
+ shiftRange,
15
+ type Problem
16
+ } from './diagnostics-model.ts';
8
17
 
9
18
  export interface ProblemCounts {
10
19
  readonly error: number;
@@ -26,22 +35,29 @@ export class DiagnosticsFeature {
26
35
  constructor(language: EditorLanguage) {
27
36
  this.language = language;
28
37
  const { editor, project, uri } = language;
38
+
29
39
  const reports = project.service.onDiagnostics((report) => {
30
40
  if (report.uri === uri) {
31
41
  this.reports.set(report.source, report.diagnostics);
32
42
  this.rebuild();
33
43
  }
34
44
  });
45
+
35
46
  const edits = editor.onTextChange((change) => this.shift(change.changes));
47
+
36
48
  // These keys are the editor's while it has the keyboard; elsewhere the menu's commands answer.
37
49
  const keys = language.onKey((event) => {
38
50
  const next = isShortcut(this.language.shortcuts.nextProblem, event);
51
+
39
52
  if (!next && !isShortcut(this.language.shortcuts.previousProblem, event)) {
40
53
  return false;
41
54
  }
55
+
42
56
  this.step(next ? 1 : -1);
57
+
43
58
  return true;
44
59
  }, KEY_PRIORITY.diagnostics);
60
+
45
61
  language.onDispose(() => {
46
62
  keys();
47
63
  reports.dispose();
@@ -67,26 +83,33 @@ export class DiagnosticsFeature {
67
83
  step(direction: 1 | -1): boolean {
68
84
  const caret = this.language.editor.getCaret();
69
85
  const target = neighborProblem(this.list, caret, direction);
86
+
70
87
  if (target === null) {
71
88
  return false;
72
89
  }
90
+
73
91
  const { start } = target.diagnostic.range;
74
92
  this.reveal(target, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
93
+
75
94
  return true;
76
95
  }
77
96
 
78
97
  /* Puts the caret on the first problem of the file. */
79
98
  goToFirst(): boolean {
80
99
  const target = neighborProblem(this.list, { line: -1, character: 0 }, 1);
100
+
81
101
  if (target === null) {
82
102
  return false;
83
103
  }
104
+
84
105
  this.reveal(target, 'center');
106
+
85
107
  return true;
86
108
  }
87
109
 
88
110
  onChange(listener: () => void): () => void {
89
111
  this.listeners.add(listener);
112
+
90
113
  return () => {
91
114
  this.listeners.delete(listener);
92
115
  };
@@ -104,20 +127,25 @@ export class DiagnosticsFeature {
104
127
  /* The problems follow the edit as the markers do. Their number does not change, so nobody is told. */
105
128
  private shift(changes: readonly EditorContentChange[]): void {
106
129
  let moved = false;
130
+
107
131
  for (const [server, diagnostics] of this.reports) {
108
132
  let shifted: Diagnostic[] | null = null;
133
+
109
134
  for (const [index, diagnostic] of diagnostics.entries()) {
110
135
  const range = shiftRange(diagnostic.range, changes);
136
+
111
137
  if (!sameRange(range, diagnostic.range)) {
112
138
  shifted ??= [...diagnostics];
113
139
  shifted[index] = { ...diagnostic, range };
114
140
  }
115
141
  }
142
+
116
143
  if (shifted !== null) {
117
144
  this.reports.set(server, shifted);
118
145
  moved = true;
119
146
  }
120
147
  }
148
+
121
149
  if (moved) {
122
150
  this.collect();
123
151
  }
@@ -126,6 +154,7 @@ export class DiagnosticsFeature {
126
154
  private rebuild(): void {
127
155
  this.collect();
128
156
  this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
157
+
129
158
  for (const listener of [...this.listeners]) {
130
159
  listener();
131
160
  }
@@ -11,39 +11,48 @@ export const DEFAULT_UNFOLD_LIMITS: DiffLimits = { rows: 20000, characters: 2000
11
11
  export function fitsDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, limits: DiffLimits, maxRows = limits.rows): boolean {
12
12
  let rows = 0;
13
13
  let characters = 0;
14
+
14
15
  for (const row of diffRows(document, 'unified', unfolded)) {
15
16
  if (row.kind === 'gap') {
16
17
  continue;
17
18
  }
19
+
18
20
  const text = row.new === undefined ? document.sides.old[row.old!.index]! : document.sides.new[row.new.index]!;
19
21
  characters += text.length + (rows > 0 ? 1 : 0);
22
+
20
23
  if (++rows > maxRows || characters > limits.characters || text.length > limits.lineLength) {
21
24
  return false;
22
25
  }
23
26
  }
27
+
24
28
  return true;
25
29
  }
26
30
 
27
31
  export function expandDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, id: number, limits: DiffLimits): ReadonlyMap<number, number> {
28
32
  const rows = diffRows(document, 'unified', unfolded);
29
33
  const gap = rows.find((row) => row.kind === 'gap' && row.id === id);
34
+
30
35
  if (gap?.kind !== 'gap' || !gap.unfoldable) {
31
36
  return unfolded;
32
37
  }
38
+
33
39
  const visible = rows.filter((row) => row.kind === 'line').length;
34
40
  const maxRows = Math.min(limits.rows, visible + 500);
35
41
  const previous = unfolded.get(id) ?? 0;
36
42
  let low = 0;
37
43
  let high = Math.min(500, gap.oldCount);
44
+
38
45
  // Between-hunk gaps reveal from both ends; search the per-end count within the shared row budget.
39
46
  while (low < high) {
40
47
  const count = Math.ceil((low + high) / 2);
41
48
  const candidate = new Map(unfolded).set(id, previous + count);
49
+
42
50
  if (fitsDiff(document, candidate, limits, maxRows)) {
43
51
  low = count;
44
52
  } else {
45
53
  high = count - 1;
46
54
  }
47
55
  }
56
+
48
57
  return low === 0 ? unfolded : new Map(unfolded).set(id, previous + low);
49
58
  }
@@ -26,18 +26,24 @@ export function DiffOverlays({
26
26
  blocked?: ReadonlySet<number>;
27
27
  }) {
28
28
  const { t } = useTranslation('editor');
29
+
29
30
  const getRows = () => mounted.rows.getContainers();
30
31
  const getActions = () => mounted.actions.getContainers();
32
+
31
33
  const rows = useSyncExternalStore((listener) => mounted.rows.subscribe(listener), getRows, getRows);
32
34
  const actions = useSyncExternalStore((listener) => mounted.actions.subscribe(listener), getActions, getActions);
35
+
33
36
  return (
34
37
  <>
35
38
  {display.gaps.map(({ row }) => {
36
39
  const container = rows.get(`gap-${row.id}`);
40
+
37
41
  if (container === undefined) {
38
42
  return null;
39
43
  }
44
+
40
45
  const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
46
+
41
47
  return createPortal(
42
48
  <div className="adecore-editor-diff-gap">
43
49
  {row.unfoldable && blocked?.has(row.id) === true ? (
@@ -65,7 +71,9 @@ export function DiffOverlays({
65
71
  if (row.noNewline === undefined) {
66
72
  return null;
67
73
  }
74
+
68
75
  const container = actions.get(`no-newline-${index}`);
76
+
69
77
  return container === undefined
70
78
  ? null
71
79
  : createPortal(<span className="adecore-editor-diff-no-newline">{t('diff.noNewline')}</span>, container, `no-newline-${index}`);
@@ -15,49 +15,103 @@ export interface DiffSyntaxSources {
15
15
  locate(side: DiffSourceSide, index: number): { readonly block: number; readonly line: number } | undefined;
16
16
  }
17
17
 
18
+ /* Calls `refresh` once per frame while tokens arrive, or in a microtask where the window draws no frames. */
19
+ export function repaintOnTokens(ownerWindow: Window | null | undefined, tokens: DiffSourceTokens | undefined, refresh: () => void): () => void {
20
+ let pending = false;
21
+ let stopped = false;
22
+ let frame: number | undefined;
23
+
24
+ const schedule = (): void => {
25
+ if (pending) {
26
+ return;
27
+ }
28
+
29
+ pending = true;
30
+
31
+ const paint = (): void => {
32
+ pending = false;
33
+
34
+ if (!stopped) {
35
+ refresh();
36
+ }
37
+ };
38
+
39
+ if (ownerWindow?.requestAnimationFrame !== undefined) {
40
+ frame = ownerWindow.requestAnimationFrame(paint);
41
+ } else {
42
+ queueMicrotask(paint);
43
+ }
44
+ };
45
+
46
+ const stop = tokens?.subscribe(schedule);
47
+
48
+ return () => {
49
+ stopped = true;
50
+ stop?.();
51
+
52
+ if (frame !== undefined) {
53
+ ownerWindow?.cancelAnimationFrame?.(frame);
54
+ }
55
+ };
56
+ }
57
+
18
58
  function canTokenize(source: readonly string[]): boolean {
19
59
  if (source.length > 20000) {
20
60
  return false;
21
61
  }
62
+
22
63
  let characters = Math.max(0, source.length - 1);
64
+
23
65
  for (const line of source) {
24
66
  characters += line.length;
67
+
25
68
  if (line.length > 10000 || characters > 2000000) {
26
69
  return false;
27
70
  }
28
71
  }
72
+
29
73
  return true;
30
74
  }
31
75
 
32
76
  export function diffSyntaxDocument(document: DiffDocument, language: string, theme: string): DiffSyntaxSources {
33
77
  const locations: Record<DiffSourceSide, Array<{ block: number; line: number }>> = { old: [], new: [] };
78
+
34
79
  const build = (side: DiffSourceSide): DiffSyntaxDocument => {
35
80
  const source = document.sides[side];
81
+
36
82
  // Skipping a long hidden line loses grammar state; keep the entire side plain instead.
37
83
  if (!canTokenize(source)) {
38
84
  return { language, theme, blocks: [], resets: [] };
39
85
  }
86
+
40
87
  const blocks: string[] = [];
41
88
  const resets: number[] = [];
89
+
42
90
  const ranges = document.snapshots
43
91
  ? [{ from: 0, to: source.length }]
44
92
  : document.offsets.map((offset, index) => ({ from: offset[side], to: document.offsets[index + 1]?.[side] ?? source.length }));
93
+
45
94
  for (const range of ranges) {
46
95
  if (!document.snapshots && range.from < range.to) {
47
96
  resets.push(blocks.length);
48
97
  }
98
+
49
99
  for (let from = range.from; from < range.to; from += 200) {
50
100
  const to = Math.min(from + 200, range.to);
51
101
  const block = blocks.length;
52
102
  blocks.push(source.slice(from, to).join('\n'));
103
+
53
104
  for (let index = from; index < to; index++) {
54
105
  locations[side][index] = { block, line: index - from };
55
106
  }
56
107
  }
57
108
  }
109
+
58
110
  return { language, theme, blocks, resets };
59
111
  };
112
+
60
113
  const documents = { old: build('old'), new: build('new') };
114
+
61
115
  return {
62
116
  documents,
63
117
  locate(side, index) {
@@ -7,25 +7,33 @@ export function wordMarks(document: DiffDocument): {
7
7
  const marks = { old: new Map<number, readonly (readonly [number, number])[]>(), new: new Map<number, readonly (readonly [number, number])[]>() };
8
8
  let pairs = 0;
9
9
  let characters = 0;
10
+
10
11
  for (const row of diffRows(document, 'split')) {
11
12
  if (row.kind !== 'line' || row.change !== 'pair' || document.hunks[row.hunk!]!.coarse) {
12
13
  continue;
13
14
  }
15
+
14
16
  const old = document.sides.old[row.old!.index]!;
15
17
  const fresh = document.sides.new[row.new!.index]!;
16
18
  characters += old.length + fresh.length;
19
+
17
20
  // Per-pair limits alone still allow thousands of expensive word diffs during one mount.
18
21
  if (++pairs > 1000 || characters > 100000) {
19
22
  break;
20
23
  }
24
+
21
25
  const words = wordChanges(old, fresh);
26
+
22
27
  if (words === null) {
23
28
  continue;
24
29
  }
30
+
25
31
  const ranges = (text: string, ranges: readonly DiffWordRange[]): Array<readonly [number, number]> =>
26
32
  ranges.filter((range) => !document.ignoreWhitespace || /\S/.test(text.slice(range.from, range.to))).map((range) => [range.from, range.to]);
33
+
27
34
  marks.old.set(row.old!.index, ranges(old, words.old));
28
35
  marks.new.set(row.new!.index, ranges(fresh, words.new));
29
36
  }
37
+
30
38
  return marks;
31
39
  }