@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 { comparePositions, rangeHolds, shiftRange } from './diagnostics-model.js';
2
+ /* Where an offset of inserted text lands when the text is put in at `start`. */
3
+ export function positionInInsertion(start, text, offset) {
4
+ const lines = text.slice(0, offset).split(/\r\n|\r|\n/);
5
+ return lines.length === 1
6
+ ? { line: start.line, character: start.character + offset }
7
+ : { line: start.line + lines.length - 1, character: lines[lines.length - 1].length };
8
+ }
9
+ /*
10
+ * The tab stops of a snippet that was just inserted. The caret goes to the first one, with its default text
11
+ * selected; Tab goes on to the next and Shift+Tab back, and the last stop (`$0`, or the end of the text)
12
+ * ends it. Escape, a caret that leaves the snippet and any change that is not typing end it too. Typing in
13
+ * a stop does not touch the others: they follow the text, but a mirror of a stop is not edited along.
14
+ */
15
+ export class SnippetFeature {
16
+ language;
17
+ stops = [];
18
+ current = 0;
19
+ constructor(language) {
20
+ this.language = language;
21
+ const { editor } = language;
22
+ const offs = [
23
+ editor.onKeyDown((event) => this.key(event)),
24
+ editor.onTextChange((change) => this.edited(change)),
25
+ editor.onCaret((position) => this.moved(position))
26
+ ];
27
+ language.onDispose(() => {
28
+ for (const off of offs) {
29
+ off();
30
+ }
31
+ });
32
+ }
33
+ get isActive() {
34
+ return this.stops.length > 0;
35
+ }
36
+ /* Puts the caret where the snippet says, and keeps the stops to go through when there is more than the last one. */
37
+ begin(start, text, stops) {
38
+ this.end();
39
+ if (stops.length === 0) {
40
+ return;
41
+ }
42
+ const ranges = stops.map((stop) => ({ start: positionInInsertion(start, text, stop.start), end: positionInInsertion(start, text, stop.end) }));
43
+ if (ranges.length === 1) {
44
+ this.language.editor.setCaret(ranges[0].start);
45
+ return;
46
+ }
47
+ this.stops = ranges;
48
+ this.visit(0);
49
+ }
50
+ end() {
51
+ this.stops = [];
52
+ this.current = 0;
53
+ }
54
+ visit(index) {
55
+ const { editor } = this.language;
56
+ const target = this.stops[index];
57
+ this.current = index;
58
+ if (comparePositions(target.start, target.end) === 0) {
59
+ editor.setCaret(target.start);
60
+ }
61
+ else {
62
+ editor.setSelection(target);
63
+ }
64
+ if (index === this.stops.length - 1) {
65
+ this.end();
66
+ }
67
+ }
68
+ key(event) {
69
+ if (!this.isActive || event.ctrlKey || event.metaKey || event.altKey) {
70
+ return false;
71
+ }
72
+ if (event.key === 'Tab') {
73
+ this.visit(event.shiftKey ? Math.max(0, this.current - 1) : this.current + 1);
74
+ return true;
75
+ }
76
+ if (event.key === 'Escape') {
77
+ this.end();
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ edited(change) {
83
+ if (!this.isActive) {
84
+ return;
85
+ }
86
+ if (change.source !== 'input') {
87
+ this.end();
88
+ return;
89
+ }
90
+ this.stops = this.stops.map((stop) => shiftRange(stop, change.changes));
91
+ }
92
+ /* A caret outside the stretch the stops span is a person moving on. */
93
+ moved(position) {
94
+ if (!this.isActive) {
95
+ return;
96
+ }
97
+ const hull = { start: this.stops[0].start, end: this.stops[this.stops.length - 1].end };
98
+ for (const stop of this.stops) {
99
+ if (comparePositions(stop.start, hull.start) < 0) {
100
+ hull.start = stop.start;
101
+ }
102
+ if (comparePositions(stop.end, hull.end) > 0) {
103
+ hull.end = stop.end;
104
+ }
105
+ }
106
+ if (!rangeHolds(hull, position)) {
107
+ this.end();
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,11 @@
1
+ export interface SnippetStop {
2
+ readonly index: number;
3
+ readonly start: number;
4
+ readonly end: number;
5
+ }
6
+ export interface ParsedSnippet {
7
+ readonly text: string;
8
+ readonly stops: readonly SnippetStop[];
9
+ }
10
+ export declare function parseSnippet(snippet: string): ParsedSnippet;
11
+ export declare function tabOrder(parsed: ParsedSnippet): SnippetStop[];
@@ -0,0 +1,134 @@
1
+ const NAME = '[A-Za-z_]\\w*';
2
+ const TABSTOP = new RegExp(`^\\$(\\d+)|^\\$\\{(\\d+)\\}`);
3
+ const PLACEHOLDER = /^\$\{(\d+):/;
4
+ const CHOICE = /^\$\{(\d+)\|/;
5
+ const DEFAULT_VARIABLE = new RegExp(`^\\$\\{${NAME}:`);
6
+ const TRANSFORMED_VARIABLE = new RegExp(`^\\$\\{${NAME}/`);
7
+ const BARE_VARIABLE = new RegExp(`^\\$${NAME}|^\\$\\{${NAME}\\}`);
8
+ class SnippetParser {
9
+ text = '';
10
+ stops = [];
11
+ at = 0;
12
+ source;
13
+ constructor(source) {
14
+ this.source = source;
15
+ }
16
+ parse() {
17
+ this.sequence(false);
18
+ return { text: this.text, stops: this.stops };
19
+ }
20
+ /* Text up to the `}` that closes the placeholder this is in, which is left for the caller. */
21
+ sequence(nested) {
22
+ while (this.at < this.source.length) {
23
+ const character = this.source[this.at];
24
+ if (character === '}' && nested) {
25
+ return;
26
+ }
27
+ if (character === '\\' && /[\\$}]/.test(this.source[this.at + 1] ?? '')) {
28
+ this.text += this.source[this.at + 1];
29
+ this.at += 2;
30
+ }
31
+ else if (character === '$') {
32
+ this.dollar();
33
+ }
34
+ else {
35
+ this.text += character;
36
+ this.at++;
37
+ }
38
+ }
39
+ }
40
+ dollar() {
41
+ const rest = this.source.slice(this.at);
42
+ const tabstop = TABSTOP.exec(rest);
43
+ const placeholder = PLACEHOLDER.exec(rest);
44
+ const choice = CHOICE.exec(rest);
45
+ if (tabstop !== null) {
46
+ this.at += tabstop[0].length;
47
+ this.stops.push({ index: Number(tabstop[1] ?? tabstop[2]), start: this.text.length, end: this.text.length });
48
+ }
49
+ else if (placeholder !== null) {
50
+ this.at += placeholder[0].length;
51
+ const start = this.text.length;
52
+ this.sequence(true);
53
+ this.closeBrace();
54
+ this.stops.push({ index: Number(placeholder[1]), start, end: this.text.length });
55
+ }
56
+ else if (choice !== null) {
57
+ this.at += choice[0].length;
58
+ const start = this.text.length;
59
+ this.firstChoice();
60
+ this.stops.push({ index: Number(choice[1]), start, end: this.text.length });
61
+ }
62
+ else if (DEFAULT_VARIABLE.test(rest)) {
63
+ this.at += rest.indexOf(':') + 1;
64
+ this.sequence(true);
65
+ this.closeBrace();
66
+ }
67
+ else if (TRANSFORMED_VARIABLE.test(rest)) {
68
+ this.skipBraces();
69
+ }
70
+ else if (BARE_VARIABLE.test(rest)) {
71
+ this.at += BARE_VARIABLE.exec(rest)[0].length;
72
+ }
73
+ else {
74
+ this.text += '$';
75
+ this.at++;
76
+ }
77
+ }
78
+ closeBrace() {
79
+ if (this.source[this.at] === '}') {
80
+ this.at++;
81
+ }
82
+ }
83
+ /* The first of `a,b,c|}`, and the rest dropped. */
84
+ firstChoice() {
85
+ let first = true;
86
+ while (this.at < this.source.length && !(this.source[this.at] === '|' && this.source[this.at + 1] === '}')) {
87
+ const character = this.source[this.at];
88
+ if (character === '\\' && /[\\,|]/.test(this.source[this.at + 1] ?? '')) {
89
+ this.at++;
90
+ if (first) {
91
+ this.text += this.source[this.at];
92
+ }
93
+ }
94
+ else if (character === ',') {
95
+ first = false;
96
+ }
97
+ else if (first) {
98
+ this.text += character;
99
+ }
100
+ this.at++;
101
+ }
102
+ this.at += 2;
103
+ }
104
+ /* Past the `}` that closes a `${name/regex/format/}` variable, which inserts nothing here. */
105
+ skipBraces() {
106
+ let depth = 0;
107
+ while (this.at < this.source.length) {
108
+ const character = this.source[this.at];
109
+ this.at += character === '\\' ? 2 : 1;
110
+ if (character === '{') {
111
+ depth++;
112
+ }
113
+ else if (character === '}' && --depth === 0) {
114
+ return;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ export function parseSnippet(snippet) {
120
+ return new SnippetParser(snippet).parse();
121
+ }
122
+ /* The stops in the order Tab visits them: 1, 2 and on, one per index, then the final stop `$0`, which the end of the text stands in for. */
123
+ export function tabOrder(parsed) {
124
+ const first = new Map();
125
+ for (const stop of parsed.stops) {
126
+ const known = first.get(stop.index);
127
+ if (known === undefined || stop.start < known.start) {
128
+ first.set(stop.index, stop);
129
+ }
130
+ }
131
+ const visited = [...first.values()].filter((stop) => stop.index > 0).sort((left, right) => left.index - right.index);
132
+ const final = first.get(0) ?? { index: 0, start: parsed.text.length, end: parsed.text.length };
133
+ return [...visited, final];
134
+ }
@@ -0,0 +1,6 @@
1
+ import type { Location, WorkspaceSymbolResult } from '@adecore/lsp';
2
+ export declare const SYMBOL_LINK_CLASS = "cursor-pointer hover:text-accent hover:underline";
3
+ export declare function isTypeName(name: string): boolean;
4
+ export declare function linkTypeNames(html: string): string;
5
+ export declare function declaredNameOf(code: string): string | null;
6
+ export declare function placesOfName(result: WorkspaceSymbolResult, name: string, fileText: string): Location[];
@@ -0,0 +1,50 @@
1
+ /* The classes a name in a signature has: a pointer and an underline on hover, and the accent only then, so the signature stays calm. */
2
+ export const SYMBOL_LINK_CLASS = 'cursor-pointer hover:text-accent hover:underline';
3
+ /* A name that reads as a type: it starts with a capital and has a lowercase letter, which leaves `DEFAULT_WEIGHTS` and `T` alone. */
4
+ const TYPE_NAME = /^[A-Z][A-Za-z0-9_]*$/;
5
+ export function isTypeName(name) {
6
+ return TYPE_NAME.test(name) && /[a-z]/.test(name);
7
+ }
8
+ /*
9
+ * Marks the type-like names of a highlighted snippet as links. Shiki writes each token as a span of its own,
10
+ * so a name that is the whole text of a token is one; a string or a comment is never that.
11
+ */
12
+ export function linkTypeNames(html) {
13
+ return html.replace(/>([A-Z][A-Za-z0-9_]*)</g, (match, name) => isTypeName(name) ? `><span data-symbol="${name}" class="${SYMBOL_LINK_CLASS}">${name}</span><` : match);
14
+ }
15
+ const DECLARES = /\b(?:class|interface|enum|trait|type|namespace|struct|function|const|let|var)\s+&?([A-Za-z_$][\w$]*)/;
16
+ /* The name a signature declares, such as `IssueTicketsMessage` in `final class IssueTicketsMessage implements MessageInterface`; null for one that declares nothing, such as a method of a class. */
17
+ export function declaredNameOf(code) {
18
+ return DECLARES.exec(code)?.[1] ?? null;
19
+ }
20
+ /* What a workspace symbol is that a type name can mean. */
21
+ const TYPE_KINDS = new Set([2, 3, 5, 10, 11, 23, 26]);
22
+ /*
23
+ * The places a type name leads to among the symbols a server found for it. Only symbols of exactly that name
24
+ * count, those that are types ahead of the rest, and when several are left the ones whose container (a
25
+ * namespace) the current file names, which is how a file says which `Message` it means.
26
+ */
27
+ export function placesOfName(result, name, fileText) {
28
+ const found = (result ?? []).filter((symbol) => symbol.name === name);
29
+ const types = found.filter((symbol) => TYPE_KINDS.has(symbol.kind));
30
+ let candidates = types.length > 0 ? types : found;
31
+ if (candidates.length > 1) {
32
+ const named = candidates.filter((symbol) => symbol.containerName !== undefined && symbol.containerName !== '' && fileText.includes(symbol.containerName));
33
+ if (named.length > 0) {
34
+ candidates = named;
35
+ }
36
+ }
37
+ const seen = new Set();
38
+ return candidates
39
+ .map((symbol) => 'range' in symbol.location
40
+ ? symbol.location
41
+ : { uri: symbol.location.uri, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } })
42
+ .filter((location) => {
43
+ const key = `${location.uri}\0${location.range.start.line}`;
44
+ if (seen.has(key)) {
45
+ return false;
46
+ }
47
+ seen.add(key);
48
+ return true;
49
+ });
50
+ }
@@ -0,0 +1,44 @@
1
+ import type { DocumentSymbolResult, WorkspaceSymbolResult } from '@adecore/lsp';
2
+ export type SymbolGroup = 'namespaces' | 'classes' | 'interfaces' | 'enums' | 'functions' | 'methods' | 'properties' | 'variables' | 'constants' | 'other';
3
+ export declare const SYMBOL_GROUPS: readonly SymbolGroup[];
4
+ export declare function groupOfKind(kind: number): SymbolGroup;
5
+ export declare function letterOfKind(kind: number): string;
6
+ export type SymbolTone = 'callable' | 'value' | 'type' | 'other';
7
+ export declare function toneOfKind(kind: number): SymbolTone;
8
+ export interface SymbolEntry {
9
+ readonly id: string;
10
+ readonly name: string;
11
+ readonly kind: number;
12
+ readonly container: string;
13
+ readonly detail: string;
14
+ readonly line: number;
15
+ readonly character: number;
16
+ }
17
+ export declare function entriesOf(result: DocumentSymbolResult): SymbolEntry[];
18
+ export declare function scoreOf(name: string, query: string): number;
19
+ export declare function filterEntries(entries: readonly SymbolEntry[], query: string): SymbolEntry[];
20
+ export interface EntryGroup {
21
+ readonly group: SymbolGroup;
22
+ readonly entries: readonly SymbolEntry[];
23
+ }
24
+ export declare function groupEntries(entries: readonly SymbolEntry[]): EntryGroup[];
25
+ export type PickerMode = {
26
+ readonly mode: 'symbol';
27
+ readonly text: string;
28
+ } | {
29
+ readonly mode: 'workspace';
30
+ readonly text: string;
31
+ } | {
32
+ readonly mode: 'line';
33
+ readonly line: number | null;
34
+ };
35
+ export declare function modeOf(input: string): PickerMode;
36
+ export interface WorkspaceEntry {
37
+ readonly id: string;
38
+ readonly name: string;
39
+ readonly kind: number;
40
+ readonly container: string;
41
+ readonly uri: string;
42
+ readonly line: number;
43
+ }
44
+ export declare function workspaceEntriesOf(result: WorkspaceSymbolResult): WorkspaceEntry[];
@@ -0,0 +1,146 @@
1
+ /* The order the groups are listed in: what a file declares, then what those declare. */
2
+ export const SYMBOL_GROUPS = [
3
+ 'namespaces',
4
+ 'classes',
5
+ 'interfaces',
6
+ 'enums',
7
+ 'functions',
8
+ 'methods',
9
+ 'properties',
10
+ 'variables',
11
+ 'constants',
12
+ 'other'
13
+ ];
14
+ const GROUP_OF_KIND = {
15
+ 2: 'namespaces',
16
+ 3: 'namespaces',
17
+ 4: 'namespaces',
18
+ 5: 'classes',
19
+ 23: 'classes',
20
+ 11: 'interfaces',
21
+ 10: 'enums',
22
+ 22: 'enums',
23
+ 12: 'functions',
24
+ 6: 'methods',
25
+ 9: 'methods',
26
+ 7: 'properties',
27
+ 8: 'properties',
28
+ 13: 'variables',
29
+ 14: 'constants',
30
+ 26: 'other'
31
+ };
32
+ export function groupOfKind(kind) {
33
+ return GROUP_OF_KIND[kind] ?? 'other';
34
+ }
35
+ const LETTERS = {
36
+ namespaces: 'N',
37
+ classes: 'C',
38
+ interfaces: 'I',
39
+ enums: 'E',
40
+ functions: 'f',
41
+ methods: 'm',
42
+ properties: 'p',
43
+ variables: 'v',
44
+ constants: 'c',
45
+ other: '·'
46
+ };
47
+ /* The letter in the badge in front of a symbol. */
48
+ export function letterOfKind(kind) {
49
+ return kind === 22 ? 'e' : LETTERS[groupOfKind(kind)];
50
+ }
51
+ export function toneOfKind(kind) {
52
+ const group = groupOfKind(kind);
53
+ return group === 'functions' || group === 'methods'
54
+ ? 'callable'
55
+ : group === 'classes' || group === 'interfaces' || group === 'enums'
56
+ ? 'type'
57
+ : group === 'properties' || group === 'variables' || group === 'constants'
58
+ ? 'value'
59
+ : 'other';
60
+ }
61
+ function isHierarchical(symbols) {
62
+ return symbols.length > 0 && 'selectionRange' in symbols[0];
63
+ }
64
+ /* The symbols of a file, parents before their children, in the order of the file. */
65
+ export function entriesOf(result) {
66
+ const entries = [];
67
+ const add = (name, kind, container, detail, at) => {
68
+ entries.push({ id: String(entries.length), name, kind, container, detail, line: at.line, character: at.character });
69
+ };
70
+ const walk = (symbols, container) => {
71
+ for (const symbol of symbols) {
72
+ add(symbol.name, symbol.kind, container, symbol.detail ?? '', symbol.selectionRange.start);
73
+ walk(symbol.children ?? [], container === '' ? symbol.name : `${container}.${symbol.name}`);
74
+ }
75
+ };
76
+ if (result === null || result.length === 0) {
77
+ return entries;
78
+ }
79
+ if (isHierarchical(result)) {
80
+ walk(result, '');
81
+ }
82
+ else {
83
+ for (const symbol of result) {
84
+ add(symbol.name, symbol.kind, symbol.containerName ?? '', '', symbol.location.range.start);
85
+ }
86
+ }
87
+ return entries;
88
+ }
89
+ /* How well a query matches a name: 3 for a prefix, 2 for a part of it, 1 for its letters in order, 0 for no match. Case is ignored. */
90
+ export function scoreOf(name, query) {
91
+ const haystack = name.toLowerCase();
92
+ const needle = query.toLowerCase();
93
+ if (needle === '') {
94
+ return 1;
95
+ }
96
+ if (haystack.startsWith(needle)) {
97
+ return 3;
98
+ }
99
+ if (haystack.includes(needle)) {
100
+ return 2;
101
+ }
102
+ let from = 0;
103
+ for (const character of needle) {
104
+ from = haystack.indexOf(character, from) + 1;
105
+ if (from === 0) {
106
+ return 0;
107
+ }
108
+ }
109
+ return 1;
110
+ }
111
+ /* The entries that match, best first within each group; without a query, in the order of the file. */
112
+ export function filterEntries(entries, query) {
113
+ const trimmed = query.trim();
114
+ const scored = entries.map((entry, index) => ({ entry, index, score: scoreOf(entry.name, trimmed) })).filter((candidate) => candidate.score > 0);
115
+ return scored
116
+ .sort((left, right) => (trimmed === '' ? left.index - right.index : right.score - left.score || left.index - right.index))
117
+ .map((candidate) => candidate.entry);
118
+ }
119
+ /* The entries in their groups, in the order of the groups, and so in the order the list is walked with the arrows. */
120
+ export function groupEntries(entries) {
121
+ return SYMBOL_GROUPS.flatMap((group) => {
122
+ const own = entries.filter((entry) => groupOfKind(entry.kind) === group);
123
+ return own.length === 0 ? [] : [{ group, entries: own }];
124
+ });
125
+ }
126
+ /* `#name` searches the whole project, `:12` goes to a line, anything else filters the symbols of the file. */
127
+ export function modeOf(input) {
128
+ if (input.startsWith('#')) {
129
+ return { mode: 'workspace', text: input.slice(1).trim() };
130
+ }
131
+ if (input.startsWith(':')) {
132
+ const line = Number.parseInt(input.slice(1), 10);
133
+ return { mode: 'line', line: Number.isFinite(line) && line > 0 ? line : null };
134
+ }
135
+ return { mode: 'symbol', text: input };
136
+ }
137
+ export function workspaceEntriesOf(result) {
138
+ return (result ?? []).map((symbol, index) => ({
139
+ id: String(index),
140
+ name: symbol.name,
141
+ kind: symbol.kind,
142
+ container: symbol.containerName ?? '',
143
+ uri: symbol.location.uri,
144
+ line: 'range' in symbol.location ? symbol.location.range.start.line : 0
145
+ }));
146
+ }
@@ -0,0 +1,12 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ import { type SymbolEntry, type WorkspaceEntry } from './symbol-picker-model.ts';
3
+ export declare class SymbolPickerFeature {
4
+ private readonly language;
5
+ constructor(language: EditorLanguage);
6
+ open(): Promise<void>;
7
+ close(): void;
8
+ goTo(entry: SymbolEntry): void;
9
+ goToLine(line: number): void;
10
+ search(text: string, signal: AbortSignal): Promise<WorkspaceEntry[]>;
11
+ goToWorkspace(entry: WorkspaceEntry): void;
12
+ }
@@ -0,0 +1,77 @@
1
+ import i18next from 'i18next';
2
+ import { StaleResultError } from '@adecore/lsp';
3
+ import { basenameOf } from './paths.js';
4
+ import { entriesOf, workspaceEntriesOf } from './symbol-picker-model.js';
5
+ import { isShortcut } from './shortcut-keys.js';
6
+ const TOAST_ID = 'language-symbols';
7
+ /*
8
+ * Go to symbol: a picker over the symbols the language servers know in the file, filtered as it is typed.
9
+ * A `#` in front searches the whole project and a `:` in front goes to a line. The picker answers to
10
+ * its own input, so the editor only has to open it.
11
+ */
12
+ export class SymbolPickerFeature {
13
+ language;
14
+ constructor(language) {
15
+ this.language = language;
16
+ const off = language.editor.onKeyDown((event) => {
17
+ if (!isShortcut(this.language.shortcuts.goToSymbol, event)) {
18
+ return false;
19
+ }
20
+ void this.open();
21
+ return true;
22
+ });
23
+ language.onDispose(() => {
24
+ off();
25
+ this.close();
26
+ });
27
+ }
28
+ async open() {
29
+ const { project, uri, popups } = this.language;
30
+ if (!project.service.supports('textDocument/documentSymbol', uri)) {
31
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: i18next.t('editor:language.symbols.unavailable') });
32
+ return;
33
+ }
34
+ try {
35
+ const entries = entriesOf(await project.service.documentSymbols(uri));
36
+ popups.setState({ symbols: { file: basenameOf(this.language.uri), entries } });
37
+ }
38
+ catch (error) {
39
+ if (!(error instanceof StaleResultError)) {
40
+ this.language.project.host.notify?.({
41
+ id: TOAST_ID,
42
+ kind: 'error',
43
+ title: i18next.t('editor:language.symbols.failed', { message: error instanceof Error ? error.message : String(error) })
44
+ });
45
+ }
46
+ }
47
+ }
48
+ close() {
49
+ if (this.language.popups.getState().symbols !== null) {
50
+ this.language.popups.setState({ symbols: null });
51
+ this.language.editor.focus();
52
+ }
53
+ }
54
+ /* Puts the caret on a symbol of the file. */
55
+ goTo(entry) {
56
+ this.close();
57
+ this.language.jump({ line: entry.line, character: entry.character });
58
+ }
59
+ /* Puts the caret on a one-based line. */
60
+ goToLine(line) {
61
+ this.close();
62
+ this.language.jump({ line: line - 1, character: 0 });
63
+ }
64
+ /* The symbols of the project that answer to a name, which is a request to the server and so waits for a pause in the typing. */
65
+ async search(text, signal) {
66
+ const { project, uri } = this.language;
67
+ if (text === '' || !project.service.supports('workspace/symbol', uri)) {
68
+ return [];
69
+ }
70
+ return workspaceEntriesOf(await project.service.workspaceSymbols(uri, text, { signal }));
71
+ }
72
+ goToWorkspace(entry) {
73
+ this.close();
74
+ const location = { uri: entry.uri, range: { start: { line: entry.line, character: 0 }, end: { line: entry.line, character: 0 } } };
75
+ this.language.goTo(location);
76
+ }
77
+ }
@@ -0,0 +1,11 @@
1
+ import type { EditorBlock } from '@adecore/editor';
2
+ import type { DocumentSymbolResult } from '@adecore/lsp';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { type Timers } from './timers.ts';
5
+ export declare function blocksOf(result: DocumentSymbolResult): EditorBlock[];
6
+ export declare class SymbolsFeature {
7
+ private readonly refresher;
8
+ private readonly listeners;
9
+ constructor(language: EditorLanguage, timers?: Timers);
10
+ onResult(listener: (result: DocumentSymbolResult) => void): () => void;
11
+ }