@adecore/editor-react 0.21.1 → 0.21.2

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 (109) hide show
  1. package/dist/AnchoredPopup.js +2 -3
  2. package/dist/HoverSections.js +5 -11
  3. package/dist/PeekPanel.js +6 -13
  4. package/dist/RenameCard.js +2 -2
  5. package/dist/SymbolPicker.js +2 -3
  6. package/dist/code-actions-model.js +9 -8
  7. package/dist/code-actions.d.ts +1 -1
  8. package/dist/code-actions.js +21 -20
  9. package/dist/code-authors.js +1 -1
  10. package/dist/code-vision.d.ts +1 -0
  11. package/dist/code-vision.js +17 -14
  12. package/dist/completion-call.js +2 -6
  13. package/dist/completion-model.d.ts +3 -2
  14. package/dist/completion-model.js +8 -9
  15. package/dist/completion.js +8 -10
  16. package/dist/context-menu.js +4 -5
  17. package/dist/definition-link.js +2 -1
  18. package/dist/diagnostics-model.d.ts +2 -0
  19. package/dist/diagnostics-model.js +8 -2
  20. package/dist/diagnostics.d.ts +1 -0
  21. package/dist/diagnostics.js +13 -12
  22. package/dist/editor-language.d.ts +2 -2
  23. package/dist/editor-language.js +10 -10
  24. package/dist/error-message.d.ts +1 -0
  25. package/dist/error-message.js +3 -0
  26. package/dist/folding.js +5 -24
  27. package/dist/history.d.ts +1 -0
  28. package/dist/history.js +9 -12
  29. package/dist/hover-content.d.ts +1 -0
  30. package/dist/hover-content.js +5 -4
  31. package/dist/hover.d.ts +2 -2
  32. package/dist/hover.js +10 -9
  33. package/dist/inlay-hints.js +4 -4
  34. package/dist/language-ids.js +1 -1
  35. package/dist/line-text.d.ts +3 -0
  36. package/dist/line-text.js +7 -0
  37. package/dist/navigation.js +9 -9
  38. package/dist/paths.d.ts +1 -0
  39. package/dist/paths.js +4 -0
  40. package/dist/peek.js +11 -4
  41. package/dist/project-language.js +15 -13
  42. package/dist/project-problems.d.ts +1 -2
  43. package/dist/project-problems.js +2 -4
  44. package/dist/refresher.js +11 -6
  45. package/dist/rename.d.ts +2 -2
  46. package/dist/rename.js +14 -12
  47. package/dist/review-group.d.ts +1 -0
  48. package/dist/review-group.js +10 -14
  49. package/dist/row-host.d.ts +13 -20
  50. package/dist/row-host.js +33 -60
  51. package/dist/semantic-model.js +4 -6
  52. package/dist/semantic-tokens.js +2 -4
  53. package/dist/signature.js +1 -1
  54. package/dist/snippet-session.js +3 -4
  55. package/dist/symbol-links.js +2 -2
  56. package/dist/symbol-picker-model.js +15 -9
  57. package/dist/symbol-picker.js +2 -1
  58. package/dist/symbols.d.ts +7 -1
  59. package/dist/symbols.js +22 -28
  60. package/dist/testing.js +3 -3
  61. package/dist/word-diff.js +2 -4
  62. package/dist/workspace-edit.js +7 -8
  63. package/package.json +5 -5
  64. package/src/AnchoredPopup.tsx +2 -3
  65. package/src/HoverSections.tsx +5 -11
  66. package/src/PeekPanel.tsx +6 -14
  67. package/src/RenameCard.tsx +2 -2
  68. package/src/SymbolPicker.tsx +3 -4
  69. package/src/code-actions-model.ts +10 -8
  70. package/src/code-actions.ts +36 -38
  71. package/src/code-authors.ts +2 -2
  72. package/src/code-vision.ts +19 -15
  73. package/src/completion-call.ts +2 -6
  74. package/src/completion-model.ts +10 -12
  75. package/src/completion.ts +8 -13
  76. package/src/context-menu.ts +4 -5
  77. package/src/definition-link.ts +2 -1
  78. package/src/diagnostics-model.ts +10 -2
  79. package/src/diagnostics.ts +15 -13
  80. package/src/editor-language.ts +11 -11
  81. package/src/error-message.ts +3 -0
  82. package/src/folding.ts +6 -26
  83. package/src/history.ts +11 -12
  84. package/src/hover-content.ts +6 -4
  85. package/src/hover.ts +10 -9
  86. package/src/inlay-hints.ts +4 -4
  87. package/src/language-ids.ts +1 -1
  88. package/src/line-text.ts +10 -0
  89. package/src/navigation.ts +11 -10
  90. package/src/paths.ts +5 -0
  91. package/src/peek.ts +11 -10
  92. package/src/project-language.ts +17 -16
  93. package/src/project-problems.ts +3 -6
  94. package/src/refresher.ts +12 -6
  95. package/src/rename.ts +16 -18
  96. package/src/review-group.ts +11 -15
  97. package/src/row-host.ts +54 -76
  98. package/src/semantic-model.ts +4 -6
  99. package/src/semantic-tokens.ts +2 -4
  100. package/src/signature.ts +1 -1
  101. package/src/snippet-session.ts +3 -4
  102. package/src/symbol-links.ts +2 -2
  103. package/src/symbol-picker-model.ts +17 -9
  104. package/src/symbol-picker.ts +2 -1
  105. package/src/symbols.ts +29 -28
  106. package/src/testing.ts +4 -2
  107. package/src/view-state.ts +2 -0
  108. package/src/word-diff.ts +2 -4
  109. package/src/workspace-edit.ts +7 -8
