@adecore/editor-react 0.0.1 → 0.17.0-beta.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 (264) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +60 -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 +16 -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 +6 -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 +30 -0
  44. package/dist/code-actions.js +326 -0
  45. package/dist/code-authors.d.ts +18 -0
  46. package/dist/code-authors.js +79 -0
  47. package/dist/code-vision-model.d.ts +21 -0
  48. package/dist/code-vision-model.js +183 -0
  49. package/dist/code-vision.d.ts +48 -0
  50. package/dist/code-vision.js +236 -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 +463 -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 +65 -0
  68. package/dist/editor-language.js +170 -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 +95 -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 +46 -0
  80. package/dist/holder.d.ts +5 -0
  81. package/dist/holder.js +19 -0
  82. package/dist/host-types.d.ts +45 -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 +260 -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 +71 -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 +23 -0
  103. package/dist/navigation.js +155 -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 +30 -0
  109. package/dist/peek.js +215 -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 +36 -0
  117. package/dist/project-language.js +190 -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 +21 -0
  131. package/dist/rename.js +257 -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 +38 -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 +76 -0
  162. package/dist/symbols.d.ts +11 -0
  163. package/dist/symbols.js +95 -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 +47 -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 +65 -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 +384 -0
  198. package/src/code-authors.ts +104 -0
  199. package/src/code-vision-model.ts +230 -0
  200. package/src/code-vision.ts +269 -0
  201. package/src/completion-call.ts +139 -0
  202. package/src/completion-model.ts +440 -0
  203. package/src/completion.ts +500 -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 +189 -0
  210. package/src/editor-react.css +43 -0
  211. package/src/find-query.ts +74 -0
  212. package/src/folding.ts +110 -0
  213. package/src/highlight-layers.ts +37 -0
  214. package/src/highlights.ts +123 -0
  215. package/src/history.ts +56 -0
  216. package/src/holder.ts +19 -0
  217. package/src/host-types.ts +52 -0
  218. package/src/hover-content.ts +264 -0
  219. package/src/hover.ts +280 -0
  220. package/src/index.ts +63 -0
  221. package/src/inlay-hints.ts +85 -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 +168 -0
  229. package/src/paths.ts +3 -0
  230. package/src/peek-model.ts +147 -0
  231. package/src/peek.ts +247 -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 +223 -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 +290 -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 +46 -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 +85 -0
  258. package/src/symbols.ts +110 -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
