@adecore/editor-react 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
package/src/navigation.ts CHANGED
@@ -7,6 +7,7 @@ import { rangeHolds } from './diagnostics-model.ts';
7
7
  import { placesOfName } from './symbol-links.ts';
8
8
  import { isShortcut } from './shortcut-keys.ts';
9
9
  import { messageOf } from './error-message.ts';
10
+ import { placeKey, uniqueBy } from './unique.ts';
10
11
 
11
12
  export type NavigationKind = 'definition' | 'declaration' | 'typeDefinition' | 'implementation';
12
13
 
@@ -34,23 +35,16 @@ const TOAST_ID = 'language-navigation';
34
35
 
35
36
  /* The places of a result, each once: the two servers of a Vue file may both know the same one. `target` is the whole declaration when the server gave it. */
36
37
  export function uniquePlaces(result: NavigationResult): { location: Location; target: Range | null }[] {
37
- const seen = new Set<string>();
38
38
  const list = result === null ? [] : Array.isArray(result) ? (result as (Location | LocationLink)[]) : [result];
39
- return list
40
- .map((entry) =>
39
+
40
+ return uniqueBy(
41
+ list.map((entry) =>
41
42
  'targetUri' in entry
42
43
  ? { location: { uri: entry.targetUri, range: entry.targetSelectionRange }, target: entry.targetRange as Range | null }
43
44
  : { location: entry, target: null }
44
- )
45
- .filter(({ location }) => {
46
- const { start } = location.range;
47
- const key = `${location.uri}\0${start.line}\0${start.character}`;
48
- if (seen.has(key)) {
49
- return false;
50
- }
51
- seen.add(key);
52
- return true;
53
- });
45
+ ),
46
+ ({ location }) => placeKey(location)
47
+ );
54
48
  }
55
49
 
56
50
  /* The places of a result, each once. */
