@adecore/editor-react 0.22.1 → 0.23.0

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 (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
@@ -1,3 +1,5 @@
1
+ import type { WorkerRequestPort } from '@adecore/editor/worker-client';
2
+ import type { EditorPosition, EditorRange } from '@adecore/editor';
1
3
  import type { i18n } from 'i18next';
2
4
  import type { Keymap } from '@adecore/editor/keymap';
3
5
  import type { ProjectFiles } from './workspace-edit.ts';
@@ -31,7 +33,21 @@ export interface RenameSuggestionsRequest {
31
33
  context: string;
32
34
  uses: readonly string[];
33
35
  }
36
+ export interface SnippetVariableContext {
37
+ readonly uri: string;
38
+ readonly languageId: string;
39
+ readonly position: EditorPosition;
40
+ readonly selection: EditorRange;
41
+ readonly selectedText: string;
42
+ readonly currentLine: string;
43
+ readonly currentWord: string;
44
+ readonly cursorIndex: number;
45
+ readonly cursorCount: number;
46
+ readonly folder?: string;
47
+ readonly path?: string;
48
+ }
34
49
  export interface LanguageHost {
50
+ readonly snippetTransformWorker?: () => WorkerRequestPort;
35
51
  readonly i18n?: i18n;
36
52
  readonly folder?: string;
37
53
  readonly files?: ProjectFiles;
@@ -41,5 +57,6 @@ export interface LanguageHost {
41
57
  pathOfUri?(uri: string): string | null;
42
58
  serverNames?(uri: string): readonly string[];
43
59
  notify?(notification: EditorNotification): void;
60
+ resolveSnippetVariable?(name: string, context: SnippetVariableContext): string | undefined;
44
61
  suggestNames?(request: RenameSuggestionsRequest, signal: AbortSignal): Promise<readonly string[]>;
45
62
  }
package/dist/hover.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { EditorPosition, EditorRange } from '@adecore/editor';
2
2
  import type { EditorLanguage } from './editor-language.ts';
3
+ import { type HoverText } from './hover-content.ts';
3
4
  import { type Timers } from './timers.ts';
5
+ export declare const SHOW_DELAY_MS = 300;
4
6
  export declare class HoverFeature {
5
7
  private readonly language;
6
8
  private readonly timers;
@@ -19,6 +21,8 @@ export declare class HoverFeature {
19
21
  showProblemsAt(position: EditorPosition): boolean;
20
22
  keep(): void;
21
23
  showRange(range: EditorRange): void;
24
+ showText(position: EditorPosition, text: HoverText): void;
25
+ release(): void;
22
26
  hide(): void;
23
27
  private moved;
24
28
  private scheduleHide;
package/dist/hover.js CHANGED
@@ -4,7 +4,7 @@ import { lineTextOf } from './line-text.js';
4
4
  import { wordRangeAt } from './rename-model.js';
5
5
  import { isShortcut } from './shortcut-keys.js';
6
6
  import { realTimers } from './timers.js';
7
- const SHOW_DELAY_MS = 300;
7
+ export const SHOW_DELAY_MS = 300;
8
8
  const HIDE_DELAY_MS = 250;
9
9
  /* How long a card has to stay up before the references of its symbol are counted, which can be a request over the whole project. */
10
10
  const COUNT_DELAY_MS = 500;
@@ -61,7 +61,7 @@ export class HoverFeature {
61
61
  this.quickInfo();
62
62
  return true;
63
63
  }
64
- if (event.key === 'Escape' && this.language.popups.getState().hover !== null) {
64
+ if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy == null) {
65
65
  this.hide();
66
66
  return true;
67
67
  }
@@ -114,6 +114,15 @@ export class HoverFeature {
114
114
  this.keep();
115
115
  this.present(range.start, [], null, range);
116
116
  }
117
+ /* A card of text another feature brings, such as the tooltip of an inlay hint, which goes the way any hover card does. */
118
+ showText(position, text) {
119
+ this.hide();
120
+ this.present(position, [], { text, definition: null, word: '', references: null, plain: true }, null);
121
+ }
122
+ /* The pointer left what a card was opened for: it closes unless the pointer reaches the card in time. */
123
+ release() {
124
+ this.scheduleHide();
125
+ }
117
126
  hide() {
118
127
  this.pinned = false;
119
128
  this.held = false;
@@ -199,6 +208,10 @@ export class HoverFeature {
199
208
  }
200
209
  if (problems.length === 0 && info === null) {
201
210
  this.hide();
211
+ if (pinned && (await this.language.inlayHints.showTooltipAt(position))) {
212
+ this.pinned = true;
213
+ return;
214
+ }
202
215
  if (pinned) {
203
216
  this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.hover.none') });
204
217
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export { EditorLanguage } from './editor-language.ts';
2
- export { ProjectLanguage } from './project-language.ts';
1
+ export { EditorLanguage, type EditorLanguageOptions } from './editor-language.ts';
2
+ export { DEFAULT_RANGE_LINES, DEFAULT_RANGE_CHARACTERS, type SemanticTokensOptions } from './semantic-tokens.ts';
3
+ export { ProjectLanguage, type FileOperationTarget } from './project-language.ts';
3
4
  export type { LanguageDocumentHandle } from './project-language.ts';
4
- export type { LanguageHost, DiskText, GitBlameCommit, GitBlameResult, EditorNotification, RenameSuggestionsRequest } from './host-types.ts';
5
+ export type { SnippetVariableContext, LanguageHost, DiskText, GitBlameCommit, GitBlameResult, EditorNotification, RenameSuggestionsRequest } from './host-types.ts';
5
6
  export { EditorView } from './EditorView.tsx';
6
7
  export { createHolder } from './holder.ts';
7
8
  export type { EditorViewProps } from './EditorView.tsx';
@@ -13,8 +14,14 @@ export { SignatureCard } from './SignatureCard.tsx';
13
14
  export { RenameCard } from './RenameCard.tsx';
14
15
  export { PickPopup } from './PickPopup.tsx';
15
16
  export { PeekPanel } from './PeekPanel.tsx';
17
+ export { CallHierarchyPanel } from './CallHierarchyPanel.tsx';
16
18
  export { SymbolPicker } from './SymbolPicker.tsx';
17
19
  export { CodeAuthorsCard } from './CodeAuthorsCard.tsx';
20
+ export { RenderedDocs } from './RenderedDocs.tsx';
21
+ export { RenderedDocsFeature } from './rendered-docs.ts';
22
+ export type { RenderedDocEntry, RenderedDocsSettings } from './rendered-docs.ts';
23
+ export { parseDocComment, sectionsOf, htmlToMarkdown, DOC_LINK_PREFIX } from './doc-comment-model.ts';
24
+ export type { DocTagEntry, DocTagSection, ParsedDocComment } from './doc-comment-model.ts';
18
25
  export { EditorContextMenu } from './EditorContextMenu.tsx';
19
26
  export { EditorRenderingProvider } from './rendering.tsx';
20
27
  export type { EditorRendering } from './rendering-context.ts';
@@ -23,13 +30,13 @@ export { ChangeReview } from './ChangeReview.tsx';
23
30
  export type { ChangeReviewProps } from './ChangeReview.tsx';
24
31
  export { ProjectProblems, countsOf, compareRows } from './project-problems.ts';
25
32
  export type { ProblemFile, ProblemRow } from './project-problems.ts';
26
- export { applyWorkspaceEdit } from './workspace-edit.ts';
33
+ export { applyWorkspaceEdit, supportedResourceOperations } from './workspace-edit.ts';
27
34
  export type { ProjectFiles, StagedFile, WorkspaceEditHost } from './workspace-edit.ts';
28
35
  export { useProblemCounts } from './use-problem-counts.ts';
29
36
  export type { ProblemCounts } from './diagnostics.ts';
30
37
  export type { BlameSource, CodeVisionSettings } from './code-vision.ts';
31
38
  export type { Problem } from './diagnostics-model.ts';
32
- export type { PopupState, PopupStore, HoverInfo, HoverView, CompletionView, CompletionDocs, CompletionRow, SignatureView, RenameView, RenameFileView, PeekView, PickView, PickGroup, PickRow, PickPreview, SymbolsView, MenuView, AuthorsView } from './popups.ts';
39
+ export type { PopupState, PopupStore, HoverInfo, HoverView, CompletionView, CompletionDocs, CompletionRow, SignatureView, RenameView, RenameFileView, PeekView, CallHierarchyView, PickView, PickGroup, PickRow, PickPreview, SymbolsView, MenuView, AuthorsView } from './popups.ts';
33
40
  export { RowHost, LineActionHost } from './row-host.ts';
34
41
  export type { HostedRow, HostedAction } from './row-host.ts';
35
42
  export { HighlightLayers, highlightLayers } from './highlight-layers.ts';
@@ -42,3 +49,10 @@ export { AttributionCard } from './AttributionCard.tsx';
42
49
  export type { AttributionCardProps } from './AttributionCard.tsx';
43
50
  export { ReviewGroup, joinReviewGroup } from './review-group.ts';
44
51
  export type { ReviewMember } from './review-group.ts';
52
+ export { DiffView } from './DiffView.tsx';
53
+ export type { DiffViewProps } from './DiffView.tsx';
54
+ export type { DiffLimits } from './diff-expansion.ts';
55
+ export { diffSyntaxDocument } from './diff-syntax.ts';
56
+ export type { DiffSourceSide, DiffSourceTokens, DiffSyntaxSources } from './diff-syntax.ts';
57
+ export { decoratePatch, usePatchDecorations } from './patch-decorations.ts';
58
+ export type { PatchDecoration, PatchDecorationOptions, PatchLineLabels, PatchTimer } from './patch-decorations.ts';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { EditorLanguage } from './editor-language.js';
2
+ export { DEFAULT_RANGE_LINES, DEFAULT_RANGE_CHARACTERS } from './semantic-tokens.js';
2
3
  export { ProjectLanguage } from './project-language.js';
3
4
  export { EditorView } from './EditorView.js';
4
5
  export { createHolder } from './holder.js';
@@ -10,14 +11,18 @@ export { SignatureCard } from './SignatureCard.js';
10
11
  export { RenameCard } from './RenameCard.js';
11
12
  export { PickPopup } from './PickPopup.js';
12
13
  export { PeekPanel } from './PeekPanel.js';
14
+ export { CallHierarchyPanel } from './CallHierarchyPanel.js';
13
15
  export { SymbolPicker } from './SymbolPicker.js';
14
16
  export { CodeAuthorsCard } from './CodeAuthorsCard.js';
17
+ export { RenderedDocs } from './RenderedDocs.js';
18
+ export { RenderedDocsFeature } from './rendered-docs.js';
19
+ export { parseDocComment, sectionsOf, htmlToMarkdown, DOC_LINK_PREFIX } from './doc-comment-model.js';
15
20
  export { EditorContextMenu } from './EditorContextMenu.js';
16
21
  export { EditorRenderingProvider } from './rendering.js';
17
22
  export { FindReplace } from './FindReplace.js';
18
23
  export { ChangeReview } from './ChangeReview.js';
19
24
  export { ProjectProblems, countsOf, compareRows } from './project-problems.js';
20
- export { applyWorkspaceEdit } from './workspace-edit.js';
25
+ export { applyWorkspaceEdit, supportedResourceOperations } from './workspace-edit.js';
21
26
  export { useProblemCounts } from './use-problem-counts.js';
22
27
  export { RowHost, LineActionHost } from './row-host.js';
23
28
  export { HighlightLayers, highlightLayers } from './highlight-layers.js';
@@ -26,3 +31,6 @@ export { openingPlace, viewStates, followViewStates, viewStateKey, forgetMovesFr
26
31
  export { lspLanguageIdOf, shikiLanguageOf, shikiLanguageOfPath } from './language-ids.js';
27
32
  export { AttributionCard } from './AttributionCard.js';
28
33
  export { ReviewGroup, joinReviewGroup } from './review-group.js';
34
+ export { DiffView } from './DiffView.js';
35
+ export { diffSyntaxDocument } from './diff-syntax.js';
36
+ export { decoratePatch, usePatchDecorations } from './patch-decorations.js';
@@ -1,13 +1,58 @@
1
- import type { EditorInlayHint, EditorPosition, EditorRange } from '@adecore/editor';
2
- import type { InlayHint } from '@adecore/lsp';
1
+ import type { Editor, EditorInlayClick, EditorInlayHint, EditorPosition, EditorRange } from '@adecore/editor';
2
+ import type { Command, InlayHint, InlayHintLabelPart, Location, MarkupContent, TextEdit } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
+ import { type HoverText } from './hover-content.ts';
4
5
  import { type Timers } from './timers.ts';
5
6
  export declare function hintLabelOf(hint: InlayHint): string;
7
+ export declare function drawableHints(hints: readonly InlayHint[]): InlayHint[];
8
+ export declare function editorHintOf(hint: InlayHint, id: string): EditorInlayHint;
6
9
  export declare function editorHintsOf(hints: readonly InlayHint[]): EditorInlayHint[];
7
10
  export declare function withMargin(visible: EditorRange, endOfDocument: EditorPosition, endOfLine: (line: number) => EditorPosition): EditorRange;
8
11
  export declare function covers(asked: EditorRange, visible: EditorRange): boolean;
12
+ export declare function tooltipTextOf(tooltip: string | MarkupContent | undefined): HoverText | null;
13
+ export declare function partOf(hint: InlayHint, index: number | null): InlayHintLabelPart | null;
14
+ export declare function tooltipOf(hint: InlayHint, index: number | null): HoverText | null;
15
+ export declare function lacksInfo(hint: InlayHint, index: number | null): boolean;
16
+ export declare function editsFit(editor: Editor, edits: readonly TextEdit[]): boolean;
17
+ type Action = {
18
+ readonly kind: 'go';
19
+ readonly location: Location;
20
+ } | {
21
+ readonly kind: 'run';
22
+ readonly command: Command;
23
+ } | {
24
+ readonly kind: 'insert';
25
+ readonly edits: TextEdit[];
26
+ };
27
+ export declare function actionOf(hint: InlayHint, click: Pick<EditorInlayClick, 'part' | 'mod' | 'clicks'>): Action | null;
9
28
  export declare class InlayHintsFeature {
29
+ private readonly language;
30
+ private readonly timers;
10
31
  private readonly refresher;
11
32
  private asked;
33
+ private readonly entries;
34
+ private shown;
35
+ private generation;
36
+ private epoch;
37
+ private target;
38
+ private modHeld;
39
+ private showTimer;
40
+ private commandTimer;
41
+ private pressCount;
42
+ private request;
12
43
  constructor(language: EditorLanguage, timers?: Timers);
44
+ setModHeld(held: boolean): void;
45
+ showTooltipAt(position: EditorPosition): Promise<boolean>;
46
+ private install;
47
+ private hovered;
48
+ private cancelHover;
49
+ private cardOpen;
50
+ private showTooltip;
51
+ private resolve;
52
+ private resolvable;
53
+ private updateLink;
54
+ private pressed;
55
+ private press;
56
+ private perform;
13
57
  }
58
+ export {};
@@ -1,20 +1,41 @@
1
+ import { hoverTextOf, isEmptyHover } from './hover-content.js';
2
+ import { SHOW_DELAY_MS } from './hover.js';
3
+ import { messageOf } from './error-message.js';
1
4
  import { lineTextOf } from './line-text.js';
2
5
  import { Refresher } from './refresher.js';
3
6
  import { realTimers } from './timers.js';
4
7
  const METHOD = 'textDocument/inlayHint';
8
+ const RESOLVE_METHOD = 'inlayHint/resolve';
9
+ const TOAST_ID = 'language-inlay-hint';
5
10
  const PAUSE_MS = 300;
11
+ /* How long a click on a hint that a double click would also act on waits for its second press; the editor's own interval for a double press. */
12
+ const DOUBLE_PRESS_MS = 500;
6
13
  /* Lines asked for above and below what is in view, so a little scrolling stays inside what is already drawn. */
7
14
  const MARGIN_LINES = 60;
8
15
  /* A hint's label is a string or a list of parts, which are read as one text. */
9
16
  export function hintLabelOf(hint) {
10
17
  return typeof hint.label === 'string' ? hint.label : hint.label.map((part) => part.value).join('');
11
18
  }
12
- export function editorHintsOf(hints) {
19
+ /* The hints worth drawing, in the order of the text. */
20
+ export function drawableHints(hints) {
13
21
  return hints
14
- .map((hint) => ({ position: hint.position, label: hintLabelOf(hint) }))
15
- .filter((hint) => hint.label !== '')
22
+ .filter((hint) => hintLabelOf(hint) !== '')
16
23
  .sort((left, right) => left.position.line - right.position.line || left.position.character - right.position.character);
17
24
  }
25
+ /* A hint as the editor draws it: a label with parts keeps them apart so each can be pointed at. */
26
+ export function editorHintOf(hint, id) {
27
+ return {
28
+ id,
29
+ position: hint.position,
30
+ label: hintLabelOf(hint),
31
+ ...(typeof hint.label === 'string' ? {} : { parts: hint.label.map((part) => part.value) }),
32
+ ...(hint.paddingLeft ? { paddingLeft: true } : {}),
33
+ ...(hint.paddingRight ? { paddingRight: true } : {})
34
+ };
35
+ }
36
+ export function editorHintsOf(hints) {
37
+ return drawableHints(hints).map((hint, index) => editorHintOf(hint, `hint-${index}`));
38
+ }
18
39
  /* The lines in view with a margin of lines around them, from the start of the first to the end of the last. */
19
40
  export function withMargin(visible, endOfDocument, endOfLine) {
20
41
  const first = Math.max(0, visible.start.line - MARGIN_LINES);
@@ -25,16 +46,102 @@ export function withMargin(visible, endOfDocument, endOfLine) {
25
46
  export function covers(asked, visible) {
26
47
  return asked.start.line <= visible.start.line && asked.end.line >= visible.end.line;
27
48
  }
49
+ /* The words of a tooltip: plain text is escaped so it reads as typed, Markdown goes through the hover card's own rendering. */
50
+ export function tooltipTextOf(tooltip) {
51
+ if (tooltip === undefined) {
52
+ return null;
53
+ }
54
+ const text = hoverTextOf({ contents: typeof tooltip === 'string' ? { kind: 'plaintext', value: tooltip } : tooltip });
55
+ return isEmptyHover(text) ? null : text;
56
+ }
57
+ /* One part of a hint's label, or null for a hint whose label is a string. */
58
+ export function partOf(hint, index) {
59
+ return index === null || typeof hint.label === 'string' ? null : (hint.label[index] ?? null);
60
+ }
61
+ /* What a hover shows: the part's tooltip, else the hint's, else (with no part to point at) that of the first part that has one. */
62
+ export function tooltipOf(hint, index) {
63
+ const part = partOf(hint, index);
64
+ const own = part?.tooltip ?? hint.tooltip;
65
+ if (own !== undefined || index !== null || typeof hint.label === 'string') {
66
+ return tooltipTextOf(own);
67
+ }
68
+ return tooltipTextOf(hint.label.find((candidate) => candidate.tooltip !== undefined)?.tooltip);
69
+ }
70
+ /* Whether a server could still say more about the hint: nothing in it gives a tooltip, a place or a command yet. */
71
+ export function lacksInfo(hint, index) {
72
+ if (hint.tooltip !== undefined) {
73
+ return false;
74
+ }
75
+ const parts = typeof hint.label === 'string' ? [] : index === null ? hint.label : [hint.label[index]];
76
+ return !parts.some((part) => part !== undefined && (part.tooltip !== undefined || part.location !== undefined || part.command !== undefined));
77
+ }
78
+ /* Whether the edits fit the text as it is now: positions inside it, each range in order, none overlapping the next. */
79
+ export function editsFit(editor, edits) {
80
+ const end = editor.positionAt(Number.MAX_SAFE_INTEGER);
81
+ const inside = (position) => Number.isInteger(position.line) &&
82
+ Number.isInteger(position.character) &&
83
+ position.line >= 0 &&
84
+ position.character >= 0 &&
85
+ position.line <= end.line &&
86
+ position.character <= lineTextOf(editor, position.line).length;
87
+ const before = (left, right) => left.line < right.line || (left.line === right.line && left.character <= right.character);
88
+ const ordered = [...edits].sort((left, right) => (before(left.range.start, right.range.start) ? -1 : 1));
89
+ return ordered.every((edit, index) => {
90
+ const next = ordered[index + 1];
91
+ return (inside(edit.range.start) &&
92
+ inside(edit.range.end) &&
93
+ before(edit.range.start, edit.range.end) &&
94
+ (next === undefined || before(edit.range.end, next.range.start)));
95
+ });
96
+ }
97
+ /* What a press does, from what is known of the hint: a double click inserts it and nothing else, Mod+click follows a part's place, a plain click runs its command. */
98
+ export function actionOf(hint, click) {
99
+ if (click.clicks === 2) {
100
+ return hint.textEdits !== undefined && hint.textEdits.length > 0 ? { kind: 'insert', edits: hint.textEdits } : null;
101
+ }
102
+ const part = partOf(hint, click.part);
103
+ if (click.mod) {
104
+ return part?.location === undefined ? null : { kind: 'go', location: part.location };
105
+ }
106
+ return part?.command === undefined ? null : { kind: 'run', command: part.command };
107
+ }
28
108
  /*
29
109
  * Types and parameter names the servers infer, asked for the lines in view and a margin around them. The
30
110
  * last ones stay until the answer comes, so nothing blinks while typing; scrolling asks again only once the
31
111
  * view leaves the lines that were asked for.
112
+ *
113
+ * A hint can be acted on with the pointer. The server is asked to resolve a hint as the pointer arrives, and
114
+ * resting on it for the hover delay shows its tooltip. Mod+click on a part follows its place, a click on a
115
+ * part runs its command and a double click inserts its text edits as one undo step, never together with the
116
+ * command: when a hint has both, the command waits for the double press interval. A press on a hint that
117
+ * offers nothing (yet) moves the caret as beside the text. A finger has no hover, so a tap shows the
118
+ * tooltip and does nothing more. All of it is for the pointer: the hints are not part
119
+ * of the text and stay out of the keyboard's way, and Quick Info on the caret shows the tooltip of a hint that
120
+ * sits there.
32
121
  */
33
122
  export class InlayHintsFeature {
123
+ language;
124
+ timers;
34
125
  refresher;
35
126
  /* The lines the hints on screen were asked for. */
36
127
  asked = null;
128
+ entries = new Map();
129
+ shown = [];
130
+ generation = 0;
131
+ /* Counts the edits of the text, which hints and answers made for an earlier one are tested against. */
132
+ epoch = 0;
133
+ target = null;
134
+ modHeld = false;
135
+ showTimer;
136
+ /* The command of a click that may still become a double click. */
137
+ commandTimer;
138
+ /* Counts presses, so one that waited for a resolve gives way to a later one. */
139
+ pressCount = 0;
140
+ /* Counts what the pointer does, so a tooltip that was waited for is dropped once the pointer has moved on. */
141
+ request = 0;
37
142
  constructor(language, timers = realTimers) {
143
+ this.language = language;
144
+ this.timers = timers;
38
145
  const { editor, project, uri } = language;
39
146
  this.refresher = new Refresher(async (signal) => {
40
147
  const { service } = project;
@@ -46,26 +153,256 @@ export class InlayHintsFeature {
46
153
  character: lineTextOf(editor, line).length
47
154
  }));
48
155
  this.asked = range;
156
+ const epoch = this.epoch;
49
157
  const hints = await service.inlayHints(uri, range, { signal });
50
- if (!signal.aborted) {
51
- editor.setInlayHints(hints === null ? [] : editorHintsOf(hints));
158
+ if (!signal.aborted && epoch === this.epoch) {
159
+ this.install(hints === null ? [] : drawableHints(hints), epoch);
52
160
  }
53
161
  }, PAUSE_MS, timers);
54
- const edits = editor.onTextChange(() => this.refresher.later());
55
- const scrolls = editor.onViewChange(() => {
56
- if (this.asked !== null && !covers(this.asked, editor.getVisibleRange())) {
162
+ const offs = [
163
+ editor.onTextChange(() => {
164
+ this.epoch++;
165
+ this.cancelHover();
57
166
  this.refresher.later();
58
- }
59
- });
60
- const providers = language.onProvidersChanged(() => {
61
- this.refresher.now();
62
- });
167
+ }),
168
+ editor.onViewChange(() => {
169
+ if (this.asked !== null && !covers(this.asked, editor.getVisibleRange())) {
170
+ this.refresher.later();
171
+ }
172
+ }),
173
+ language.onProvidersChanged(() => {
174
+ this.refresher.now();
175
+ })
176
+ ];
177
+ if (editor.onInlayHover !== undefined) {
178
+ offs.push(editor.onInlayHover((target) => this.hovered(target === null ? null : { id: target.id, part: target.part })));
179
+ }
180
+ if (editor.onInlayClick !== undefined) {
181
+ offs.push(editor.onInlayClick((click) => this.pressed(click)));
182
+ }
183
+ if (typeof window !== 'undefined') {
184
+ const modKey = language.apple ? 'Meta' : 'Control';
185
+ const keys = (event) => {
186
+ if (event.key === modKey) {
187
+ this.setModHeld(event.type === 'keydown');
188
+ }
189
+ };
190
+ const lost = () => this.setModHeld(false);
191
+ window.addEventListener('keydown', keys);
192
+ window.addEventListener('keyup', keys);
193
+ window.addEventListener('blur', lost);
194
+ offs.push(() => {
195
+ window.removeEventListener('keydown', keys);
196
+ window.removeEventListener('keyup', keys);
197
+ window.removeEventListener('blur', lost);
198
+ });
199
+ }
63
200
  language.onDispose(() => {
64
- edits();
65
- scrolls();
66
- providers();
201
+ for (const off of offs) {
202
+ off();
203
+ }
204
+ this.cancelHover();
205
+ this.timers.clear(this.commandTimer);
67
206
  this.refresher.dispose();
207
+ this.entries.clear();
68
208
  editor.setInlayHints([]);
69
209
  });
70
210
  }
211
+ /* The key that follows a place is down or up; it comes from the window, as the editor only says where the pointer is. */
212
+ setModHeld(held) {
213
+ if (held !== this.modHeld) {
214
+ this.modHeld = held;
215
+ this.updateLink();
216
+ }
217
+ }
218
+ /* The tooltip of a hint that sits at a position, as a card; false when there is none. This is how the keyboard reaches it. */
219
+ async showTooltipAt(position) {
220
+ const found = [...this.entries].find(([, entry]) => entry.epoch === this.epoch && sameSpot(entry.hint.position, position));
221
+ if (found === undefined) {
222
+ return false;
223
+ }
224
+ return this.showTooltip({ id: found[0], part: null }, this.request, false);
225
+ }
226
+ install(hints, epoch) {
227
+ this.generation++;
228
+ this.entries.clear();
229
+ this.shown = hints.map((hint, index) => {
230
+ const id = `hint-${this.generation}-${index}`;
231
+ this.entries.set(id, { id, hint, epoch, resolution: null, answered: false });
232
+ return editorHintOf(hint, id);
233
+ });
234
+ this.language.editor.setInlayHints(this.shown);
235
+ this.target = null;
236
+ this.updateLink();
237
+ }
238
+ hovered(target) {
239
+ this.target = target;
240
+ this.timers.clear(this.showTimer);
241
+ const request = ++this.request;
242
+ this.updateLink();
243
+ if (target === null) {
244
+ if (this.cardOpen()) {
245
+ this.language.hover.release();
246
+ }
247
+ return;
248
+ }
249
+ this.showTimer = this.timers.set(() => void this.showTooltip(target, request), SHOW_DELAY_MS);
250
+ // A press has to know what the hint offers when it comes, so the answer is asked for as the pointer arrives.
251
+ const entry = this.entries.get(target.id);
252
+ if (entry !== undefined && this.resolvable(entry)) {
253
+ void this.resolve(entry).then(() => this.updateLink());
254
+ }
255
+ }
256
+ cancelHover() {
257
+ this.timers.clear(this.showTimer);
258
+ this.request++;
259
+ this.target = null;
260
+ this.updateLink();
261
+ if (this.cardOpen()) {
262
+ this.language.hover.hide();
263
+ }
264
+ }
265
+ /* Whether the card up is a tooltip, which is the only kind that has no footer. */
266
+ cardOpen() {
267
+ return this.language.popups.getState().hover?.info?.plain === true;
268
+ }
269
+ /* The card for a hint or part, once the server has said what it can. Says whether it opened. */
270
+ async showTooltip(target, request, pointer = true) {
271
+ const entry = this.entries.get(target.id);
272
+ if (entry === undefined) {
273
+ return false;
274
+ }
275
+ if (lacksInfo(entry.hint, target.part)) {
276
+ await this.resolve(entry);
277
+ this.updateLink();
278
+ }
279
+ if ((pointer && request !== this.request) || entry.epoch !== this.epoch) {
280
+ return false;
281
+ }
282
+ const text = tooltipOf(entry.hint, target.part);
283
+ if (text === null) {
284
+ if (this.cardOpen()) {
285
+ this.language.hover.hide();
286
+ }
287
+ return false;
288
+ }
289
+ this.language.hover.showText(entry.hint.position, text);
290
+ return true;
291
+ }
292
+ /* Asks the server for more of a hint, once; what it says is kept only while the text is the one the hint was made for. */
293
+ resolve(entry) {
294
+ if (entry.resolution !== null) {
295
+ return entry.resolution;
296
+ }
297
+ const { service } = this.language.project;
298
+ if (entry.epoch !== this.epoch || !service.supports(RESOLVE_METHOD, this.language.uri)) {
299
+ return Promise.resolve(entry.hint);
300
+ }
301
+ entry.resolution = service
302
+ .resolveInlayHint(this.language.uri, entry.hint)
303
+ .then((resolved) => {
304
+ if (entry.epoch === this.epoch) {
305
+ entry.hint = { ...entry.hint, ...resolved, position: entry.hint.position };
306
+ }
307
+ return entry.hint;
308
+ })
309
+ .catch(() => entry.hint)
310
+ .finally(() => {
311
+ entry.answered = true;
312
+ });
313
+ return entry.resolution;
314
+ }
315
+ /* Whether asking the server could still change what the hint offers. */
316
+ resolvable(entry) {
317
+ return !entry.answered && entry.epoch === this.epoch && this.language.project.service.supports(RESOLVE_METHOD, this.language.uri);
318
+ }
319
+ /* Underlines the part under the pointer when a press would do something with it: a command always, a place while the key is down. */
320
+ updateLink() {
321
+ const { target } = this;
322
+ const entry = target === null ? undefined : this.entries.get(target.id);
323
+ const part = entry === undefined || target === null || entry.epoch !== this.epoch ? null : partOf(entry.hint, target.part);
324
+ const link = part !== null && (part.command !== undefined || (this.modHeld && part.location !== undefined));
325
+ this.language.editor.setInlayLink?.(link ? target : null);
326
+ }
327
+ pressed(click) {
328
+ const entry = this.entries.get(click.id);
329
+ if (entry === undefined || entry.epoch !== this.epoch) {
330
+ return false;
331
+ }
332
+ if (click.touch) {
333
+ this.timers.clear(this.showTimer);
334
+ const request = ++this.request;
335
+ const worth = tooltipOf(entry.hint, click.part) !== null || this.resolvable(entry);
336
+ if (worth) {
337
+ void this.showTooltip({ id: click.id, part: click.part }, request);
338
+ }
339
+ return worth;
340
+ }
341
+ if (click.alt || click.shift) {
342
+ return false;
343
+ }
344
+ this.timers.clear(this.commandTimer);
345
+ const press = ++this.pressCount;
346
+ if (this.resolvable(entry) && (click.clicks === 2 || actionOf(entry.hint, click) !== null)) {
347
+ // The server may still add edits, which would make this the first press of a double click.
348
+ void this.resolve(entry).then(() => {
349
+ if (press === this.pressCount) {
350
+ this.press(entry, click);
351
+ }
352
+ });
353
+ return true;
354
+ }
355
+ return this.press(entry, click);
356
+ }
357
+ press(entry, click) {
358
+ this.timers.clear(this.commandTimer);
359
+ const action = actionOf(entry.hint, click);
360
+ const insertable = entry.hint.textEdits !== undefined && entry.hint.textEdits.length > 0;
361
+ if (action?.kind === 'run' && insertable) {
362
+ // A double click inserts the edits, so the command of its first press waits to see whether a second one comes.
363
+ const epoch = this.epoch;
364
+ this.commandTimer = this.timers.set(() => {
365
+ if (epoch === this.epoch) {
366
+ void this.perform(entry, action);
367
+ }
368
+ }, DOUBLE_PRESS_MS);
369
+ return true;
370
+ }
371
+ if (action !== null) {
372
+ void this.perform(entry, action);
373
+ return true;
374
+ }
375
+ // The first press of a double click must not move the caret either, or the second would land beside the hint.
376
+ return click.clicks === 1 && !click.mod && insertable;
377
+ }
378
+ async perform(entry, action) {
379
+ const { editor, project, uri } = this.language;
380
+ try {
381
+ if (action.kind === 'go') {
382
+ this.cancelHover();
383
+ this.language.goTo(action.location);
384
+ }
385
+ else if (action.kind === 'run') {
386
+ await project.service.executeCommand(uri, action.command);
387
+ }
388
+ else if (entry.epoch === this.epoch && editsFit(editor, action.edits)) {
389
+ this.cancelHover();
390
+ const applied = editor.applyEdits(action.edits.map((edit) => ({ range: edit.range, text: edit.newText })));
391
+ if (applied) {
392
+ this.shown = this.shown.filter((hint) => hint.id !== entry.id);
393
+ editor.setInlayHints(this.shown);
394
+ }
395
+ }
396
+ }
397
+ catch (error) {
398
+ this.language.project.host.notify?.({
399
+ id: TOAST_ID,
400
+ kind: 'error',
401
+ title: project.i18n.t('editor:language.inlay.failed', { message: messageOf(error) })
402
+ });
403
+ }
404
+ }
405
+ }
406
+ function sameSpot(left, right) {
407
+ return left.line === right.line && left.character === right.character;
71
408
  }