@@ -3,9 +3,8 @@ import { useLayoutEffect, useRef } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { placePopup } from './popup-placement.js';
5
5
  /*
6
- * A card next to a character of the editor, in a layer of the page itself: a canvas puts a scale on its
7
- * nodes, and a card inside one would be drawn at that scale and cut off by the node's edge. It is
8
- * placed from the character's screen rectangle, so it is right at any zoom, and it never leaves the window.
6
+ * A card next to a character of the editor, portaled to the page: inside a scaled canvas node it would be
7
+ * drawn at that scale and clipped by the node. Placed from the character's screen rectangle, so it holds at any zoom.
9
8
  */
10
9
  export function AnchoredPopup({ rect, className, children, placement, onPointerEnter, onPointerLeave }) {
11
10
  const element = useRef(null);
@@ -1,10 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEditorRendering } from './rendering-context.js';
3
2
  import { useEffect, useState } from 'react';
4
- import { Markdown } from './Markdown.js';
5
3
  import { Tooltip } from '@adecore/ui';
6
- import { hoverSectionsOf, markdownParts } from './hover-content.js';
7
4
  import { BaselineStatus } from './BaselineStatus.js';
5
+ import { hoverSectionsOf, markdownParts } from './hover-content.js';
6
+ import { Markdown } from './Markdown.js';
7
+ import { useEditorRendering } from './rendering-context.js';
8
8
  import { declaredNameOf, linkTypeNames } from './symbol-links.js';
9
9
  /* Code as the viewer colors it, plain until the grammar is in so the card never changes size under the pointer by much. */