@@ -69,23 +63,30 @@ export class NavigationFeature {
69
63
  constructor(language: EditorLanguage) {
70
64
  this.language = language;
71
65
  const { editor } = language;
66
+
72
67
  const offs = [
73
68
  editor.onClick((click) => {
74
69
  if (!click.mod || click.alt || click.shift || !this.supports('definition')) {
75
70
  return false;
76
71
  }
72
+
77
73
  void this.go('definition', click.position);
74
+
78
75
  return true;
79
76
  }),
80
77
  language.onKey((event) => {
81
78
  const found = SHORTCUT_KINDS.find(([, name]) => isShortcut(this.language.shortcuts[name], event));
79
+
82
80
  if (found === undefined) {
83
81
  return false;
84
82
  }
83
+
85
84
  void this.go(found[0]);
85
+
86
86
  return true;
87
87
  }, KEY_PRIORITY.navigation)
88
88
  ];
89
+
89
90
  language.onDispose(() => {
90
91
  for (const off of offs) {
91
92
  off();
@@ -99,20 +100,27 @@ export class NavigationFeature {
99
100
 
100
101
  async go(kind: NavigationKind, position: EditorPosition = this.language.editor.getCaret()): Promise<void> {
101
102
  const { project, uri, editor } = this.language;
103
+
102
104
  if (!this.supports(kind)) {
103
105
  this.tell(this.say('unavailable'));
106
+
104
107
  return;
105
108
  }
109
+
106
110
  let result: NavigationResult;
111
+
107
112
  try {
108
113
  result = await REQUESTS[kind](project.service, uri, position);
109
114
  } catch (error) {
110
115
  if (!(error instanceof StaleResultError)) {
111
116
  this.tell(this.say('failed', { message: messageOf(error) }));
112
117
  }
118
+
113
119
  return;
114
120
  }
121
+
115
122
  const places = uniqueLocations(result);
123
+
116
124
  if (places.length === 0) {
117
125
  this.tell(this.say('none'));
118
126
  } else if (kind === 'definition' && places.length === 1 && places[0]!.uri === uri && rangeHolds(places[0]!.range, position)) {
@@ -123,6 +131,7 @@ export class NavigationFeature {
123
131
  } else {
124
132
  this.language.locations(places, position, this.say(`titles.${kind}`));
125
133
  }
134
+
126
135
  editor.focus();
127
136
  }
128
137
 
@@ -133,15 +142,20 @@ export class NavigationFeature {
133
142
  */
134
143
  async goToName(name: string, anchor: EditorPosition, own: Location | null): Promise<void> {
135
144
  const { project, uri, editor } = this.language;
145
+
136
146
  if (own !== null) {
137
147
  this.language.goTo(own);
148
+
138
149
  return;
139
150
  }
151
+
140
152
  if (!project.service.supports('workspace/symbol', uri)) {
141
153
  return;
142
154
  }
155
+
143
156
  try {
144
157
  const places = placesOfName(await project.service.workspaceSymbols(uri, name), name, editor.getText());
158
+
145
159
  if (places.length === 1) {
146
160
  this.language.goTo(places[0]!);
147
161
  } else if (places.length > 1) {
@@ -166,5 +180,6 @@ export function locationRow(location: Location, stored: string | null): { label:
166
180
  const path = stored ?? fileUriToPath(location.uri) ?? location.uri;
167
181
  const name = basenameOf(path);
168
182
  const folder = path.slice(0, Math.max(0, path.length - name.length)).replace(/\/$/, '');
183
+
169
184
  return { label: `${name}:${location.range.start.line + 1}`, detail: folder, path: true };
170
185
  }
package/src/panel-size.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  /* The height of the list-and-preview area of a peek or a call hierarchy, which is what a person drags; the bands and the title bar around it stay. */
2
2
  export const PANEL_BODY_HEIGHT = 180;
3
+
3
4
  /* Four rows of 22px, so a list never shrinks to nothing. */
4
5
  export const PANEL_BODY_MIN = 88;
6
+
5
7
  /* What the bands and the title bar take of the panel, which the share of the editor has to leave room for. */
6
8
  const PANEL_CHROME = 63;
9
+
7
10
  /* The share of the editor's height a panel may take, so the line it belongs to stays in view. */
8
11
  const PANEL_MAX_SHARE = 0.8;
12
+
9
13
  const PANEL_FALLBACK_AVAILABLE = 800;
14
+
10
15
  export const PANEL_KEY_STEP = 22;
11
16
 
12
17
  /* The tallest body for an editor that is `available` pixels high; never below the minimum, so a tiny editor still gets a usable panel. */
@@ -18,11 +23,13 @@ export function clampPanelBody(height: number, available: number = PANEL_FALLBAC
18
23
  if (!Number.isFinite(height)) {
19
24
  return PANEL_BODY_HEIGHT;
20
25
  }
26
+
21
27
  return Math.min(panelBodyMax(available), Math.max(PANEL_BODY_MIN, Math.round(height)));
22
28
  }
23
29
 
24
30
  /* The height of the editor's scrolling area around a panel's row; the fallback stands in where nothing is laid out. */
25
31
  export function availableHeightOf(container: HTMLElement | null): number {
26
32
  const height = container?.closest<HTMLElement>('.se-viewport')?.clientHeight ?? 0;
33
+
27
34
  return height > 0 ? height : PANEL_FALLBACK_AVAILABLE;
28
35
  }
@@ -49,10 +49,13 @@ function numberOf(line: PatchLayoutLine, numbers: 'single' | 'both', width: numb
49
49
  if (line.old === null && line.new === null) {
50
50
  return null;
51
51
  }
52
+
52
53
  if (numbers === 'single') {
53
54
  return String(line.new ?? line.old);
54
55
  }
56
+
55
57
  const pad = (value: number | null): string => (value === null ? '' : String(value)).padStart(width, ' ');
58
+
56
59
  return `${pad(line.old)} ${pad(line.new)}`;
57
60
  }
58
61
 
@@ -61,28 +64,35 @@ function highlightsOf(layout: PatchLayout): { highlights: EditorLineHighlight[];
61
64
  const highlights: EditorLineHighlight[] = [];
62
65
  const marks: EditorChangeMark[] = [];
63
66
  const lines = layout.lines;
67
+
64
68
  for (let index = 0; index < lines.length; index++) {
65
69
  const role = lines[index]!.role;
70
+
66
71
  if (role === 'file-header') {
67
72
  highlights.push({ startLine: index + 1, endLine: index + 1, color: '--text-muted', fill: '--surface-raised' });
68
73
  } else if (role === 'hunk-header') {
69
74
  highlights.push({ startLine: index + 1, endLine: index + 1, color: '--accent' });
70
75
  } else if (role === 'added' || role === 'removed') {
71
76
  let end = index;
77
+
72
78
  while (end + 1 < lines.length && lines[end + 1]!.role === role) {
73
79
  end++;
74
80
  }
81
+
75
82
  const removed = role === 'removed';
83
+
76
84
  highlights.push({
77
85
  startLine: index + 1,
78
86
  endLine: end + 1,
79
87
  color: removed ? '--editor-deleted' : '--editor-added',
80
88
  bar: removed ? 'hatched' : 'solid'
81
89
  });
90
+
82
91
  marks.push({ startLine: index + 1, endLine: end + 1, kind: removed ? 'deleted' : 'added' });
83
92
  index = end;
84
93
  }
85
94
  }
95
+
86
96
  return { highlights, marks };
87
97
  }
88
98
 
@@ -101,86 +111,111 @@ export function decoratePatch(editor: Editor, options: PatchDecorationOptions =
101
111
  const timer = options.timer ?? DEFAULT_TIMER;
102
112
  const delay = options.delay ?? 150;
103
113
  const immediateBelow = options.immediateBelow ?? 100_000;
114
+
104
115
  const clearWait = (): void => {
105
116
  if (wait !== null) {
106
117
  timer.clear(wait);
107
118
  wait = null;
108
119
  }
109
120
  };
121
+
110
122
  const after = (callback: () => void): void => {
111
123
  clearWait();
124
+
112
125
  wait = timer.set(() => {
113
126
  wait = null;
114
127
  callback();
115
128
  }, delay);
116
129
  };
130
+
117
131
  const apply = (): void => {
118
132
  const numbers = options.numbers ?? 'single';
119
133
  const lines = layout.lines;
120
134
  const width = lines.reduce((digits, line) => Math.max(digits, String(Math.max(line.old ?? 0, line.new ?? 0)).length), 1);
135
+
121
136
  editor.setLineNumbers((line) => {
122
137
  const entry = lines[line];
138
+
123
139
  return entry === undefined ? null : numberOf(entry, numbers, width);
124
140
  });
141
+
125
142
  const labels = options.labels;
143
+
126
144
  editor.setLineLabels(
127
145
  labels === undefined
128
146
  ? null
129
147
  : (line) => {
130
148
  const entry = lines[line];
149
+
131
150
  if (entry?.role === 'removed') {
132
151
  return labels.removed(entry.old!);
133
152
  }
153
+
134
154
  if (entry?.role === 'added') {
135
155
  return labels.added(entry.new!);
136
156
  }
157
+
137
158
  return entry?.role === 'context' ? labels.unchanged(entry.old!, entry.new!) : null;
138
159
  }
139
160
  );
161
+
140
162
  const { highlights, marks } = highlightsOf(layout);
141
163
  editor.setLineHighlights(highlights);
142
164
  editor.setChangeMarks(marks, CHANGE_LAYER);
143
165
  };
166
+
144
167
  const decorate = (): void => {
145
168
  pending = false;
169
+
146
170
  if (disposed) {
147
171
  return;
148
172
  }
173
+
149
174
  const text = editor.getText();
150
175
  length = text.length;
151
176
  layout = patchLayoutOf(text, options.limits);
152
177
  apply();
153
178
  };
179
+
154
180
  const announce = (): void => {
155
181
  if (!disposed) {
156
182
  options.onLayout?.(layout);
157
183
  }
158
184
  };
185
+
159
186
  const read = (): void => {
160
187
  clearWait();
161
188
  decorate();
162
189
  announce();
163
190
  };
191
+
164
192
  const schedule = (): void => {
165
193
  if (length < immediateBelow) {
166
194
  if (pending) {
167
195
  return;
168
196
  }
197
+
169
198
  pending = true;
199
+
170
200
  queueMicrotask(() => {
171
201
  decorate();
172
202
  after(announce);
173
203
  });
204
+
174
205
  return;
175
206
  }
207
+
176
208
  pending = true;
209
+
177
210
  after(() => {
178
211
  decorate();
179
212
  announce();
180
213
  });
181
214
  };
215
+
182
216
  const stop = editor.onTextChange(schedule);
183
217
  read();
218
+
184
219
  return {
185
220
  get layout() {
186
221
  return layout;
@@ -204,10 +239,12 @@ export function usePatchDecorations(editor: Editor | null, options: Omit<PatchDe
204
239
  const [state, setState] = useState<{ editor: Editor; layout: PatchLayout } | null>(null);
205
240
  const { numbers } = options;
206
241
  const { maxLines, maxCharacters } = options.limits ?? {};
242
+
207
243
  useEffect(() => {
208
244
  if (editor === null) {
209
245
  return;
210
246
  }
247
+
211
248
  const decoration = decoratePatch(editor, {
212
249
  ...(numbers === undefined ? {} : { numbers }),
213
250
  limits: { ...(maxLines === undefined ? {} : { maxLines }), ...(maxCharacters === undefined ? {} : { maxCharacters }) },
@@ -218,7 +255,9 @@ export function usePatchDecorations(editor: Editor | null, options: Omit<PatchDe
218
255
  },
219
256
  onLayout: (layout) => setState({ editor, layout })
220
257
  });
258
+
221
259
  return () => decoration.dispose();
222
260
  }, [editor, numbers, maxLines, maxCharacters, t]);
261
+
223
262
  return state !== null && state.editor === editor ? state.layout : null;
224
263
  }
@@ -101,18 +101,23 @@ function unquote(raw: string): string {
101
101
  if (raw.length < 2 || !raw.startsWith('"') || !raw.endsWith('"')) {
102
102
  return raw;
103
103
  }
104
+
104
105
  const bytes: number[] = [];
105
106
  const encoder = new TextEncoder();
106
107
  const body = raw.slice(1, -1);
108
+
107
109
  for (let i = 0; i < body.length; i++) {
108
110
  if (body[i] !== '\\') {
109
111
  const point = String.fromCodePoint(body.codePointAt(i)!);
110
112
  bytes.push(...encoder.encode(point));
111
113
  i += point.length - 1;
114
+
112
115
  continue;
113
116
  }
117
+
114
118
  const octal = /^[0-3][0-7]{2}/.exec(body.slice(i + 1, i + 4));
115
119
  const escaped = ESCAPES[body[i + 1] ?? ''];
120
+
116
121
  if (octal !== null) {
117
122
  bytes.push(Number.parseInt(octal[0], 8));
118
123
  i += 3;
@@ -123,6 +128,7 @@ function unquote(raw: string): string {
123
128
  bytes.push(92);
124
129
  }
125
130
  }
131
+
126
132
  return new TextDecoder().decode(new Uint8Array(bytes));
127
133
  }
128
134
 
@@ -130,6 +136,7 @@ function unquote(raw: string): string {
130
136
  function pathOf(raw: string): string | null {
131
137
  const tab = raw.indexOf('\t');
132
138
  const name = unquote(tab === -1 ? raw : raw.slice(0, tab));
139
+
133
140
  return name === '/dev/null' ? null : name;
134
141
  }
135
142
 
@@ -141,23 +148,30 @@ function stripPrefix(path: string | null, prefix: 'a/' | 'b/'): string | null {
141
148
  function gitPaths(rest: string): { oldPath: string | null; newPath: string | null; prefixed: boolean } {
142
149
  if (rest.startsWith('"')) {
143
150
  const end = rest.indexOf('" ', 1);
151
+
144
152
  if (end !== -1) {
145
153
  const oldName = unquote(rest.slice(0, end + 1));
146
154
  const newName = unquote(rest.slice(end + 2));
147
155
  const prefixed = oldName.startsWith('a/') && newName.startsWith('b/');
156
+
148
157
  return prefixed ? { oldPath: oldName.slice(2), newPath: newName.slice(2), prefixed } : { oldPath: oldName, newPath: newName, prefixed };
149
158
  }
150
159
  }
160
+
151
161
  const middle = rest.indexOf(' b/');
162
+
152
163
  if (rest.startsWith('a/') && middle !== -1) {
153
164
  for (let at = middle; at !== -1; at = rest.indexOf(' b/', at + 1)) {
154
165
  if (rest.slice(2, at) === rest.slice(at + 3)) {
155
166
  return { oldPath: rest.slice(2, at), newPath: rest.slice(at + 3), prefixed: true };
156
167
  }
157
168
  }
169
+
158
170
  return { oldPath: rest.slice(2, middle), newPath: unquote(rest.slice(middle + 3)), prefixed: true };
159
171
  }
172
+
160
173
  const split = rest.indexOf(' ');
174
+
161
175
  return split === -1
162
176
  ? { oldPath: unquote(rest), newPath: null, prefixed: false }
163
177
  : { oldPath: unquote(rest.slice(0, split)), newPath: unquote(rest.slice(split + 1)), prefixed: false };
@@ -187,30 +201,39 @@ function statusOf(section: Section): PatchFileStatus {
187
201
  if (section.binary) {
188
202
  return 'binary';
189
203
  }
204
+
190
205
  if (section.created || (!section.git && section.oldPath === null && section.newPath !== null)) {
191
206
  return 'added';
192
207
  }
208
+
193
209
  if (section.deleted || (!section.git && section.newPath === null && section.oldPath !== null)) {
194
210
  return 'deleted';
195
211
  }
212
+
196
213
  if (section.renamed) {
197
214
  return 'renamed';
198
215
  }
216
+
199
217
  return section.copied ? 'copied' : 'modified';
200
218
  }
201
219
 
202
220
  function linesOf(text: string, limits: Required<PatchLimits>): { lines: string[]; truncated: boolean } {
203
221
  const lines: string[] = [];
204
222
  let start = 0;
223
+
205
224
  while (lines.length < limits.maxLines && start <= limits.maxCharacters) {
206
225
  const end = text.indexOf('\n', start);
226
+
207
227
  if (end === -1) {
208
228
  lines.push(text.slice(start).replace(/\r$/, ''));
229
+
209
230
  return { lines, truncated: false };
210
231
  }
232
+
211
233
  lines.push(text.slice(start, end).replace(/\r$/, ''));
212
234
  start = end + 1;
213
235
  }
236
+
214
237
  // Stopping right after a final newline leaves nothing unread.
215
238
  return { lines, truncated: start < text.length };
216
239
  }
@@ -228,33 +251,42 @@ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayo
228
251
  let section: Section | undefined;
229
252
  let hunk: OpenHunk | undefined;
230
253
  let binaryData = false;
254
+
231
255
  const seal = (): void => {
232
256
  if (hunk !== undefined && (hunk.oldLeft > 0 || hunk.newLeft > 0)) {
233
257
  hunk.layout.consistent = false;
234
258
  }
259
+
235
260
  hunk = undefined;
236
261
  };
262
+
237
263
  const open = (line: number, git: boolean): Section => {
238
264
  seal();
239
265
  binaryData = false;
240
266
  section = emptySection(line, git);
241
267
  sections.push(section);
268
+
242
269
  return section;
243
270
  };
271
+
244
272
  // A removed line can read `-- x`, so `---` starts a file only with `+++` under it.
245
273
  const startsFile = (index: number): boolean => source[index]!.startsWith('--- ') && (source[index + 1]?.startsWith('+++ ') ?? false);
274
+
246
275
  const push = (line: PatchLayoutLine, index: number): void => {
247
276
  lines.push(line);
277
+
248
278
  if (section !== undefined) {
249
279
  section.lastLine = index;
250
280
  }
251
281
  };
282
+
252
283
  for (let index = 0; index < source.length; index++) {
253
284
  const line = source[index]!;
254
285
  const remaining = hunk !== undefined && (hunk.oldLeft > 0 || hunk.newLeft > 0);
255
286
  const gitStart = line.startsWith('diff --git ');
256
287
  const header = line.startsWith('@@') ? parseHunkHeader(line) : undefined;
257
288
  const fileStart = startsFile(index) && (!remaining || source[index + 2]?.startsWith('@@') === true);
289
+
258
290
  if (remaining && !gitStart && header === undefined && !fileStart) {
259
291
  if (isContent(line) || line === '' || line.startsWith('\\')) {
260
292
  push(contentLine(line, hunk!, section!), index);
@@ -262,14 +294,18 @@ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayo
262
294
  hunk!.layout.consistent = false;
263
295
  push(OTHER, index);
264
296
  }
297
+
265
298
  continue;
266
299
  }
300
+
267
301
  if (gitStart) {
268
302
  const created = open(index, true);
269
303
  ({ oldPath: created.oldPath, newPath: created.newPath, prefixed: created.prefixed } = gitPaths(line.slice('diff --git '.length)));
270
304
  push(FILE_HEADER, index);
305
+
271
306
  continue;
272
307
  }
308
+
273
309
  if (fileStart) {
274
310
  const reuse = section !== undefined && section.git && !section.named && section.hunks.length === 0;
275
311
  const target = reuse ? section! : open(index, false);
@@ -278,8 +314,10 @@ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayo
278
314
  push(FILE_HEADER, index);
279
315
  push(FILE_HEADER, index + 1);
280
316
  index++;
317
+
281
318
  continue;
282
319
  }
320
+
283
321
  if (header !== undefined) {
284
322
  seal();
285
323
  const target = section ?? open(index, false);
@@ -287,8 +325,10 @@ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayo
287
325
  target.hunks.push(layout);
288
326
  hunk = { layout, oldLeft: header.oldCount, newLeft: header.newCount, oldNumber: header.oldStart, newNumber: header.newStart };
289
327
  push(HUNK_HEADER, index);
328
+
290
329
  continue;
291
330
  }
331
+
292
332
  if (section === undefined) {
293
333
  push(OTHER, index);
294
334
  } else if (hunk !== undefined && line.startsWith('\\')) {
@@ -312,7 +352,9 @@ export function patchLayoutOf(text: string, limits: PatchLimits = {}): PatchLayo
312
352
  push(OTHER, index);
313
353
  }
314
354
  }
355
+
315
356
  seal();
357
+
316
358
  return {
317
359
  lines,
318
360
  truncated,
@@ -338,21 +380,26 @@ function contentLine(line: string, hunk: OpenHunk, section: Section): PatchLayou
338
380
  if (line.startsWith('\\')) {
339
381
  return NO_NEWLINE;
340
382
  }
383
+
341
384
  const kind = line.startsWith('+') ? 'added' : line.startsWith('-') ? 'removed' : 'context';
342
385
  const takesOld = kind !== 'added';
343
386
  const takesNew = kind !== 'removed';
387
+
344
388
  if ((takesOld && hunk.oldLeft === 0) || (takesNew && hunk.newLeft === 0)) {
345
389
  hunk.layout.consistent = false;
346
390
  }
391
+
347
392
  if (kind === 'added') {
348
393
  section.added++;
349
394
  } else if (kind === 'removed') {
350
395
  section.removed++;
351
396
  }
397
+
352
398
  const oldNumber = takesOld ? hunk.oldNumber++ : null;
353
399
  const newNumber = takesNew ? hunk.newNumber++ : null;
354
400
  hunk.oldLeft = takesOld ? Math.max(0, hunk.oldLeft - 1) : hunk.oldLeft;
355
401
  hunk.newLeft = takesNew ? Math.max(0, hunk.newLeft - 1) : hunk.newLeft;
402
+
356
403
  return { role: kind, old: oldNumber, new: newNumber };
357
404
  }
358
405
 
package/src/peek-model.ts CHANGED
@@ -30,7 +30,9 @@ function columnsOf(range: Range, line: number, length: number): NameRange | null
30
30
  if (range.start.line !== line) {
31
31
  return null;
32
32
  }
33
+
33
34
  const end = Math.min(length, range.end.line === line ? range.end.character : length);
35
+
34
36
  return range.start.character < end ? { start: range.start.character, end } : null;
35
37
  }
36
38
 
@@ -39,9 +41,11 @@ function trimmedNameOf(raw: string, range: Range, line: number): NameRange | nul
39
41
  const leading = raw.length - raw.trimStart().length;
40
42
  const columns = columnsOf(range, line, raw.length);
41
43
  const length = raw.trim().length;
44
+
42
45
  if (columns === null || columns.start < leading || columns.start >= leading + length) {
43
46
  return null;
44
47
  }
48
+
45
49
  return { start: columns.start - leading, end: Math.min(length, columns.end - leading) };
46
50
  }
47
51
 
@@ -52,22 +56,28 @@ function trimmedNameOf(raw: string, range: Range, line: number): NameRange | nul
52
56
  export function peekFilesOf(locations: readonly Location[], current: string, textOf: (uri: string) => string | null): PeekFile[] {
53
57
  const order: string[] = [];
54
58
  const byFile = new Map<string, Location[]>();
59
+
55
60
  for (const location of locations) {
56
61
  if (!byFile.has(location.uri)) {
57
62
  order.push(location.uri);
58
63
  byFile.set(location.uri, []);
59
64
  }
65
+
60
66
  byFile.get(location.uri)!.push(location);
61
67
  }
68
+
62
69
  const uris = [...order.filter((uri) => uri === current), ...order.filter((uri) => uri !== current)];
70
+
63
71
  return uris.map((uri, fileIndex) => {
64
72
  const text = textOf(uri);
65
73
  const lines = text === null ? null : text.split(/\r\n|\r|\n/);
74
+
66
75
  const places = [...byFile.get(uri)!]
67
76
  .sort((left, right) => left.range.start.line - right.range.start.line || left.range.start.character - right.range.start.character)
68
77
  .map((location, index) => {
69
78
  const line = location.range.start.line;
70
79
  const raw = lines?.[line] ?? '';
80
+
71
81
  return {
72
82
  id: `${fileIndex}:${index}`,
73
83
  location,
@@ -76,6 +86,7 @@ export function peekFilesOf(locations: readonly Location[], current: string, tex
76
86
  name: lines === null ? null : trimmedNameOf(raw, location.range, line)
77
87
  };
78
88
  });
89
+
79
90
  return { uri, places };
80
91
  });
81
92
  }
@@ -97,6 +108,7 @@ export function snippetOf(text: string, line: number, range: Range): PeekSnippet
97
108
  const lines = text.split(/\r\n|\r|\n/);
98
109
  const startLine = Math.max(0, line - BEFORE);
99
110
  const end = Math.min(lines.length, line + AFTER + 1);
111
+
100
112
  return { startLine, text: lines.slice(startLine, end).join('\n'), active: line - startLine, name: columnsOf(range, line, (lines[line] ?? '').length) };
101
113
  }
102
114
 
@@ -110,6 +122,7 @@ export function definitionSnippetOf(text: string, line: number, declaration: Ran
110
122
  const lines = text.split(/\r\n|\r|\n/);
111
123
  const startLine = declaration === null ? Math.max(0, line - 2) : Math.min(declaration.start.line, line);
112
124
  const end = Math.min(lines.length - 1, declaration === null ? line + DEFINITION_LINES / 2 : Math.min(declaration.end.line, startLine + DEFINITION_LINES));
125
+
113
126
  return { startLine, text: lines.slice(startLine, end + 1).join('\n'), active: line - startLine, name: columnsOf(range, line, (lines[line] ?? '').length) };
114
127
  }
115
128
 
@@ -120,19 +133,25 @@ export function definitionSnippetOf(text: string, line: number, declaration: Ran
120
133
  export function distinguishingFolders(paths: readonly string[]): string[] {
121
134
  const folders = paths.map((path) => path.split('/').slice(0, -1));
122
135
  const names = paths.map((path) => path.slice(path.lastIndexOf('/') + 1));
136
+
123
137
  return paths.map((path, index) => {
124
138
  const same = paths.map((_, other) => other).filter((other) => names[other] === names[index] && paths[other] !== path);
139
+
125
140
  if (same.length === 0) {
126
141
  return '';
127
142
  }
143
+
128
144
  const rivals = [...new Set(same.map((other) => paths[other]!))].map((other) => folders[paths.indexOf(other)]!);
129
145
  const mine = folders[index]!;
146
+
130
147
  for (let depth = 1; depth <= mine.length; depth++) {
131
148
  const tail = mine.slice(-depth).join('/');
149
+
132
150
  if (rivals.every((rival) => rival.slice(-depth).join('/') !== tail)) {
133
151
  return tail;
134
152
  }
135
153
  }
154
+
136
155
  return mine.join('/');
137
156
  });
138
157
  }
@@ -140,8 +159,10 @@ export function distinguishingFolders(paths: readonly string[]): string[] {
140
159
  /* The column a character stands at once tabs are expanded to their stops, which is where a monospace face draws it. */
141
160
  export function visualColumnOf(line: string, character: number, tabSize: number): number {
142
161
  let column = 0;
162
+
143
163
  for (let i = 0; i < Math.min(character, line.length); i++) {
144
164
  column += line[i] === '\t' ? tabSize - (column % tabSize) : 1;
145
165
  }
166
+
146
167
  return column;
147
168
  }