package/src/folding.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { EditorFoldHints, EditorFoldRange, EditorFoldSymbol } from '@adecore/editor';
2
+ import type { DocumentSymbol, DocumentSymbolResult, FoldingRange, SymbolInformation } from '@adecore/lsp';
3
+ import type { EditorLanguage } from './editor-language.ts';
4
+ import { Refresher } from './refresher.ts';
5
+ import { realTimers, type Timers } from './timers.ts';
6
+
7
+ const METHOD = 'textDocument/foldingRange';
8
+ const PAUSE_MS = 500;
9
+
10
+ /* The kind of body a symbol has, by LSP symbol kind; a namespace, a property of a type or an enum member has none worth folding. */
11
+ const BODIES: Readonly<Record<number, EditorFoldSymbol['body']>> = {
12
+ 5: 'class',
13
+ 6: 'method',
14
+ 7: 'value',
15
+ 8: 'value',
16
+ 9: 'method',
17
+ 10: 'class',
18
+ 11: 'class',
19
+ 12: 'function',
20
+ 13: 'value',
21
+ 14: 'value',
22
+ 23: 'class'
23
+ };
24
+
25
+ function walk(symbols: readonly DocumentSymbol[], found: EditorFoldSymbol[]): void {
26
+ for (const symbol of symbols) {
27
+ const body = BODIES[symbol.kind];
28
+ if (body !== undefined && symbol.range.end.line > symbol.range.start.line) {
29
+ found.push({ range: symbol.range, body });
30
+ }
31
+ walk(symbol.children ?? [], found);
32
+ }
33
+ }
34
+
35
+ /* The symbols that have a body of more than one line, as what the editor folds them by. */
36
+ export function foldSymbolsOf(result: DocumentSymbolResult): EditorFoldSymbol[] {
37
+ const found: EditorFoldSymbol[] = [];
38
+ if (result === null || result.length === 0) {
39
+ return found;
40
+ }
41
+ if ('range' in result[0]!) {
42
+ walk(result as DocumentSymbol[], found);
43
+ return found;
44
+ }
45
+ for (const symbol of result as SymbolInformation[]) {
46
+ const body = BODIES[symbol.kind];
47
+ if (body !== undefined && symbol.location.range.end.line > symbol.location.range.start.line) {
48
+ found.push({ range: symbol.location.range, body });
49
+ }
50
+ }
51
+ return found;
52
+ }
53
+
54
+ /* The ranges a server folds that span more than one line. */
55
+ export function foldRangesOf(ranges: readonly FoldingRange[] | null): (EditorFoldRange & { kind?: string })[] {
56
+ return (ranges ?? [])
57
+ .filter((range) => range.endLine > range.startLine)
58
+ .map((range) => ({ startLine: range.startLine, endLine: range.endLine, ...(range.kind === undefined ? {} : { kind: range.kind }) }));
59
+ }
60
+
61
+ /*
62
+ * What the servers know about folding: the bodies of the symbols (read from the symbols the document already
63
+ * has, so nothing is asked twice) and the ranges they fold, such as an element or a Markdown section. The
64
+ * editor folds what the settings choose by them when the file opens, and keeps them with their text after.
65
+ */
66
+ export class FoldingFeature {
67
+ private symbols: EditorFoldSymbol[] = [];
68
+ private ranges: (EditorFoldRange & { kind?: string })[] = [];
69
+ private readonly refresher: Refresher;
70
+ private readonly language: EditorLanguage;
71
+
72
+ constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
73
+ this.language = language;
74
+ const { editor, project, uri } = language;
75
+ this.refresher = new Refresher(
76
+ async (signal) => {
77
+ if (!project.service.supports(METHOD, uri)) {
78
+ return;
79
+ }
80
+ const ranges = await project.service.foldingRanges(uri, { signal });
81
+ if (!signal.aborted) {
82
+ this.ranges = foldRangesOf(ranges);
83
+ this.publish();
84
+ }
85
+ },
86
+ PAUSE_MS,
87
+ timers
88
+ );
89
+ const symbols = onSymbols((result) => {
90
+ this.symbols = foldSymbolsOf(result);
91
+ this.publish();
92
+ });
93
+ const edits = editor.onTextChange(() => this.refresher.later());
94
+ const providers = language.onProvidersChanged(() => {
95
+ this.refresher.now();
96
+ });
97
+ language.onDispose(() => {
98
+ symbols();
99
+ edits();
100
+ providers();
101
+ this.refresher.dispose();
102
+ editor.setFoldHints(null);
103
+ });
104
+ }
105
+
106
+ private publish(): void {
107
+ const hints: EditorFoldHints = { symbols: this.symbols, ranges: this.ranges };
108
+ this.language.editor.setFoldHints(hints);
109
+ }
110
+ }
@@ -0,0 +1,37 @@
1
+ import type { Editor, EditorLineHighlight } from '@adecore/editor';
2
+
3
+ type Provider = () => readonly EditorLineHighlight[];
4
+
5
+ /*
6
+ * The editor has one set of tinted lines, and a conflict and a review each have lines to tint. Every
7
+ * owner hands over a function that says where its lines are now, so setting one owner's never carries
8
+ * another's from before an edit.
9
+ */
10
+ export class HighlightLayers {
11
+ private readonly editor: Editor;
12
+ private readonly owners = new Map<string, Provider>();
13
+
14
+ constructor(editor: Editor) {
15
+ this.editor = editor;
16
+ }
17
+
18
+ set(owner: string, provider: Provider | null): void {
19
+ if (provider === null) {
20
+ this.owners.delete(owner);
21
+ } else {
22
+ this.owners.set(owner, provider);
23
+ }
24
+ this.editor.setLineHighlights([...this.owners.values()].flatMap((read) => read()));
25
+ }
26
+ }
27
+
28
+ const layers = new WeakMap<Editor, HighlightLayers>();
29
+
30
+ export function highlightLayers(editor: Editor): HighlightLayers {
31
+ let found = layers.get(editor);
32
+ if (found === undefined) {
33
+ found = new HighlightLayers(editor);
34
+ layers.set(editor, found);
35
+ }
36
+ return found;
37
+ }
@@ -0,0 +1,123 @@
1
+ import type { DocumentHighlight } from '@adecore/lsp';
2
+ import type { EditorHighlight, EditorHighlightKind, EditorPosition, EditorRange } from '@adecore/editor';
3
+ import { comparePositions, rangeHolds } from './diagnostics-model.ts';
4
+ import { isIdentifierCharacter } from './completion-model.ts';
5
+ import type { EditorLanguage } from './editor-language.ts';
6
+ import { Refresher } from './refresher.ts';
7
+ import { isShortcut } from './shortcut-keys.ts';
8
+ import { realTimers, type Timers } from './timers.ts';
9
+
10
+ const METHOD = 'textDocument/documentHighlight';
11
+ const PAUSE_MS = 150;
12
+
13
+ const KINDS: Record<number, EditorHighlightKind> = { 1: 'text', 2: 'read', 3: 'write' };
14
+
15
+ export function editorHighlightsOf(highlights: readonly DocumentHighlight[]): EditorHighlight[] {
16
+ return highlights
17
+ .filter((highlight) => comparePositions(highlight.range.start, highlight.range.end) < 0)
18
+ .map((highlight) => ({ range: highlight.range, kind: KINDS[highlight.kind ?? 1] ?? 'text' }))
19
+ .sort((left, right) => comparePositions(left.range.start, right.range.start));
20
+ }
21
+
22
+ /* The range before the caret when stepping back, or after it when stepping forward, wrapping round the document; null for no ranges. */
23
+ export function neighborRange(ranges: readonly EditorRange[], from: EditorPosition, direction: 1 | -1): EditorRange | null {
24
+ if (ranges.length === 0) {
25
+ return null;
26
+ }
27
+ const ordered = [...ranges].sort((left, right) => comparePositions(left.start, right.start));
28
+ if (direction === 1) {
29
+ return ordered.find((range) => comparePositions(range.start, from) > 0) ?? ordered[0]!;
30
+ }
31
+ return [...ordered].reverse().find((range) => comparePositions(range.start, from) < 0) ?? ordered[ordered.length - 1]!;
32
+ }
33
+
34
+ /*
35
+ * The other uses of the name at the caret, drawn softly in the text, which the next and previous highlight
36
+ * keys step through. They are asked for when the caret rests on a name and go with the next edit.
37
+ */
38
+ export class HighlightsFeature {
39
+ private readonly language: EditorLanguage;
40
+ private readonly refresher: Refresher;
41
+ private current: readonly EditorHighlight[] = [];
42
+
43
+ constructor(language: EditorLanguage, timers: Timers = realTimers) {
44
+ this.language = language;
45
+ const { editor, project, uri } = language;
46
+ this.refresher = new Refresher(
47
+ async (signal) => {
48
+ if (!project.service.supports(METHOD, uri) || !this.onName()) {
49
+ this.clear();
50
+ return;
51
+ }
52
+ const result = await project.service.documentHighlights(uri, editor.getCaret(), { signal });
53
+ if (!signal.aborted) {
54
+ this.set(result === null ? [] : editorHighlightsOf(result));
55
+ }
56
+ },
57
+ PAUSE_MS,
58
+ timers
59
+ );
60
+ const offs = [
61
+ editor.onCaret((position) => {
62
+ // Moving along the same name keeps its marks, which is what makes stepping through them quiet.
63
+ if (!this.current.some((highlight) => rangeHolds(highlight.range, position))) {
64
+ this.clear();
65
+ }
66
+ this.refresher.later();
67
+ }),
68
+ editor.onTextChange(() => {
69
+ // The editor drops its marks with the edit; this follows, and asks again when the caret settles.
70
+ this.current = [];
71
+ this.refresher.later();
72
+ }),
73
+ editor.onKeyDown((event) => {
74
+ const next = isShortcut(this.language.shortcuts.nextHighlight, event);
75
+ if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
76
+ return false;
77
+ }
78
+ return this.step(next ? 1 : -1);
79
+ })
80
+ ];
81
+ language.onDispose(() => {
82
+ for (const off of offs) {
83
+ off();
84
+ }
85
+ this.refresher.dispose();
86
+ editor.setHighlights([]);
87
+ });
88
+ }
89
+
90
+ /* Puts the caret on the next use of the name, or the one before; false when there are none. */
91
+ step(direction: 1 | -1): boolean {
92
+ const { editor } = this.language;
93
+ const target = neighborRange(
94
+ this.current.map((highlight) => highlight.range),
95
+ editor.getCaret(),
96
+ direction
97
+ );
98
+ if (target === null) {
99
+ return false;
100
+ }
101
+ editor.setCaret(target.start, comparePositions(target.start, editor.getCaret()) > 0 ? 'centerDown' : 'centerUp');
102
+ return true;
103
+ }
104
+
105
+ private onName(): boolean {
106
+ const { editor } = this.language;
107
+ const caret = editor.getCaret();
108
+ const before = caret.character > 0 ? editor.textInRange({ start: { line: caret.line, character: caret.character - 1 }, end: caret }) : '';
109
+ const after = editor.textInRange({ start: caret, end: { line: caret.line, character: caret.character + 1 } });
110
+ return (before !== '' && isIdentifierCharacter(before)) || (after !== '' && isIdentifierCharacter(after));
111
+ }
112
+
113
+ private set(highlights: readonly EditorHighlight[]): void {
114
+ this.current = highlights;
115
+ this.language.editor.setHighlights(highlights);
116
+ }
117
+
118
+ private clear(): void {
119
+ if (this.current.length > 0) {
120
+ this.set([]);
121
+ }
122
+ }
123
+ }
package/src/history.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { EditorLanguage } from './editor-language.ts';
2
+ import { isShortcut } from './shortcut-keys.ts';
3
+
4
+ /* Back, Forward and the list of recent locations over the history the whole project shares. */
5
+ export class HistoryFeature {
6
+ private readonly language: EditorLanguage;
7
+
8
+ constructor(language: EditorLanguage) {
9
+ this.language = language;
10
+ const off = language.editor.onKeyDown((event) => {
11
+ if (isShortcut(this.language.shortcuts.historyBack, event)) {
12
+ this.back();
13
+ return true;
14
+ }
15
+ if (isShortcut(this.language.shortcuts.historyForward, event)) {
16
+ this.forward();
17
+ return true;
18
+ }
19
+ if (isShortcut(this.language.shortcuts.recentLocations, event)) {
20
+ this.recent();
21
+ return true;
22
+ }
23
+ return false;
24
+ });
25
+ language.onDispose(off);
26
+ }
27
+
28
+ /* Returns to where the caret was before the last jump; false when there is no such place. */
29
+ back(): boolean {
30
+ const place = this.language.project.history.back(this.language.place);
31
+ if (place === null) {
32
+ return false;
33
+ }
34
+ this.language.visit(place);
35
+ return true;
36
+ }
37
+
38
+ forward(): boolean {
39
+ const place = this.language.project.history.forward(this.language.place);
40
+ if (place === null) {
41
+ return false;
42
+ }
43
+ this.language.visit(place);
44
+ return true;
45
+ }
46
+
47
+ /* Lists the places of the history under the caret; choosing one jumps there like any other jump. */
48
+ recent(): void {
49
+ const places = this.language.project.history.recent(this.language.place);
50
+ this.language.locations(
51
+ places.map((place) => ({ uri: place.uri, range: { start: place.position, end: place.position } })),
52
+ this.language.editor.getCaret(),
53
+ this.language.project.i18n.t('editor:language.history.recent')
54
+ );
55
+ }
56
+ }
package/src/holder.ts ADDED
@@ -0,0 +1,19 @@
1
+ export function createHolder<T>() {
2
+ let value: T | null = null;
3
+ const listeners = new Set<() => void>();
4
+ return {
5
+ get: () => value,
6
+ set: (next: T | null) => {
7
+ value = next;
8
+ for (const listener of listeners) {
9
+ listener();
10
+ }
11
+ },
12
+ subscribe: (listener: () => void) => {
13
+ listeners.add(listener);
14
+ return () => {
15
+ listeners.delete(listener);
16
+ };
17
+ }
18
+ };
19
+ }
@@ -0,0 +1,52 @@
1
+ import type { i18n } from 'i18next';
2
+ import type { Keymap } from '@adecore/editor/keymap';
3
+ import type { ProjectFiles } from './workspace-edit.ts';
4
+ import type { Place } from './navigation-history.ts';
5
+
6
+ export interface DiskText {
7
+ text: string;
8
+ mtime: number;
9
+ }
10
+
11
+ export interface GitBlameCommit {
12
+ hash: string;
13
+ shortHash: string;
14
+ author: string;
15
+ email: string;
16
+ at: number;
17
+ summary: string;
18
+ }
19
+
20
+ export interface GitBlameResult {
21
+ commits: GitBlameCommit[];
22
+ lines: number[];
23
+ omitted?: 'untracked' | 'too-large';
24
+ }
25
+
26
+ export interface EditorNotification {
27
+ id: string;
28
+ kind: 'success' | 'error';
29
+ title: string;
30
+ }
31
+
32
+ export interface RenameSuggestionsRequest {
33
+ uri: string;
34
+ languageId: string;
35
+ name: string;
36
+ context: string;
37
+ uses: readonly string[];
38
+ }
39
+
40
+ export interface LanguageHost {
41
+ /* The instance the app hands `UIProvider`, which the features read their words from too; the default instance without one. */
42
+ readonly i18n?: i18n;
43
+ readonly folder?: string;
44
+ readonly files?: ProjectFiles;
45
+ readonly apple?: boolean;
46
+ readonly keymap?: Keymap;
47
+ openPlace?(place: Place): void;
48
+ pathOfUri?(uri: string): string | null;
49
+ serverNames?(uri: string): readonly string[];
50
+ notify?(notification: EditorNotification): void;
51
+ suggestNames?(request: RenameSuggestionsRequest, signal: AbortSignal): Promise<readonly string[]>;
52
+ }
@@ -0,0 +1,264 @@
1
+ import type { EditorRange } from '@adecore/editor';
2
+ import type { Hover, Location, LocationLink, MarkedString, NavigationResult } from '@adecore/lsp';
3
+
4
+ export interface SignatureBlock {
5
+ readonly language: string;
6
+ readonly code: string;
7
+ }
8
+
9
+ /* What a server said about a symbol: its signature as code, then prose as markdown. */
10
+ export interface HoverText {
11
+ readonly signatures: readonly SignatureBlock[];
12
+ readonly markdown: string;
13
+ }
14
+
15
+ const FENCE = /^```([\w+#.-]*)[^\n]*\n([\s\S]*?)\n```[ \t]*(?:\n|$)/;
16
+
17
+ /*
18
+ * The leading fenced blocks of a markdown text as signatures, and what follows them, minus the rule
19
+ * between the two that language servers draw, as the prose. A block further down, such as an example
20
+ * in the documentation, stays where it is.
21
+ */
22
+ export function splitSignatures(markdown: string): HoverText {
23
+ const signatures: SignatureBlock[] = [];
24
+ let rest = markdown.replace(/^\s+/, '');
25
+ for (let match = FENCE.exec(rest); match !== null; match = FENCE.exec(rest)) {
26
+ signatures.push({ language: match[1] || 'text', code: match[2]! });
27
+ rest = rest.slice(match[0].length).replace(/^\s+/, '');
28
+ }
29
+ return { signatures, markdown: rest.replace(/^(?:-{3,}|\*{3,})\s*(?:\n|$)/, '').trim() };
30
+ }
31
+
32
+ function markedStringText(content: MarkedString): HoverText {
33
+ if (typeof content !== 'string') {
34
+ return { signatures: [{ language: content.language || 'text', code: content.value }], markdown: '' };
35
+ }
36
+ return splitSignatures(content);
37
+ }
38
+
39
+ /* The text of a hover, whichever of the three shapes the protocol allows it in. */
40
+ export function hoverTextOf(hover: Hover): HoverText {
41
+ const { contents } = hover;
42
+ if (Array.isArray(contents)) {
43
+ const parts = contents.map(markedStringText);
44
+ return {
45
+ signatures: parts.flatMap((part) => part.signatures),
46
+ markdown: parts
47
+ .map((part) => part.markdown)
48
+ .filter(Boolean)
49
+ .join('\n\n')
50
+ };
51
+ }
52
+ if (typeof contents === 'string' || 'language' in contents) {
53
+ return markedStringText(contents);
54
+ }
55
+ // Plain text is text, and not markdown that happens to have underscores in it.
56
+ return contents.kind === 'plaintext'
57
+ ? { signatures: [], markdown: contents.value.replace(/([\\`*_{}[\]()#+\-.!|<>~])/g, '\\$1').trim() }
58
+ : splitSignatures(contents.value);
59
+ }
60
+
61
+ export function isEmptyHover(text: HoverText): boolean {
62
+ return text.signatures.length === 0 && text.markdown === '';
63
+ }
64
+
65
+ function isLink(entry: Location | LocationLink): entry is LocationLink {
66
+ return 'targetUri' in entry;
67
+ }
68
+
69
+ /* Where a navigation result leads, as plain locations: the part of a link that names the symbol is the one to land on. */
70
+ export function locationsOf(result: NavigationResult): Location[] {
71
+ if (result === null) {
72
+ return [];
73
+ }
74
+ const list = Array.isArray(result) ? (result as (Location | LocationLink)[]) : [result];
75
+ return list.map((entry) => (isLink(entry) ? { uri: entry.targetUri, range: entry.targetSelectionRange } : entry));
76
+ }
77
+
78
+ export function sameRange(left: EditorRange, right: EditorRange): boolean {
79
+ return (
80
+ left.start.line === right.start.line &&
81
+ left.start.character === right.start.character &&
82
+ left.end.line === right.end.line &&
83
+ left.end.character === right.end.character
84
+ );
85
+ }
86
+
87
+ /* One `@tag` of a docblock, the way a server writes it into a hover: `_@since_ 1.0` or `*@param* `name` - what it is`. */
88
+ export interface DocTag {
89
+ readonly name: string;
90
+ readonly markdown: string;
91
+ }
92
+
93
+ const TAG_LINE = /^(?:_@([\w-]+)_|\*@([\w-]+)\*|@([\w-]+))(?:\s+|$)([\s\S]*)$/;
94
+
95
+ /* Tags whose text is a block of its own, which a row of the tag list has no room for. */
96
+ const BLOCK_TAGS = new Set(['example']);
97
+
98
+ /*
99
+ * The docblock tags of hover prose apart from the rest, so a card can list them as rows instead of a
100
+ * paragraph each. Only a paragraph that holds nothing but tag lines is taken; prose that mentions an
101
+ * `@` stays prose.
102
+ */
103
+ export function splitDocTags(markdown: string): { readonly markdown: string; readonly tags: readonly DocTag[] } {
104
+ const tags: DocTag[] = [];
105
+ const kept: string[] = [];
106
+ for (const paragraph of markdown.split(/\n{2,}/)) {
107
+ const lines = paragraph.split('\n');
108
+ const matches = lines.map((line) => TAG_LINE.exec(line.trim()));
109
+ const names = matches.map((match) => (match === null ? null : (match[1] ?? match[2] ?? match[3])!));
110
+ if (paragraph.trim() === '' || names.some((name) => name === null || BLOCK_TAGS.has(name))) {
111
+ kept.push(paragraph);
112
+ continue;
113
+ }
114
+ matches.forEach((match, index) => {
115
+ // The TypeScript server puts a dash between a tag, its parameter and its text.
116
+ const text = match![4]!.replace(/^[\u2014\u2013-]\s*/, '').replace(/^(`[^`]+`)\s+[\u2014\u2013-]\s+/, '$1 ');
117
+ tags.push({ name: names[index]!, markdown: text.trim() });
118
+ });
119
+ }
120
+ // By name, and in the order written within one name, so parameters keep the order of the signature.
121
+ return { markdown: kept.join('\n\n').trim(), tags: tags.toSorted((left, right) => left.name.localeCompare(right.name)) };
122
+ }
123
+
124
+ /* One symbol of a hover. A server can describe several at once, such as a class and its constructor. */
125
+ export interface HoverSection {
126
+ /* The qualified name a server heads the section with, when it gives one. */
127
+ readonly title: string | null;
128
+ readonly signatures: readonly SignatureBlock[];
129
+ readonly markdown: string;
130
+ readonly tags: readonly DocTag[];
131
+ readonly baseline: Baseline | null;
132
+ }
133
+
134
+ export type BaselineLevel = 'widely' | 'newly' | 'limited';
135
+
136
+ /* How widely a web feature is available, as the CSS servers state it under a property. */
137
+ export interface Baseline {
138
+ readonly level: BaselineLevel;
139
+ readonly text: string;
140
+ }
141
+
142
+ /* A CSS server draws the status as a data URL image in front of an italic sentence, and a data URL is not an address the markdown renderer lets through. */
143
+ const BASELINE = /!\[Baseline icon\]\([^)\s]*\)[ \t]*_([^\n]+?)_[ \t]*(?:\n|$)/;
144
+
145
+ /* The Baseline status a text carries, which the card draws with a mark of its own, and the text without it. */
146
+ function takeBaseline(markdown: string): { baseline: Baseline | null; rest: string } {
147
+ const match = BASELINE.exec(markdown);
148
+ if (match === null) {
149
+ return { baseline: null, rest: markdown };
150
+ }
151
+ const text = match[1]!.trim();
152
+ const level = text.startsWith('Widely') ? 'widely' : text.startsWith('Newly') ? 'newly' : 'limited';
153
+ return { baseline: { level, text }, rest: (markdown.slice(0, match.index) + markdown.slice(match.index + match[0].length)).replace(/\n{3,}/g, '\n\n') };
154
+ }
155
+
156
+ const RULE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/m;
157
+ const TITLE = /^(?:__(.+)__|\*\*(.+)\*\*)\s*(?:\n|$)/;
158
+ const ANY_FENCE = /```([\w+#.-]*)[^\n]*\n([\s\S]*?)\n```[ \t]*(?:\n|$)/;
159
+
160
+ /* The PHP server opens every snippet with a tag the reader does not need to see. */
161
+ function withoutOpenTag(block: SignatureBlock): SignatureBlock {
162
+ return { language: block.language, code: block.code.replace(/^<\?php\s*\n/, '') };
163
+ }
164
+
165
+ const HTML_CODE = /<(code|pre)>([\s\S]*?)<\/\1>/g;
166
+ const INLINE_TAG = /\{@(see|link|linkplain)\s+([^}\s]+)(?:\s+([^}]+?))?\s*\}/g;
167
+
168
+ function decodeEntities(text: string): string {
169
+ return text
170
+ .replace(/&lt;/g, '<')
171
+ .replace(/&gt;/g, '>')
172
+ .replace(/&quot;/g, '"')
173
+ .replace(/&#0?39;/g, "'")
174
+ .replace(/&amp;/g, '&');
175
+ }
176
+
177
+ function dedent(text: string): string {
178
+ const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
179
+ const indents = lines.filter((line) => line.trim() !== '').map((line) => line.length - line.trimStart().length);
180
+ const common = indents.length === 0 ? 0 : Math.min(...indents);
181
+ return lines.map((line) => line.slice(common)).join('\n');
182
+ }
183
+
184
+ /*
185
+ * What a docblock writes for a reader of its source, written for one of a hover: a `<code>` or `<pre>`
186
+ * example as a block in the symbol's language, and an inline `{@see Name}` or `{@link Name label}` as
187
+ * the name in code. Servers pass both on as they are, and markdown would show the tags as text.
188
+ */
189
+ export function docblockMarkdown(markdown: string, language: string): string {
190
+ return markdown
191
+ .replace(HTML_CODE, (_match, _tag: string, body: string) => {
192
+ const code = decodeEntities(body);
193
+ return code.includes('\n') ? `\n\n\`\`\`${language}\n${dedent(code)}\n\`\`\`\n\n` : `\`${code.trim()}\``;
194
+ })
195
+ .replace(INLINE_TAG, (_match, _tag: string, target: string, label: string | undefined) => (label === undefined ? `\`${target}\`` : label))
196
+ .replace(/\n{3,}/g, '\n\n')
197
+ .trim();
198
+ }
199
+
200
+ /* Markdown cut at its fenced blocks, so a card can draw the blocks as compactly as its signatures. */
201
+ export type MarkdownPart = { readonly kind: 'prose'; readonly text: string } | { readonly kind: 'code'; readonly block: SignatureBlock };
202
+
203
+ export function markdownParts(markdown: string): MarkdownPart[] {
204
+ const parts: MarkdownPart[] = [];
205
+ let rest = markdown;
206
+ for (let match = ANY_FENCE.exec(rest); match !== null; match = ANY_FENCE.exec(rest)) {
207
+ const prose = rest.slice(0, match.index).trim();
208
+ if (prose !== '') {
209
+ parts.push({ kind: 'prose', text: prose });
210
+ }
211
+ parts.push({ kind: 'code', block: { language: match[1] || 'text', code: match[2]! } });
212
+ rest = rest.slice(match.index + match[0].length);
213
+ }
214
+ if (rest.trim() !== '') {
215
+ parts.push({ kind: 'prose', text: rest.trim() });
216
+ }
217
+ return parts;
218
+ }
219
+
220
+ /* The title is drawn as text, so the escapes that kept it from reading as markdown come off: `A\\B::\_\_construct` is `A\B::__construct`. */
221
+ function unescapeMarkdown(text: string): string {
222
+ return text.replace(/\\([\\`*_{}[\]()#+\-.!|<>~])/g, '$1');
223
+ }
224
+
225
+ function sectionOf(chunk: string, leading: readonly SignatureBlock[]): HoverSection {
226
+ let rest = chunk.trim();
227
+ const title = TITLE.exec(rest);
228
+ if (title !== null) {
229
+ rest = rest.slice(title[0].length).trim();
230
+ }
231
+ const signatures = [...leading];
232
+ // A section's own snippet is its first block; one further down after the prose, such as an example, stays prose.
233
+ const fence = ANY_FENCE.exec(rest);
234
+ if (fence !== null && !/@example/.test(rest.slice(0, fence.index))) {
235
+ signatures.push({ language: fence[1] || 'text', code: fence[2]! });
236
+ rest = (rest.slice(0, fence.index) + rest.slice(fence.index + fence[0].length)).trim();
237
+ }
238
+ const taken = takeBaseline(rest);
239
+ const doc = splitDocTags(docblockMarkdown(taken.rest, signatures[0]?.language ?? 'text'));
240
+ return {
241
+ title: title === null ? null : unescapeMarkdown((title[1] ?? title[2])!.trim()),
242
+ signatures: signatures.map(withoutOpenTag),
243
+ markdown: doc.markdown,
244
+ tags: doc.tags,
245
+ baseline: taken.baseline
246
+ };
247
+ }
248
+
249
+ /* The symbols of a hover apart, split where a server draws a rule between them, each described once. */
250
+ export function hoverSectionsOf(text: HoverText): HoverSection[] {
251
+ const chunks = text.markdown === '' ? [''] : text.markdown.split(RULE);
252
+ const sections = chunks.map((chunk, index) => sectionOf(chunk, index === 0 ? text.signatures : []));
253
+ const seen = new Set<string>();
254
+ return sections.filter((section) => {
255
+ const empty =
256
+ section.title === null && section.signatures.length === 0 && section.markdown === '' && section.tags.length === 0 && section.baseline === null;
257
+ const key = JSON.stringify([section.title, section.signatures, section.markdown, section.baseline]);
258
+ if (empty || seen.has(key)) {
259
+ return false;
260
+ }
261
+ seen.add(key);
262
+ return true;
263
+ });
264
+ }