@adecore/editor-react 0.0.1 → 0.17.0-beta.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 (264) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +55 -0
  3. package/dist/AnchoredPopup.d.ts +12 -0
  4. package/dist/AnchoredPopup.js +27 -0
  5. package/dist/AttributionCard.d.ts +14 -0
  6. package/dist/AttributionCard.js +5 -0
  7. package/dist/BaselineStatus.d.ts +4 -0
  8. package/dist/BaselineStatus.js +13 -0
  9. package/dist/ChangeReview.d.ts +12 -0
  10. package/dist/ChangeReview.js +27 -0
  11. package/dist/CodeAuthorsCard.d.ts +6 -0
  12. package/dist/CodeAuthorsCard.js +22 -0
  13. package/dist/CompletionPopup.d.ts +8 -0
  14. package/dist/CompletionPopup.js +103 -0
  15. package/dist/EditorContextMenu.d.ts +8 -0
  16. package/dist/EditorContextMenu.js +23 -0
  17. package/dist/EditorView.d.ts +15 -0
  18. package/dist/EditorView.js +27 -0
  19. package/dist/FindReplace.d.ts +6 -0
  20. package/dist/FindReplace.js +44 -0
  21. package/dist/HoverCard.d.ts +11 -0
  22. package/dist/HoverCard.js +38 -0
  23. package/dist/HoverSections.d.ts +21 -0
  24. package/dist/HoverSections.js +82 -0
  25. package/dist/LanguagePopups.d.ts +4 -0
  26. package/dist/LanguagePopups.js +38 -0
  27. package/dist/Markdown.d.ts +3 -0
  28. package/dist/Markdown.js +7 -0
  29. package/dist/PathText.d.ts +4 -0
  30. package/dist/PathText.js +12 -0
  31. package/dist/PeekPanel.d.ts +6 -0
  32. package/dist/PeekPanel.js +111 -0
  33. package/dist/PickPopup.d.ts +8 -0
  34. package/dist/PickPopup.js +33 -0
  35. package/dist/RenameCard.d.ts +9 -0
  36. package/dist/RenameCard.js +108 -0
  37. package/dist/SignatureCard.d.ts +4 -0
  38. package/dist/SignatureCard.js +8 -0
  39. package/dist/SymbolPicker.d.ts +6 -0
  40. package/dist/SymbolPicker.js +108 -0
  41. package/dist/code-actions-model.d.ts +23 -0
  42. package/dist/code-actions-model.js +114 -0
  43. package/dist/code-actions.d.ts +29 -0
  44. package/dist/code-actions.js +329 -0
  45. package/dist/code-authors.d.ts +17 -0
  46. package/dist/code-authors.js +80 -0
  47. package/dist/code-vision-model.d.ts +20 -0
  48. package/dist/code-vision-model.js +184 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +234 -0
  51. package/dist/completion-call.d.ts +24 -0
  52. package/dist/completion-call.js +110 -0
  53. package/dist/completion-model.d.ts +51 -0
  54. package/dist/completion-model.js +353 -0
  55. package/dist/completion.d.ts +43 -0
  56. package/dist/completion.js +464 -0
  57. package/dist/conflict-model.d.ts +15 -0
  58. package/dist/conflict-model.js +36 -0
  59. package/dist/context-menu.d.ts +10 -0
  60. package/dist/context-menu.js +55 -0
  61. package/dist/definition-link.d.ts +14 -0
  62. package/dist/definition-link.js +99 -0
  63. package/dist/diagnostics-model.d.ts +15 -0
  64. package/dist/diagnostics-model.js +83 -0
  65. package/dist/diagnostics.d.ts +23 -0
  66. package/dist/diagnostics.js +95 -0
  67. package/dist/editor-language.d.ts +64 -0
  68. package/dist/editor-language.js +148 -0
  69. package/dist/editor-react.css +43 -0
  70. package/dist/find-query.d.ts +26 -0
  71. package/dist/find-query.js +51 -0
  72. package/dist/folding.d.ts +16 -0
  73. package/dist/folding.js +97 -0
  74. package/dist/highlight-layers.d.ts +10 -0
  75. package/dist/highlight-layers.js +30 -0
  76. package/dist/highlights.d.ts +16 -0
  77. package/dist/highlights.js +102 -0
  78. package/dist/history.d.ts +8 -0
  79. package/dist/history.js +47 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +43 -0
  83. package/dist/host-types.js +1 -0
  84. package/dist/hover-content.d.ts +45 -0
  85. package/dist/hover-content.js +198 -0
  86. package/dist/hover.d.ts +32 -0
  87. package/dist/hover.js +261 -0
  88. package/dist/index.d.ts +44 -0
  89. package/dist/index.js +28 -0
  90. package/dist/inlay-hints.d.ts +13 -0
  91. package/dist/inlay-hints.js +73 -0
  92. package/dist/language-ids.d.ts +3 -0
  93. package/dist/language-ids.js +49 -0
  94. package/dist/line-edits.d.ts +8 -0
  95. package/dist/line-edits.js +39 -0
  96. package/dist/locales/en.json +171 -0
  97. package/dist/locales/nl.json +171 -0
  98. package/dist/models.d.ts +20 -0
  99. package/dist/models.js +20 -0
  100. package/dist/navigation-history.d.ts +16 -0
  101. package/dist/navigation-history.js +53 -0
  102. package/dist/navigation.d.ts +22 -0
  103. package/dist/navigation.js +156 -0
  104. package/dist/paths.d.ts +1 -0
  105. package/dist/paths.js +3 -0
  106. package/dist/peek-model.d.ts +28 -0
  107. package/dist/peek-model.js +104 -0
  108. package/dist/peek.d.ts +29 -0
  109. package/dist/peek.js +216 -0
  110. package/dist/pick.d.ts +34 -0
  111. package/dist/pick.js +166 -0
  112. package/dist/popup-placement.d.ts +23 -0
  113. package/dist/popup-placement.js +34 -0
  114. package/dist/popups.d.ts +133 -0
  115. package/dist/popups.js +14 -0
  116. package/dist/project-language.d.ts +34 -0
  117. package/dist/project-language.js +187 -0
  118. package/dist/project-problems.d.ts +24 -0
  119. package/dist/project-problems.js +63 -0
  120. package/dist/proposal-model.d.ts +36 -0
  121. package/dist/proposal-model.js +197 -0
  122. package/dist/recent-choices.d.ts +10 -0
  123. package/dist/recent-choices.js +39 -0
  124. package/dist/refresher.d.ts +13 -0
  125. package/dist/refresher.js +50 -0
  126. package/dist/relative-time.d.ts +1 -0
  127. package/dist/relative-time.js +9 -0
  128. package/dist/rename-model.d.ts +25 -0
  129. package/dist/rename-model.js +93 -0
  130. package/dist/rename.d.ts +20 -0
  131. package/dist/rename.js +258 -0
  132. package/dist/rendering-context.d.ts +6 -0
  133. package/dist/rendering-context.js +5 -0
  134. package/dist/rendering.d.ts +6 -0
  135. package/dist/rendering.js +5 -0
  136. package/dist/review-group.d.ts +16 -0
  137. package/dist/review-group.js +54 -0
  138. package/dist/row-host.d.ts +42 -0
  139. package/dist/row-host.js +120 -0
  140. package/dist/selection-ranges.d.ts +7 -0
  141. package/dist/selection-ranges.js +26 -0
  142. package/dist/semantic-model.d.ts +4 -0
  143. package/dist/semantic-model.js +76 -0
  144. package/dist/semantic-tokens.d.ts +6 -0
  145. package/dist/semantic-tokens.js +40 -0
  146. package/dist/shortcut-keys.d.ts +91 -0
  147. package/dist/shortcut-keys.js +12 -0
  148. package/dist/signature-model.d.ts +18 -0
  149. package/dist/signature-model.js +33 -0
  150. package/dist/signature.d.ts +22 -0
  151. package/dist/signature.js +150 -0
  152. package/dist/snippet-session.d.ts +17 -0
  153. package/dist/snippet-session.js +110 -0
  154. package/dist/snippet.d.ts +11 -0
  155. package/dist/snippet.js +134 -0
  156. package/dist/symbol-links.d.ts +6 -0
  157. package/dist/symbol-links.js +50 -0
  158. package/dist/symbol-picker-model.d.ts +44 -0
  159. package/dist/symbol-picker-model.js +146 -0
  160. package/dist/symbol-picker.d.ts +12 -0
  161. package/dist/symbol-picker.js +77 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +97 -0
  164. package/dist/testing.d.ts +57 -0
  165. package/dist/testing.js +156 -0
  166. package/dist/timers.d.ts +14 -0
  167. package/dist/timers.js +34 -0
  168. package/dist/use-problem-counts.d.ts +3 -0
  169. package/dist/use-problem-counts.js +20 -0
  170. package/dist/view-state.d.ts +21 -0
  171. package/dist/view-state.js +63 -0
  172. package/dist/word-diff.d.ts +1 -0
  173. package/dist/word-diff.js +32 -0
  174. package/dist/workspace-edit.d.ts +21 -0
  175. package/dist/workspace-edit.js +147 -0
  176. package/package.json +74 -2
  177. package/src/AnchoredPopup.tsx +51 -0
  178. package/src/AttributionCard.tsx +36 -0
  179. package/src/BaselineStatus.tsx +20 -0
  180. package/src/ChangeReview.tsx +43 -0
  181. package/src/CodeAuthorsCard.tsx +81 -0
  182. package/src/CompletionPopup.tsx +196 -0
  183. package/src/EditorContextMenu.tsx +67 -0
  184. package/src/EditorView.tsx +45 -0
  185. package/src/FindReplace.tsx +89 -0
  186. package/src/HoverCard.tsx +121 -0
  187. package/src/HoverSections.tsx +146 -0
  188. package/src/LanguagePopups.tsx +61 -0
  189. package/src/Markdown.tsx +11 -0
  190. package/src/PathText.tsx +18 -0
  191. package/src/PeekPanel.tsx +222 -0
  192. package/src/PickPopup.tsx +102 -0
  193. package/src/RenameCard.tsx +219 -0
  194. package/src/SignatureCard.tsx +42 -0
  195. package/src/SymbolPicker.tsx +219 -0
  196. package/src/code-actions-model.ts +146 -0
  197. package/src/code-actions.ts +387 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +267 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +501 -0
  204. package/src/conflict-model.ts +56 -0
  205. package/src/context-menu.ts +63 -0
  206. package/src/definition-link.ts +106 -0
  207. package/src/diagnostics-model.ts +102 -0
  208. package/src/diagnostics.ts +117 -0
  209. package/src/editor-language.ts +163 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +112 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +57 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +49 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +281 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +87 -0
  222. package/src/language-ids.ts +54 -0
  223. package/src/line-edits.ts +48 -0
  224. package/src/locales/en.json +171 -0
  225. package/src/locales/nl.json +171 -0
  226. package/src/models.ts +157 -0
  227. package/src/navigation-history.ts +69 -0
  228. package/src/navigation.ts +169 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +248 -0
  232. package/src/pick.ts +197 -0
  233. package/src/popup-placement.ts +64 -0
  234. package/src/popups.ts +204 -0
  235. package/src/project-language.ts +220 -0
  236. package/src/project-problems.ts +87 -0
  237. package/src/proposal-model.ts +259 -0
  238. package/src/recent-choices.ts +48 -0
  239. package/src/refresher.ts +56 -0
  240. package/src/relative-time.ts +10 -0
  241. package/src/rename-model.ts +130 -0
  242. package/src/rename.ts +291 -0
  243. package/src/rendering-context.ts +12 -0
  244. package/src/rendering.tsx +6 -0
  245. package/src/review-group.ts +70 -0
  246. package/src/row-host.ts +158 -0
  247. package/src/selection-ranges.ts +32 -0
  248. package/src/semantic-model.ts +80 -0
  249. package/src/semantic-tokens.ts +48 -0
  250. package/src/shortcut-keys.ts +21 -0
  251. package/src/signature-model.ts +49 -0
  252. package/src/signature.ts +164 -0
  253. package/src/snippet-session.ts +122 -0
  254. package/src/snippet.ts +147 -0
  255. package/src/symbol-links.ts +72 -0
  256. package/src/symbol-picker-model.ts +198 -0
  257. package/src/symbol-picker.ts +86 -0
  258. package/src/symbols.ts +112 -0
  259. package/src/testing.ts +240 -0
  260. package/src/timers.ts +45 -0
  261. package/src/use-problem-counts.ts +25 -0
  262. package/src/view-state.ts +88 -0
  263. package/src/word-diff.ts +35 -0
  264. package/src/workspace-edit.ts +194 -0