10
10
  export function Signature({ code, language, onName }) {
@@ -53,20 +53,14 @@ export function SymbolSections({ text, onName }) {
53
53
  function phpWithoutOpenTag(html) {
54
54
  return html.replace(/<span[^>]*>&#x3C;\?<\/span><span[^>]*>php<\/span>(<span[^>]*>) /, '$1');
55
55
  }
56
- /*
57
- * Where a suggestion comes from, such as `use Raxos\Database\Orm\Attribute\PrimaryKey`, on one line that gives
58
- * way from its start, so the end of the name stays readable and a word is never cut in two. The full text is in the tooltip.
59
- */
56
+ /* Where a suggestion comes from, such as a PHP `use` line, cut from its start so the end of the name stays readable. */
60
57
  export function SourceLine({ text, language }) {
61
58
  const { theme, highlight } = useEditorRendering();
62
59
  const [html, setHtml] = useState(null);
63
60
  const highlighted = language === 'php' && text.startsWith('use ');
64
61
  const key = `${theme}\0${text}`;
65
62
  useEffect(() => {
66
- if (!highlighted) {
67
- return;
68
- }
69
- if (highlight === undefined) {
63
+ if (!highlighted || highlight === undefined) {
70
64
  return;
71
65
  }
72
66
  let alive = true;
package/dist/PeekPanel.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEditorRendering } from './rendering-context.js';
3
2
  import { useEffect, useMemo, useRef, useState } from 'react';
4
3
  import { createPortal } from 'react-dom';
5
4
  import { useTranslation } from 'react-i18next';
@@ -7,11 +6,12 @@ import { ChevronDown, ChevronRight, X } from 'lucide-react';
7
6
  import { fileUriToPath } from '@adecore/lsp';
8
7
  import { FileIcon, IconButton, Tooltip } from '@adecore/ui';
9
8
  import { formatNumber } from '@adecore/ui/format';
10
- import { basenameOf } from './paths.js';
9
+ import { basenameOf, relativeTo } from './paths.js';
11
10
  import { shikiLanguageOfPath } from './language-ids.js';
12
11
  import { PathText } from './PathText.js';
13
12
  import { PEEK_HEIGHT } from './peek.js';
14
13
  import { distinguishingFolders, visualColumnOf } from './peek-model.js';
14
+ import { useEditorRendering } from './rendering-context.js';
15
15
  const TAB_SIZE = 4;
16
16
  const CODE = 'font-mono text-(length:--code-font-size) [tab-size:4] [&_.line]:block [&_.line]:h-(--code-line-height) [&_.line]:leading-(--code-line-height) [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-0 [&_pre]:whitespace-pre [&_code]:font-mono';
17
17
  /* The code around the active reference, drawn like the editor's own rows: its gutter, the line the reference is on and the name marked. */
@@ -23,10 +23,7 @@ function Preview({ view }) {
23
23
  const path = preview === null ? null : fileUriToPath(preview.uri);
24
24
  const key = preview === null ? '' : `${theme}\0${path}\0${preview.text}`;
25
25
  useEffect(() => {
26
- if (preview === null) {
27
- return;
28
- }
29
- if (highlight === undefined) {
26
+ if (preview === null || highlight === undefined) {
30
27
  return;
31
28
  }
32
29
  let alive = true;
@@ -57,9 +54,6 @@ function columnsOf(line, name) {
57
54
  const start = visualColumnOf(line, name.start, TAB_SIZE);
58
55
  return { start, end: visualColumnOf(line, name.end, TAB_SIZE) };
59
56
  }
60
- function relativeTo(path, folder) {
61
- return path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
62
- }
63
57
  function folderOf(path) {
64
58
  return path.slice(0, Math.max(0, path.lastIndexOf('/')));
65
59
  }
@@ -90,10 +84,9 @@ function Places({ language, view }) {
90
84
  }) }));
91
85
  }
92
86
  /*
93
- * The references of a name drawn in the row the editor leaves under the line: the file of the active
94
- * reference and where it is, the code around it on the left and every place on the right. It lives in a
95
- * portal into the editor's own row, which the editor makes again when it scrolls back into view. It has
96
- * the editor's own ground and no edge of its own, so it reads as part of the file.
87
+ * The references of a name in the row the editor leaves under the line: the code around the active one on
88
+ * the left and every place on the right. It portals into that row, which the editor makes again when it
89
+ * scrolls back into view, and has no edge of its own, so it reads as part of the file.
97
90
  */
98
91
  export function PeekPanel({ language, view }) {
99
92
  const { t } = useTranslation('editor');
@@ -6,7 +6,7 @@ import { Sparkles } from 'lucide-react';
6
6
  import { fileUriToPath } from '@adecore/lsp';
7
7
  import { Button, Icon } from '@adecore/ui';
8
8
  import { formatNumber } from '@adecore/ui/format';
9
- import { basenameOf } from './paths.js';
9
+ import { basenameOf, relativeTo } from './paths.js';
10
10
  import { AnchoredPopup } from './AnchoredPopup.js';
11
11
  const CARD_ATTRIBUTE = 'data-rename-card';
12
12
  /* Whether the focus moved to something that belongs to the rename, which is not leaving it. */
@@ -37,7 +37,7 @@ function Preview({ language, view }) {
37
37
  const folder = language.project.folder;
38
38
  return (_jsxs("div", { className: "flex w-[520px] max-w-full flex-col", onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "max-h-[320px] overflow-y-auto", children: view.files.map((file) => {
39
39
  const path = fileUriToPath(file.uri);
40
- const shown = path === null ? file.uri : path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path;
40
+ const shown = path === null ? file.uri : relativeTo(path, folder);
41
41
  return (_jsxs("div", { className: "border-b border-border last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 text-xs", children: [_jsx("span", { className: "font-medium", children: path === null ? shown : basenameOf(path) }), _jsx("span", { className: "min-w-0 truncate text-text-faint", children: shown })] }), _jsx("div", { className: "flex flex-col gap-px px-1 pb-1.5 font-mono text-code", children: file.rows.map((row) => (_jsxs("div", { className: "flex flex-col gap-px", children: [_jsxs("div", { className: "flex gap-2 rounded-sm bg-status-error/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0 text-right text-text-faint", children: row.line }), _jsx("span", { className: "min-w-0 truncate", children: row.before })] }), _jsxs("div", { className: "flex gap-2 rounded-sm bg-status-idle/10 px-1.5 whitespace-pre", children: [_jsx("span", { className: "w-8 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.after })] })] }, row.line))) })] }, file.uri));
42
42
  }) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => language.rename.cancel(), children: t('language.rename.cancel') }), _jsx(Button, { size: "xs", variant: "primary", onClick: () => void language.rename.confirm(), children: t('language.rename.apply') })] })] }));
43
43
  }
@@ -3,11 +3,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { AtSign, Hash } from 'lucide-react';
6
- import { Icon } from '@adecore/ui';
7
- import { formatShortcut } from '@adecore/ui';
6
+ import { fileUriToPath } from '@adecore/lsp';
7
+ import { formatShortcut, Icon } from '@adecore/ui';
8
8
  import { formatNumber } from '@adecore/ui/format';
9
9
  import { basenameOf } from './paths.js';
10
- import { fileUriToPath } from '@adecore/lsp';
11
10
  import { filterEntries, groupEntries, letterOfKind, modeOf, toneOfKind } from './symbol-picker-model.js';
12
11
  const TONES = {
13
12
  callable: 'bg-accent-soft text-accent',
@@ -5,6 +5,13 @@ import { entriesOf, renamesOf } from './workspace-edit.js';
5
5
  export const ACTION_GROUPS = ['quickfix', 'rewrite', 'extract', 'inline', 'move', 'refactor', 'source', 'other'];
6
6
  /* The order of Refactor This: what changes the structure of the code first, the rewrites after it. */
7
7
  export const REFACTOR_GROUPS = ['extract', 'inline', 'move', 'rewrite', 'refactor'];
8
+ /* Entries in the order of `order`, the preferred actions of each group first. */
9
+ function inGroupOrder(entries, order) {
10
+ return order.flatMap((group) => {
11
+ const own = entries.filter((entry) => entry.group === group);
12
+ return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
13
+ });
14
+ }
8
15
  /* The kinds nest by dots: `refactor.extract.function` is an extraction and `source.organizeImports` a source action. */
9
16
  export function groupOf(kind) {
10
17
  if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
@@ -32,10 +39,7 @@ export function actionsOf(result, order = ACTION_GROUPS) {
32
39
  .map((item) => (typeof item.command === 'string' ? { title: item.title, command: item } : item))
33
40
  .filter((action) => action.disabled === undefined)
34
41
  .map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
35
- return order.flatMap((group) => {
36
- const own = entries.filter((entry) => entry.group === group);
37
- return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
38
- });
42
+ return inGroupOrder(entries, order);
39
43
  }
40
44
  /* Entries from several requests as one list: the same order as a single answer, and an id of its own for each row. Two actions with a title and kind alike are one. */
41
45
  export function mergeEntries(lists) {
@@ -48,10 +52,7 @@ export function mergeEntries(lists) {
48
52
  seen.add(key);
49
53
  return true;
50
54
  });
51
- return ACTION_GROUPS.flatMap((group) => {
52
- const own = unique.filter((entry) => entry.group === group);
53
- return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
54
- }).map((entry, index) => ({ ...entry, id: String(index) }));
55
+ return inGroupOrder(unique, ACTION_GROUPS).map((entry, index) => ({ ...entry, id: String(index) }));
55
56
  }
56
57
  /* Whether the lightbulb offers it: a source action such as organizing imports is a command of its own and never a hint. */
57
58
  export function isHint(entry) {
@@ -3,7 +3,6 @@ import type { Problem } from './diagnostics-model.ts';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { type Timers } from './timers.ts';
5
5
  export declare class CodeActionsFeature {
6
- private tell;
7
6
  private readonly language;
8
7
  private readonly refresher;
9
8
  private asking;
@@ -25,6 +24,7 @@ export declare class CodeActionsFeature {
25
24
  private refusalOf;
26
25
  apply(entry: ActionEntry): Promise<void>;
27
26
  private startRename;
27
+ private tell;
28
28
  private fail;
29
29
  private say;
30
30
  }
@@ -3,6 +3,7 @@ import { ACTION_GROUPS, REFACTOR_GROUPS, actionsOf, diagnosticsAt, fixableOnLine
3
3
  import { Refresher } from './refresher.js';
4
4
  import { isShortcut } from './shortcut-keys.js';
5
5
  import { realTimers } from './timers.js';
6
+ import { messageOf } from './error-message.js';
6
7
  const METHOD = 'textDocument/codeAction';
7
8
  const PAUSE_MS = 250;
8
9
  const INVOKED = 1;
@@ -19,15 +20,11 @@ function isRefusal(error) {
19
20
  return error instanceof LspError && error.code === ErrorCodes.InternalError;
20
21
  }
21
22
  /*
22
- * What the language servers offer to do at the caret or the selection: a lightbulb in the gutter on the line
23
- * that has something, a list under the caret (Mod+.) grouped by kind with the change an action would make
24
- * shown under it, the Quick fix of a problem's card, and the commands that organize imports and format the file.
25
- * An action is resolved before it is applied, since a server fills in its edit only when asked for it.
23
+ * What the servers offer at the caret or the selection: the gutter's lightbulb, the list under the caret
24
+ * with a preview of each change, a problem's Quick fix, organize imports and format. An action is resolved
25
+ * before it is applied, since a server fills in its edit only when asked for it.
26
26
  */
27
27
  export class CodeActionsFeature {
28
- tell(kind, title) {
29
- this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
30
- }
31
28
  language;
32
29
  refresher;
33
30
  /* While a list is being asked for, the lightbulb waits: both are the same request and the newer one would cancel the older. */
@@ -90,7 +87,7 @@ export class CodeActionsFeature {
90
87
  async organizeImports() {
91
88
  const { editor } = this.language;
92
89
  const everything = { start: { line: 0, character: 0 }, end: editor.positionAt(Number.MAX_SAFE_INTEGER) };
93
- const entries = await this.request(everything, ['source.organizeImports'], INVOKED);
90
+ const entries = await this.request(everything, INVOKED, { only: ['source.organizeImports'] });
94
91
  const entry = entries?.find((candidate) => candidate.action.kind?.startsWith('source.organizeImports') === true);
95
92
  if (entry === undefined) {
96
93
  this.tell('success', this.say(entries === null ? 'unavailable' : 'organized'));
@@ -116,9 +113,10 @@ export class CodeActionsFeature {
116
113
  }
117
114
  /* The actions of one kind for the selection, as entries a menu can list and `apply` runs. */
118
115
  list(only) {
119
- return this.supported
120
- ? this.request(this.language.editor.getSelection(), only, AUTOMATIC, undefined, undefined, only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS)
121
- : Promise.resolve(null);
116
+ if (!this.supported) {
117
+ return Promise.resolve(null);
118
+ }
119
+ return this.request(this.language.editor.getSelection(), AUTOMATIC, { only, order: only.includes('refactor') ? REFACTOR_GROUPS : ACTION_GROUPS });
122
120
  }
123
121
  async ask(options) {
124
122
  if (!this.supported) {
@@ -132,7 +130,7 @@ export class CodeActionsFeature {
132
130
  try {
133
131
  entries = options.withLine
134
132
  ? await this.requestWithLine(options.range, INVOKED)
135
- : await this.request(options.range, options.only, INVOKED, undefined, options.diagnostics, options.groups);
133
+ : await this.request(options.range, INVOKED, { only: options.only, about: options.diagnostics, order: options.groups });
136
134
  }
137
135
  finally {
138
136
  this.asking = false;
@@ -164,14 +162,14 @@ export class CodeActionsFeature {
164
162
  });
165
163
  }
166
164
  /* Asks the servers; null when they could not answer, with the reason said aloud for a request a person made. */
167
- async request(range, only, triggerKind, signal, about, order) {
165
+ async request(range, triggerKind, options = {}) {
168
166
  const { project, uri, diagnostics } = this.language;
167
+ const { only, signal, order } = options;
169
168
  try {
169
+ const about = options.about ??
170
+ diagnosticsAt(diagnostics.problems.map((problem) => problem.diagnostic), range);
170
171
  const context = {
171
- diagnostics: [
172
- ...(about ??
173
- diagnosticsAt(diagnostics.problems.map((problem) => problem.diagnostic), range))
174
- ],
172
+ diagnostics: [...about],
175
173
  ...(only ? { only: [...only] } : {}),
176
174
  triggerKind: triggerKind
177
175
  };
@@ -191,7 +189,7 @@ export class CodeActionsFeature {
191
189
  * elsewhere on its line too, since the fix of an error is wanted from anywhere on the line it is on.
192
190
  */
193
191
  async requestWithLine(range, triggerKind, signal) {
194
- const entries = await this.request(range, undefined, triggerKind, signal);
192
+ const entries = await this.request(range, triggerKind, { signal });
195
193
  if (entries === null || range.start.line !== range.end.line) {
196
194
  return entries;
197
195
  }
@@ -200,7 +198,7 @@ export class CodeActionsFeature {
200
198
  return entries;
201
199
  }
202
200
  const onLine = fixableOnLine(all, range.start.line).slice(0, LINE_PROBLEMS);
203
- const fixes = await Promise.all(onLine.map((diagnostic) => this.request(diagnostic.range, ['quickfix'], triggerKind, signal, [diagnostic])));
201
+ const fixes = await Promise.all(onLine.map((diagnostic) => this.request(diagnostic.range, triggerKind, { only: ['quickfix'], signal, about: [diagnostic] })));
204
202
  return mergeEntries([entries, ...fixes.map((list) => list ?? [])]);
205
203
  }
206
204
  async hint(signal) {
@@ -315,9 +313,12 @@ export class CodeActionsFeature {
315
313
  await this.language.rename.start();
316
314
  }
317
315
  }
316
+ tell(kind, title) {
317
+ this.language.project.host.notify?.({ id: TOAST_ID, kind, title });
318
+ }
318
319
  fail(error) {
319
320
  if (!(error instanceof StaleResultError)) {
320
- this.tell('error', this.say('failed', { message: error instanceof Error ? error.message : String(error) }));
321
+ this.tell('error', this.say('failed', { message: messageOf(error) }));
321
322
  }
322
323
  }
323
324
  say(key, options) {
@@ -1,6 +1,6 @@
1
1
  import { diffLines, splitLines } from '@adecore/merge';
2
2
  import { formatNumber } from '@adecore/ui/format';
3
- /* What a line of the text in the editor is: the index of the commit that wrote it, or this for a line no commit holds yet. */
3
+ /* A line no commit holds yet; every other line maps to the index of the commit that wrote it. */
4
4
  export const UNCOMMITTED = -1;
5
5
  /*
6
6
  * The commit of every line of the text in the editor. Blame was run for the text on disk, so a line
@@ -42,6 +42,7 @@ export declare class CodeVisionFeature {
42
42
  private count;
43
43
  private publish;
44
44
  private rows;
45
+ private authorshipOf;
45
46
  private mapOfBlame;
46
47
  private showAuthors;
47
48
  closeAuthors(restoreFocus?: boolean): void;
@@ -11,11 +11,10 @@ const SCROLL_PAUSE_MS = 150;
11
11
  /* Past this many lines a file gets no rows at all, which is also what keeps blame and the walk of symbols cheap. */
12
12
  export const MAX_LINES = 20_000;
13
13
  /*
14
- * The rows above declarations: how often each is used and, once git has been asked, who wrote it. The
15
- * declarations come from the symbols the servers know, and a row holds its height from the first
16
- * answer on, empty until its entries are in, so the text never moves when they arrive. Counts are asked
17
- * lazily, for the declarations in view and a margin around them, a few at a time, and only after the
18
- * text has settled; an old count stays on its row until the new one is in.
14
+ * The rows above declarations: how often each is used and who wrote it. A row holds its height from the
15
+ * first answer on, empty until its entries are in, so the text never moves when they arrive. Counts are
16
+ * asked for the declarations near the view, a few at a time, once the text has settled; an old count
17
+ * stays on its row until the new one is in.
19
18
  */
20
19
  export class CodeVisionFeature {
21
20
  language;
@@ -185,23 +184,27 @@ export class CodeVisionFeature {
185
184
  });
186
185
  }
187
186
  if (mapped !== null && declaration.authors) {
188
- let authorship = this.authorships.get(declaration.id);
189
- if (authorship === undefined) {
190
- authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
191
- this.authorships.set(declaration.id, authorship);
192
- }
193
- const shown = authorship;
187
+ const authorship = this.authorshipOf(declaration, commits, mapped);
194
188
  entries.push({
195
189
  id: 'authors',
196
- text: authorsText(shown, this.language.project.i18n),
197
- ...(shown.authors.length > 0 ? { icon: shown.authors.length > 1 ? 'users' : 'user' } : {}),
198
- activate: (anchor) => this.showAuthors(declaration, shown, anchor)
190
+ text: authorsText(authorship, this.language.project.i18n),
191
+ ...(authorship.authors.length > 0 ? { icon: authorship.authors.length > 1 ? 'users' : 'user' } : {}),
192
+ activate: (anchor) => this.showAuthors(declaration, authorship, anchor)
199
193
  });
200
194
  }
201
195
  rows.push({ id: declaration.id, line: declaration.line, entries });
202
196
  }
203
197
  return rows;
204
198
  }
199
+ authorshipOf(declaration, commits, mapped) {
200
+ const known = this.authorships.get(declaration.id);
201
+ if (known !== undefined) {
202
+ return known;
203
+ }
204
+ const authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
205
+ this.authorships.set(declaration.id, authorship);
206
+ return authorship;
207
+ }
205
208
  mapOfBlame(source) {
206
209
  if (this.mapped?.blame !== source.blame) {
207
210
  this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
@@ -1,4 +1,4 @@
1
- import { CLASS_KIND, CONSTRUCTOR_KIND, FUNCTION_KIND, METHOD_KIND, SNIPPET_FORMAT } from './completion-model.js';
1
+ import { CLASS_KIND, SNIPPET_FORMAT, SOURCE_LINE, isCallableKind } from './completion-model.js';
2
2
  /* What the platform's editors send to ask for parameter info once an item is in; the card is ours to open. */
3
3
  export const PARAMETER_HINTS_COMMAND = 'editor.action.triggerParameterHints';
4
4
  const CALL_LANGUAGES = new Set(['typescript', 'typescriptreact', 'javascript', 'javascriptreact', 'vue', 'php', 'python']);
@@ -9,13 +9,9 @@ const TYPE_OPERATOR = /\b(?:typeof|keyof|instanceof|implements|satisfies|as)\s+[
9
9
  const AFTER_NEW = /\bnew\s+[\w$.\\]*$/;
10
10
  const EXISTING_CALL = /^\s*\(/;
11
11
  const SIGNATURE_KIND_PREFIX = /^\([a-z]+(?: [a-z]+)*\)\s+(?!=>)/;
12
- const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
13
12
  /* Whether the item is something that is called where it stands: a function, a method, a constructor, or a class after `new`. */
14
13
  export function isCallItem(item, lineBefore) {
15
- if (item.kind === METHOD_KIND || item.kind === FUNCTION_KIND || item.kind === CONSTRUCTOR_KIND) {
16
- return true;
17
- }
18
- return item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore);
14
+ return isCallableKind(item.kind) || (item.kind === CLASS_KIND && AFTER_NEW.test(lineBefore));
19
15
  }
20
16
  /* Whether the parameter list in a signature has anything in it; null when the text names no list. */
21
17
  function listedParameters(signature) {
@@ -1,5 +1,5 @@
1
1
  import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
2
- import type { CompletionItem, CompletionResult, InsertReplaceEdit, MarkupContent, TextEdit } from '@adecore/lsp';
2
+ import type { CompletionItem, CompletionResult, MarkupContent } from '@adecore/lsp';
3
3
  import type { CompletionDocs } from './popups.ts';
4
4
  import { type SnippetStop } from './snippet.ts';
5
5
  export declare function isIdentifierCharacter(character: string): boolean;
@@ -32,6 +32,7 @@ export declare const FUNCTION_KIND = 3;
32
32
  export declare const CONSTRUCTOR_KIND = 4;
33
33
  export declare const CLASS_KIND = 7;
34
34
  export declare const SNIPPET_FORMAT = 2;
35
+ export declare function isCallableKind(kind: number | undefined): boolean;
35
36
  export interface Insertion {
36
37
  readonly range: EditorRange;
37
38
  readonly text: string;
@@ -40,8 +41,8 @@ export interface Insertion {
40
41
  export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string): Insertion;
41
42
  export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion): EditorContentChange[];
42
43
  export declare function documentationText(documentation: string | MarkupContent | undefined): string;
44
+ export declare const SOURCE_LINE: RegExp;
43
45
  export declare function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null;
44
- export type { InsertReplaceEdit, TextEdit };
45
46
  export type KindTone = 'callable' | 'value' | 'type' | 'other';
46
47
  export declare function kindLetterOf(kind: number | undefined): string;
47
48
  export declare function kindToneOf(kind: number | undefined): KindTone;
@@ -1,5 +1,5 @@
1
1
  import { comparePositions } from './diagnostics-model.js';
2
- import { isEmptyHover, splitSignatures } from './hover-content.js';
2
+ import { escapeMarkdown, isEmptyHover, splitSignatures } from './hover-content.js';
3
3
  import { parseSnippet, tabOrder } from './snippet.js';
4
4
  const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
5
5
  export function isIdentifierCharacter(character) {
@@ -224,6 +224,9 @@ export const FUNCTION_KIND = 3;
224
224
  export const CONSTRUCTOR_KIND = 4;
225
225
  export const CLASS_KIND = 7;
226
226
  export const SNIPPET_FORMAT = 2;
227
+ export function isCallableKind(kind) {
228
+ return kind === METHOD_KIND || kind === FUNCTION_KIND || kind === CONSTRUCTOR_KIND;
229
+ }
227
230
  /* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
228
231
  function withoutCall(snippet) {
229
232
  const open = snippet.search(/(?<!\\)\(/);
@@ -244,8 +247,7 @@ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
244
247
  return { range, text: raw, stops: [] };
245
248
  }
246
249
  const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
247
- const callable = item.kind === METHOD_KIND || item.kind === FUNCTION_KIND || item.kind === CONSTRUCTOR_KIND;
248
- const parsed = parseSnippet(followed && callable ? withoutCall(raw) : raw);
250
+ const parsed = parseSnippet(followed && isCallableKind(item.kind) ? withoutCall(raw) : raw);
249
251
  return { range, text: parsed.text, stops: tabOrder(parsed) };
250
252
  }
251
253
  /*
@@ -279,13 +281,10 @@ export function documentationText(documentation) {
279
281
  if (documentation === undefined) {
280
282
  return '';
281
283
  }
282
- return typeof documentation === 'string'
283
- ? documentation
284
- : documentation.kind === 'plaintext'
285
- ? documentation.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1')
286
- : documentation.value;
284
+ return typeof documentation === 'string' ? documentation : documentation.kind === 'plaintext' ? escapeMarkdown(documentation.value) : documentation.value;
287
285
  }
288
- const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
286
+ /* A detail line that only says where an item comes from: a PHP `use` or an auto import. */
287
+ export const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
289
288
  /*
290
289
  * What is said about a suggestion beside the list, in the shape a hover has: the signature it names and the
291
290
  * documentation split into signature, prose and tags. A first line that only says where the item comes from is
@@ -4,6 +4,8 @@ import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter,
4
4
  import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses } from './completion-call.js';
5
5
  import { RecentChoices, recentChoicesOf } from './recent-choices.js';
6
6
  import { shikiLanguageOf } from './language-ids.js';
7
+ import { textBeforeOf } from './line-text.js';
8
+ import { isEmptyRange } from './proposal-model.js';
7
9
  import { isShortcut } from './shortcut-keys.js';
8
10
  import { realTimers } from './timers.js';
9
11
  const METHOD = 'textDocument/completion';
@@ -14,9 +16,6 @@ const RESOLVE_DELAY_MS = 120;
14
16
  const PAGE = 8;
15
17
  const LINES_BACK = 60;
16
18
  const TOAST_ID = 'language-completion';
17
- function isEmpty(range) {
18
- return comparePositions(range.start, range.end) === 0;
19
- }
20
19
  /*
21
20
  * The suggestions that open while a word is typed. A single typed identifier character or a trigger
22
21
  * character of the server asks for the list; every character after that only filters the list that
@@ -95,7 +94,7 @@ export class CompletionFeature {
95
94
  item = await this.resolveNow(entry.item);
96
95
  }
97
96
  const caret = editor.getCaret();
98
- const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
97
+ const lineBefore = textBeforeOf(editor, caret);
99
98
  const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
100
99
  // A character that commits is typed after the name, so a call is not added before it.
101
100
  const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
@@ -277,12 +276,11 @@ export class CompletionFeature {
277
276
  const triggers = this.triggerCharacters();
278
277
  const last = Array.from(text).at(-1) ?? '';
279
278
  const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
280
- if (text.length > 0 && triggers.includes(last) && text.length === 1) {
279
+ if (text.length === 1 && triggers.includes(last)) {
281
280
  this.close();
282
281
  this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
283
282
  }
284
- else if (isEmpty(range) &&
285
- opensCompletion(this.language.editor.textInRange({ start: { line: range.start.line, character: 0 }, end: range.start }), text)) {
283
+ else if (isEmptyRange(range) && opensCompletion(textBeforeOf(this.language.editor, range.start), text)) {
286
284
  this.close();
287
285
  this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
288
286
  }
@@ -295,7 +293,7 @@ export class CompletionFeature {
295
293
  this.close();
296
294
  }
297
295
  }
298
- else if (text.length === 1 && typedIdentifier && isEmpty(range)) {
296
+ else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
299
297
  this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
300
298
  }
301
299
  }
@@ -359,7 +357,7 @@ export class CompletionFeature {
359
357
  this.server = project.host.serverNames?.(uri)[0] ?? '';
360
358
  if (!refresh) {
361
359
  const caret = editor.getCaret();
362
- const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
360
+ const lineBefore = textBeforeOf(editor, caret);
363
361
  const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
364
362
  this.start = { line: caret.line, character: caret.character - prefix.length };
365
363
  this.active = 0;
@@ -386,7 +384,7 @@ export class CompletionFeature {
386
384
  this.close();
387
385
  return;
388
386
  }
389
- const lineBefore = editor.textInRange({ start: { line: caret.line, character: 0 }, end: caret });
387
+ const lineBefore = textBeforeOf(editor, caret);
390
388
  // Most items of a list replace the same range, so the text under it is read once and not once per item.
391
389
  const typed = new Map();
392
390
  const textBetween = (range) => {
@@ -1,7 +1,6 @@
1
1
  /*
2
- * The editor's context menu. The editor only says where it was asked for; the menu is drawn by the page
3
- * (`EditorContextMenu`) from what the language servers support at the caret, and the refactors it lists
4
- * are asked for while it opens, so it never waits for them.
2
+ * The editor only says where its context menu was asked for; `EditorContextMenu` draws it from what the
3
+ * servers support at the caret. The refactors are asked for while it opens, so it never waits for them.
5
4
  */
6
5
  export class ContextMenuFeature {
7
6
  language;
@@ -9,7 +8,7 @@ export class ContextMenuFeature {
9
8
  constructor(language) {
10
9
  this.language = language;
11
10
  const off = language.editor.onContextMenu((menu) => {
12
- // A right click outside the selection moves the caret there first, as every code editor does, so the menu is about what was clicked.
11
+ // A right click outside the selection moves the caret first, so the menu is about what was clicked.
13
12
  if (!menu.inSelection) {
14
13
  language.editor.setCaret(menu.position);
15
14
  }
@@ -26,7 +25,7 @@ export class ContextMenuFeature {
26
25
  this.language.popups.setState({ menu: null });
27
26
  }
28
27
  }
29
- /* Cut, copy and paste go through the browser's own commands on the editor's input, which is what keeps the clipboard and the undo history one thing. */
28
+ /* The browser's own commands on the editor's input, which keep the clipboard and the undo history one thing. */
30
29
  clipboard(action) {
31
30
  const { editor } = this.language;
32
31
  editor.focus();
@@ -1,5 +1,6 @@
1
1
  import { isIdentifierCharacter } from './completion-model.js';
2
2
  import { locationsOf } from './hover-content.js';
3
+ import { lineTextOf } from './line-text.js';
3
4
  import { wordRangeAt } from './rename-model.js';
4
5
  /*
5
6
  * The name under the pointer drawn as a link while Cmd (Ctrl elsewhere) is held and a server knows where it
@@ -71,7 +72,7 @@ export class DefinitionLinkFeature {
71
72
  return;
72
73
  }
73
74
  const { editor, project, uri } = this.language;
74
- const line = editor.textInRange({ start: { line: pointer.line, character: 0 }, end: { line: pointer.line, character: Number.MAX_SAFE_INTEGER } });
75
+ const line = lineTextOf(editor, pointer.line);
75
76
  const word = isIdentifierCharacter(line[pointer.character] ?? '') ? wordRangeAt(line, pointer) : null;
76
77
  if (word === null) {
77
78
  this.clear();
@@ -1,11 +1,13 @@
1
1
  import type { EditorContentChange, EditorMarker, EditorMarkerSeverity, EditorPosition, EditorRange } from '@adecore/editor';
2
2
  import type { Diagnostic } from '@adecore/lsp';
3
+ import type { ProblemCounts } from './diagnostics.ts';
3
4
  export interface Problem {
4
5
  readonly diagnostic: Diagnostic;
5
6
  readonly server: string;
6
7
  }
7
8
  export declare function severityOf(diagnostic: Diagnostic): EditorMarkerSeverity;
8
9
  export declare function markerOf(diagnostic: Diagnostic): EditorMarker;
10
+ export declare function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts;
9
11
  export declare function comparePositions(left: EditorPosition, right: EditorPosition): number;
10
12
  export declare function shiftPosition(position: EditorPosition, change: EditorContentChange): EditorPosition;
11
13
  export declare function shiftRange(range: EditorRange, changes: readonly EditorContentChange[]): EditorRange;