@@ -0,0 +1,110 @@
1
+ import { CLASS_KIND, CONSTRUCTOR_KIND, FUNCTION_KIND, METHOD_KIND, SNIPPET_FORMAT } from './completion-model.js';
2
+ /* What the platform's editors send to ask for parameter info once an item is in; the card is ours to open. */
3
+ export const PARAMETER_HINTS_COMMAND = 'editor.action.triggerParameterHints';
4
+ const CALL_LANGUAGES = new Set(['typescript', 'typescriptreact', 'javascript', 'javascriptreact', 'vue', 'php', 'python']);
5
+ const MARKUP_LANGUAGES = new Set(['typescriptreact', 'javascriptreact', 'vue']);
6
+ const IMPORT_BRACE = /\b(?:import|export)(?:\s+type)?(?:\s+[\w$]+\s*,)?\s*$/;
7
+ const PHP_USE_BRACE = /\buse(?:\s+(?:function|const))?\s+[\w\\]*\\\s*$/;
8
+ const TYPE_OPERATOR = /\b(?:typeof|keyof|instanceof|implements|satisfies|as)\s+[\w$.?]*$/;
9
+ const AFTER_NEW = /\bnew\s+[\w$.\\]*$/;
10
+ const EXISTING_CALL = /^\s*\(/;
11
+ const SIGNATURE_KIND_PREFIX = /^\([a-z]+(?: [a-z]+)*\)\s+(?!=>)/;
12
+ const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
13
+ /* Whether the item is something that is called where it stands: a function, a method, a constructor, or a class after `new`. */
14
+ 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);
19
+ }
20
+ /* Whether the parameter list in a signature has anything in it; null when the text names no list. */
21
+ function listedParameters(signature) {
22
+ const text = signature.replace(SIGNATURE_KIND_PREFIX, '');
23
+ const open = text.indexOf('(');
24
+ if (open < 0) {
25
+ return null;
26
+ }
27
+ let depth = 0;
28
+ for (let i = open; i < text.length; i++) {
29
+ if (text[i] === '(') {
30
+ depth++;
31
+ }
32
+ else if (text[i] === ')' && --depth === 0) {
33
+ return text.slice(open + 1, i).trim() !== '';
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /* From what the server says of the item: `(method) Box.open(flag: boolean): void` has parameters and `(): void` has none. Null when it says nothing about them. */
39
+ export function hasParameters(item) {
40
+ const lines = (item.detail ?? '').split('\n').filter((line) => !SOURCE_LINE.test(line.trim()));
41
+ for (const signature of [item.labelDetails?.detail ?? '', ...lines]) {
42
+ const listed = listedParameters(signature);
43
+ if (listed !== null) {
44
+ return listed;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /* The text before the innermost brace that is still open, which says whether the caret is in an import or export list. */
50
+ function beforeOpenBrace(before) {
51
+ let depth = 0;
52
+ for (let i = before.length - 1; i >= 0; i--) {
53
+ if (before[i] === '}') {
54
+ depth++;
55
+ }
56
+ else if (before[i] === '{' && depth-- === 0) {
57
+ return before.slice(0, i);
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ /* Where a name is not called: in an import or export list, a `use`, a type operator's operand, an attribute or a tag. */
63
+ function isNameOnly(languageId, before) {
64
+ const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
65
+ const brace = beforeOpenBrace(before);
66
+ if (brace !== null && (IMPORT_BRACE.test(brace) || (languageId === 'php' && PHP_USE_BRACE.test(brace)))) {
67
+ return true;
68
+ }
69
+ if (TYPE_OPERATOR.test(lineBefore) || /^\s*export\s+default\s+[\w$]*$/.test(lineBefore)) {
70
+ return true;
71
+ }
72
+ switch (languageId) {
73
+ case 'php':
74
+ return /^\s*use\s/.test(lineBefore) || /#\[[^\]]*$/.test(lineBefore);
75
+ case 'python':
76
+ return /^\s*(?:from\s+\S+\s+)?import\b/.test(lineBefore);
77
+ default:
78
+ return /^\s*import\b/.test(lineBefore) || (MARKUP_LANGUAGES.has(languageId) && /<\/?[\w$.:-]*$/.test(lineBefore));
79
+ }
80
+ }
81
+ /*
82
+ * Decides what accepting `item` does about a call, as the platform's parentheses handler does: a callable
83
+ * gets `()` with the caret inside when it has parameters (or when that is not known) and behind when it has
84
+ * none, and one that already has a parenthesis after it gets none. A snippet, or text that already holds a
85
+ * parenthesis, carries its own call and wins.
86
+ */
87
+ export function planCall(site) {
88
+ const { item, languageId, insertion, before, rest, replace } = site;
89
+ const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
90
+ if (!CALL_LANGUAGES.has(languageId) || !isCallItem(item, lineBefore) || item.insertTextFormat === SNIPPET_FORMAT || insertion.text.includes('(')) {
91
+ return { action: 'none' };
92
+ }
93
+ if (isNameOnly(languageId, before)) {
94
+ return { action: 'none' };
95
+ }
96
+ const existing = EXISTING_CALL.exec(rest);
97
+ if (existing !== null) {
98
+ return replace ? { action: 'enter', shift: existing[0].length } : { action: 'none' };
99
+ }
100
+ if (rest.startsWith('<')) {
101
+ return { action: 'none' };
102
+ }
103
+ return { action: 'add', inside: hasParameters(item) !== false };
104
+ }
105
+ /* The insertion with its parentheses, and the one tab stop that puts the caret where the plan says. */
106
+ export function withParentheses(insertion, inside) {
107
+ const text = `${insertion.text}()`;
108
+ const caret = inside ? text.length - 1 : text.length;
109
+ return { range: insertion.range, text, stops: [{ index: 0, start: caret, end: caret }] };
110
+ }
@@ -0,0 +1,51 @@
1
+ import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import type { CompletionItem, CompletionResult, InsertReplaceEdit, MarkupContent, TextEdit } from '@adecore/lsp';
3
+ import type { CompletionDocs } from './popups.ts';
4
+ import { type SnippetStop } from './snippet.ts';
5
+ export declare function isIdentifierCharacter(character: string): boolean;
6
+ export declare function identifierPrefix(before: string): string;
7
+ export interface MatchDetail {
8
+ readonly score: number;
9
+ readonly positions: readonly number[];
10
+ }
11
+ export declare function matchDetail(value: string, prefix: string): MatchDetail | undefined;
12
+ export declare function matchScore(value: string, prefix: string): number | undefined;
13
+ export declare function editRangeOf(item: CompletionItem, replace: boolean): EditorRange | null;
14
+ export declare function itemsOf(result: CompletionResult): {
15
+ items: CompletionItem[];
16
+ incomplete: boolean;
17
+ };
18
+ export declare function prefixFor(item: CompletionItem, caret: EditorPosition, lineBefore: string, textBetween: (range: EditorRange) => string): string;
19
+ export interface Ranked {
20
+ readonly item: CompletionItem;
21
+ readonly score: number;
22
+ readonly prefix: string;
23
+ }
24
+ export declare function rankCompletions(items: readonly CompletionItem[], prefixOf: (item: CompletionItem) => string, recency?: (item: CompletionItem) => number, limit?: number): Ranked[];
25
+ export declare function matchedCharacters(label: string, prefix: string): readonly number[];
26
+ export declare function qualifierOf(item: CompletionItem): string;
27
+ export declare function qualifiersOf(items: readonly CompletionItem[]): string[];
28
+ export declare function snippetToText(snippet: string): string;
29
+ export declare const METHOD_KIND = 2;
30
+ export declare const FUNCTION_KIND = 3;
31
+ export declare const CONSTRUCTOR_KIND = 4;
32
+ export declare const CLASS_KIND = 7;
33
+ export declare const SNIPPET_FORMAT = 2;
34
+ export interface Insertion {
35
+ readonly range: EditorRange;
36
+ readonly text: string;
37
+ readonly stops: readonly SnippetStop[];
38
+ }
39
+ export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string): Insertion;
40
+ export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion): EditorContentChange[];
41
+ export declare function documentationText(documentation: string | MarkupContent | undefined): string;
42
+ export declare function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null;
43
+ export type { InsertReplaceEdit, TextEdit };
44
+ export type KindTone = 'callable' | 'value' | 'type' | 'other';
45
+ export declare function kindLetterOf(kind: number | undefined): string;
46
+ export declare function kindToneOf(kind: number | undefined): KindTone;
47
+ export interface RowWindow {
48
+ readonly first: number;
49
+ readonly last: number;
50
+ }
51
+ export declare function rowWindow(scrollTop: number, active: number, total: number, rowHeight: number, visible: number, margin: number, padding: number): RowWindow;
@@ -0,0 +1,353 @@
1
+ import { comparePositions } from './diagnostics-model.js';
2
+ import { isEmptyHover, splitSignatures } from './hover-content.js';
3
+ import { parseSnippet, tabOrder } from './snippet.js';
4
+ const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
5
+ export function isIdentifierCharacter(character) {
6
+ return IDENTIFIER_CHARACTER.test(character);
7
+ }
8
+ let lastBefore = '';
9
+ let lastPrefix = '';
10
+ /* The word being typed: the identifier characters at the end of the text before the caret. A list asks it of every item with the same line, so the last answer is kept. */
11
+ export function identifierPrefix(before) {
12
+ if (before === lastBefore) {
13
+ return lastPrefix;
14
+ }
15
+ const characters = Array.from(before);
16
+ let start = characters.length;
17
+ while (start > 0 && IDENTIFIER_CHARACTER.test(characters[start - 1])) {
18
+ start--;
19
+ }
20
+ lastBefore = before;
21
+ lastPrefix = characters.slice(start).join('');
22
+ return lastPrefix;
23
+ }
24
+ function run(start, length) {
25
+ return Array.from({ length }, (_, offset) => start + offset);
26
+ }
27
+ const SHAPE_LIMIT = 4000;
28
+ const shapes = new Map();
29
+ /* What matching reads of a label, which is the same at every keystroke of a word and costs a regular expression pass to find. */
30
+ function shapeOf(value) {
31
+ const known = shapes.get(value);
32
+ if (known !== undefined) {
33
+ return known;
34
+ }
35
+ const boundaries = [...value.matchAll(/(?:^|[_$\s-])([\p{L}\p{N}])|(\p{Lu})/gu)];
36
+ const shape = {
37
+ lower: value.toLocaleLowerCase(),
38
+ offsets: boundaries.map((boundary) => boundary.index + (boundary[1] ? boundary[0].length - boundary[1].length : 0)),
39
+ initials: boundaries
40
+ .map((match) => match[1] ?? match[2])
41
+ .join('')
42
+ .toLocaleLowerCase()
43
+ };
44
+ if (shapes.size >= SHAPE_LIMIT) {
45
+ shapes.clear();
46
+ }
47
+ shapes.set(value, shape);
48
+ return shape;
49
+ }
50
+ /*
51
+ * How well a prefix fits a label, lower being better, and which characters it matched, or undefined when
52
+ * it does not fit. A match that starts the label beats one that ignores case, which beats one at a camel
53
+ * hump or after an underscore, then the initials of the humps (`gbi` for `getById`), and last the letters
54
+ * in order for a prefix of two or more.
55
+ */
56
+ export function matchDetail(value, prefix) {
57
+ if (prefix === '') {
58
+ return { score: 0, positions: [] };
59
+ }
60
+ if (value.startsWith(prefix)) {
61
+ return { score: 0, positions: run(0, prefix.length) };
62
+ }
63
+ const { lower, offsets, initials } = shapeOf(value);
64
+ const query = prefix.toLocaleLowerCase();
65
+ if (lower.startsWith(query)) {
66
+ return { score: 1, positions: run(0, query.length) };
67
+ }
68
+ for (const offset of offsets) {
69
+ if (lower.slice(offset).startsWith(query)) {
70
+ return { score: 2 + offset / 1000, positions: run(offset, query.length) };
71
+ }
72
+ }
73
+ if (initials.startsWith(query)) {
74
+ return { score: 3, positions: offsets.slice(0, query.length) };
75
+ }
76
+ if (query.length < 2 || lower[0] !== query[0]) {
77
+ return undefined;
78
+ }
79
+ let cursor = 0;
80
+ const positions = [];
81
+ for (const character of query) {
82
+ const next = lower.indexOf(character, cursor);
83
+ if (next < 0) {
84
+ return undefined;
85
+ }
86
+ positions.push(next);
87
+ cursor = next + character.length;
88
+ }
89
+ return { score: 4 + cursor / 1000, positions };
90
+ }
91
+ export function matchScore(value, prefix) {
92
+ return matchDetail(value, prefix)?.score;
93
+ }
94
+ /* The range an item replaces when it is accepted; for an insert-or-replace edit, the one `replace` asks for. */
95
+ export function editRangeOf(item, replace) {
96
+ const edit = item.textEdit;
97
+ if (edit === undefined) {
98
+ return null;
99
+ }
100
+ if ('range' in edit) {
101
+ return edit.range;
102
+ }
103
+ return replace ? edit.replace : edit.insert;
104
+ }
105
+ /* Items from a result, with the list's defaults filled in so each carries its own edit range and format. */
106
+ export function itemsOf(result) {
107
+ if (result === null) {
108
+ return { items: [], incomplete: false };
109
+ }
110
+ if (Array.isArray(result)) {
111
+ return { items: result, incomplete: false };
112
+ }
113
+ const list = result;
114
+ const defaults = list.itemDefaults;
115
+ if (defaults === undefined) {
116
+ return { items: list.items, incomplete: list.isIncomplete };
117
+ }
118
+ return {
119
+ incomplete: list.isIncomplete,
120
+ items: list.items.map((item) => {
121
+ const next = { ...item };
122
+ if (defaults.insertTextFormat !== undefined && next.insertTextFormat === undefined) {
123
+ next.insertTextFormat = defaults.insertTextFormat;
124
+ }
125
+ if (defaults.commitCharacters !== undefined && next.commitCharacters === undefined) {
126
+ next.commitCharacters = defaults.commitCharacters;
127
+ }
128
+ if (next.textEdit === undefined && defaults.editRange !== undefined) {
129
+ const text = item.textEditText ?? item.insertText ?? item.label;
130
+ next.textEdit =
131
+ 'start' in defaults.editRange
132
+ ? { range: defaults.editRange, newText: text }
133
+ : { insert: defaults.editRange.insert, replace: defaults.editRange.replace, newText: text };
134
+ }
135
+ return next;
136
+ })
137
+ };
138
+ }
139
+ /*
140
+ * What a person has typed of an item: from the start of the range it will replace up to the caret when
141
+ * the caret is in that range, else the word before the caret. Matching against that is what keeps
142
+ * `thisArg.` from filtering on the dot.
143
+ */
144
+ export function prefixFor(item, caret, lineBefore, textBetween) {
145
+ const range = editRangeOf(item, false);
146
+ if (range !== null && range.start.line === caret.line && comparePositions(range.start, caret) <= 0 && comparePositions(caret, range.end) <= 0) {
147
+ return textBetween({ start: range.start, end: caret });
148
+ }
149
+ return identifierPrefix(lineBefore);
150
+ }
151
+ /*
152
+ * The items that fit what is typed, best first: by how they match, then one the person chose lately, a
153
+ * preselected one, and the server's own order. `recency` is higher for a choice made more recently, and 0 for none.
154
+ */
155
+ export function rankCompletions(items, prefixOf, recency = () => 0, limit = 150) {
156
+ return items
157
+ .map((item, index) => {
158
+ const prefix = prefixOf(item);
159
+ return { item, index, prefix, score: matchScore(item.filterText ?? item.label, prefix), recent: recency(item) };
160
+ })
161
+ .filter((entry) => entry.score !== undefined)
162
+ .sort((left, right) => left.score - right.score ||
163
+ right.recent - left.recent ||
164
+ Number(right.item.preselect === true) - Number(left.item.preselect === true) ||
165
+ (left.item.sortText ?? left.item.label).localeCompare(right.item.sortText ?? right.item.label) ||
166
+ left.index - right.index)
167
+ .slice(0, limit)
168
+ .map(({ item, score, prefix }) => ({ item, score, prefix }));
169
+ }
170
+ /* The characters of a label to mark for what was typed; none when the label is not what the server filtered on. */
171
+ export function matchedCharacters(label, prefix) {
172
+ return matchDetail(label, prefix)?.positions ?? [];
173
+ }
174
+ /* The first line of what a server says about where an item comes from, with a PHP `use` made into the namespace it names. */
175
+ function sourceOf(item) {
176
+ const detail = item.detail?.split('\n')[0]?.trim() ?? '';
177
+ const imported = /^use\s+(.+)$/.exec(detail);
178
+ return imported === null ? detail : imported[1].slice(0, Math.max(0, imported[1].lastIndexOf('\\')));
179
+ }
180
+ /* What stands right-aligned after a row: the module or namespace the item comes from, else what the server calls its type. */
181
+ export function qualifierOf(item) {
182
+ const description = item.labelDetails?.description?.trim() ?? '';
183
+ return description !== '' ? description : sourceOf(item);
184
+ }
185
+ /*
186
+ * The qualifier of each row of a list. Items that share a label are told apart however it takes: by the
187
+ * qualifier, or by what the server says besides, and by a number when a server says nothing that differs.
188
+ */
189
+ export function qualifiersOf(items) {
190
+ const result = items.map(qualifierOf);
191
+ const groups = new Map();
192
+ items.forEach((item, index) => groups.set(item.label, [...(groups.get(item.label) ?? []), index]));
193
+ for (const indexes of groups.values()) {
194
+ if (indexes.length < 2 || new Set(indexes.map((index) => result[index])).size === indexes.length) {
195
+ continue;
196
+ }
197
+ const alternate = indexes.map((index) => sourceOf(items[index]) || result[index]);
198
+ const distinct = new Set(alternate).size === indexes.length && !alternate.includes('');
199
+ indexes.forEach((index, position) => {
200
+ result[index] = distinct ? alternate[position] : `${result[index] ?? ''} #${position + 1}`.trim();
201
+ });
202
+ }
203
+ return result;
204
+ }
205
+ /* A snippet as the plain text it would insert: tab stops and variables vanish, placeholders and choices become their text, and the escapes of the format are undone. */
206
+ export function snippetToText(snippet) {
207
+ return parseSnippet(snippet).text;
208
+ }
209
+ export const METHOD_KIND = 2;
210
+ export const FUNCTION_KIND = 3;
211
+ export const CONSTRUCTOR_KIND = 4;
212
+ export const CLASS_KIND = 7;
213
+ export const SNIPPET_FORMAT = 2;
214
+ /* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
215
+ function withoutCall(snippet) {
216
+ const open = snippet.search(/(?<!\\)\(/);
217
+ return open > 0 ? snippet.slice(0, open) : snippet;
218
+ }
219
+ /*
220
+ * What goes into the text for an item and the range it takes the place of, given the caret. `lineAfter` is
221
+ * the rest of the line after the caret: an item that would add a call does not when a parenthesis follows.
222
+ */
223
+ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
224
+ const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
225
+ const edit = editRangeOf(item, replace);
226
+ const range = edit === null
227
+ ? { start: { line: caret.line, character: caret.character - identifierPrefix(lineBefore).length }, end: caret }
228
+ : // The word grew since the answer came, so what was typed since belongs to what is replaced.
229
+ { start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
230
+ if (item.insertTextFormat !== SNIPPET_FORMAT) {
231
+ return { range, text: raw, stops: [] };
232
+ }
233
+ const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
234
+ const callable = item.kind === METHOD_KIND || item.kind === FUNCTION_KIND || item.kind === CONSTRUCTOR_KIND;
235
+ const parsed = parseSnippet(followed && callable ? withoutCall(raw) : raw);
236
+ return { range, text: parsed.text, stops: tabOrder(parsed) };
237
+ }
238
+ /*
239
+ * The same insertion at every other caret that has the word the primary caret has before it, so a name typed at
240
+ * several carets is completed at all of them. A caret that is inside a selection, or after other text, is left alone.
241
+ */
242
+ export function mirroredInsertions(carets, primary, caret, text, insertion) {
243
+ const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
244
+ const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
245
+ if (before < 0 || after < 0) {
246
+ return [];
247
+ }
248
+ const typed = text({ start: insertion.range.start, end: caret });
249
+ const replaced = text({ start: caret, end: insertion.range.end });
250
+ return carets.flatMap((other) => {
251
+ const at = other.start;
252
+ if (comparePositions(other.start, primary.start) === 0 && comparePositions(other.end, primary.end) === 0) {
253
+ return [];
254
+ }
255
+ if (comparePositions(other.start, other.end) !== 0 || at.character < before) {
256
+ return [];
257
+ }
258
+ const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };
259
+ const preceding = range.start.character > 0 ? text({ start: { line: at.line, character: range.start.character - 1 }, end: range.start }) : '';
260
+ const startsWord = preceding === '' || !isIdentifierCharacter(preceding);
261
+ const matches = startsWord && text({ start: range.start, end: at }) === typed && text({ start: at, end: range.end }) === replaced;
262
+ return matches ? [{ range, text: insertion.text }] : [];
263
+ });
264
+ }
265
+ export function documentationText(documentation) {
266
+ if (documentation === undefined) {
267
+ return '';
268
+ }
269
+ return typeof documentation === 'string'
270
+ ? documentation
271
+ : documentation.kind === 'plaintext'
272
+ ? documentation.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1')
273
+ : documentation.value;
274
+ }
275
+ const SOURCE_LINE = /^(?:use\s|Auto import from\b)/;
276
+ /*
277
+ * What is said about a suggestion beside the list, in the shape a hover has: the signature it names and the
278
+ * documentation split into signature, prose and tags. A first line that only says where the item comes from is
279
+ * kept apart, since it is no code.
280
+ */
281
+ export function completionDocsOf(item, highlightLanguage) {
282
+ const lines = (item.detail ?? '').trim().split('\n');
283
+ const hasSource = SOURCE_LINE.test(lines[0] ?? '');
284
+ const source = hasSource ? lines[0].trim() : '';
285
+ const code = (hasSource ? lines.slice(1) : lines).join('\n').trim();
286
+ const parsed = splitSignatures(documentationText(item.documentation));
287
+ const text = {
288
+ signatures: code === '' ? parsed.signatures : [{ language: highlightLanguage, code }, ...parsed.signatures],
289
+ markdown: parsed.markdown
290
+ };
291
+ return source === '' && isEmptyHover(text) ? null : { source, text };
292
+ }
293
+ const KIND_LETTERS = {
294
+ 1: 't',
295
+ 2: 'm',
296
+ 3: 'f',
297
+ 4: 'c',
298
+ 5: 'p',
299
+ 6: 'v',
300
+ 7: 'c',
301
+ 8: 'i',
302
+ 9: 'm',
303
+ 10: 'p',
304
+ 11: 'u',
305
+ 12: 'v',
306
+ 13: 'e',
307
+ 14: 'k',
308
+ 15: 's',
309
+ 16: 'c',
310
+ 17: 'f',
311
+ 18: 'r',
312
+ 19: 'f',
313
+ 20: 'e',
314
+ 21: 'k',
315
+ 22: 's',
316
+ 23: 'e',
317
+ 24: 'o',
318
+ 25: 't'
319
+ };
320
+ const KIND_TONES = {
321
+ 2: 'callable',
322
+ 3: 'callable',
323
+ 4: 'callable',
324
+ 5: 'value',
325
+ 6: 'value',
326
+ 10: 'value',
327
+ 12: 'value',
328
+ 20: 'value',
329
+ 21: 'value',
330
+ 7: 'type',
331
+ 8: 'type',
332
+ 9: 'type',
333
+ 13: 'type',
334
+ 22: 'type',
335
+ 25: 'type'
336
+ };
337
+ export function kindLetterOf(kind) {
338
+ return kind === undefined ? '·' : (KIND_LETTERS[kind] ?? '·');
339
+ }
340
+ export function kindToneOf(kind) {
341
+ return kind === undefined ? 'other' : (KIND_TONES[kind] ?? 'other');
342
+ }
343
+ /*
344
+ * The rows of a list worth drawing: the ones in view, a few on each side, and the active one with the
345
+ * same margin, so the rows between never leave a gap. The rest is only space.
346
+ */
347
+ export function rowWindow(scrollTop, active, total, rowHeight, visible, margin, padding) {
348
+ const shown = Math.floor(Math.max(0, scrollTop - padding) / rowHeight);
349
+ return {
350
+ first: Math.max(0, Math.min(shown, active) - margin),
351
+ last: Math.min(total, Math.max(shown + visible + 1, active + 1) + margin)
352
+ };
353
+ }
@@ -0,0 +1,43 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ import { type Timers } from './timers.ts';
3
+ export declare class CompletionFeature {
4
+ private readonly language;
5
+ private readonly timers;
6
+ private pool;
7
+ private incomplete;
8
+ private ranked;
9
+ private active;
10
+ private touched;
11
+ private activeKey;
12
+ private explicit;
13
+ private detailsOpen;
14
+ private start;
15
+ private server;
16
+ private request;
17
+ private controller;
18
+ private requestTimer;
19
+ private refreshTimer;
20
+ private resolveTimer;
21
+ private readonly resolved;
22
+ constructor(language: EditorLanguage, timers?: Timers);
23
+ get isOpen(): boolean;
24
+ invoke(): void;
25
+ move(delta: number): void;
26
+ accept(replace: boolean, index?: number, commit?: string): Promise<void>;
27
+ private placeMirroredCarets;
28
+ private surroundings;
29
+ private afterInsert;
30
+ private resolveNow;
31
+ close(): void;
32
+ private key;
33
+ private commits;
34
+ private edited;
35
+ private caretMoved;
36
+ private triggerCharacters;
37
+ private scheduleRefresh;
38
+ private beginRequest;
39
+ private ask;
40
+ private refilter;
41
+ private scheduleResolve;
42
+ private publish;
43
+ }