@singapore-editor/lsp-plugin 0.1.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 (185) hide show
  1. package/README.md +103 -0
  2. package/dist/bufferSync.d.ts +12 -0
  3. package/dist/bufferSync.d.ts.map +1 -0
  4. package/dist/bufferSync.js +35 -0
  5. package/dist/bufferSync.js.map +1 -0
  6. package/dist/codeActions.d.ts +83 -0
  7. package/dist/codeActions.d.ts.map +1 -0
  8. package/dist/codeActions.js +294 -0
  9. package/dist/codeActions.js.map +1 -0
  10. package/dist/colorAlpha.d.ts +4 -0
  11. package/dist/colorAlpha.d.ts.map +1 -0
  12. package/dist/colorAlpha.js +28 -0
  13. package/dist/colorAlpha.js.map +1 -0
  14. package/dist/completion.d.ts +124 -0
  15. package/dist/completion.d.ts.map +1 -0
  16. package/dist/completion.js +609 -0
  17. package/dist/completion.js.map +1 -0
  18. package/dist/completionCommit.d.ts +19 -0
  19. package/dist/completionCommit.d.ts.map +1 -0
  20. package/dist/completionCommit.js +57 -0
  21. package/dist/completionCommit.js.map +1 -0
  22. package/dist/completionController.d.ts +89 -0
  23. package/dist/completionController.d.ts.map +1 -0
  24. package/dist/completionController.js +376 -0
  25. package/dist/completionController.js.map +1 -0
  26. package/dist/completionProviders.d.ts +50 -0
  27. package/dist/completionProviders.d.ts.map +1 -0
  28. package/dist/completionProviders.js +64 -0
  29. package/dist/completionProviders.js.map +1 -0
  30. package/dist/connectionContext.d.ts +7 -0
  31. package/dist/connectionContext.d.ts.map +1 -0
  32. package/dist/definitionLinkController.d.ts +54 -0
  33. package/dist/definitionLinkController.d.ts.map +1 -0
  34. package/dist/definitionLinkController.js +221 -0
  35. package/dist/definitionLinkController.js.map +1 -0
  36. package/dist/definitionNavigation.d.ts +76 -0
  37. package/dist/definitionNavigation.d.ts.map +1 -0
  38. package/dist/definitionNavigation.js +143 -0
  39. package/dist/definitionNavigation.js.map +1 -0
  40. package/dist/diagnosticNotes.d.ts +7 -0
  41. package/dist/diagnosticNotes.d.ts.map +1 -0
  42. package/dist/diagnosticNotes.js +37 -0
  43. package/dist/diagnosticNotes.js.map +1 -0
  44. package/dist/diagnosticProjection.d.ts +31 -0
  45. package/dist/diagnosticProjection.d.ts.map +1 -0
  46. package/dist/diagnosticProjection.js +79 -0
  47. package/dist/diagnosticProjection.js.map +1 -0
  48. package/dist/diagnostics.d.ts +16 -0
  49. package/dist/diagnostics.d.ts.map +1 -0
  50. package/dist/diagnostics.js +94 -0
  51. package/dist/diagnostics.js.map +1 -0
  52. package/dist/diagnosticsPresenter.d.ts +64 -0
  53. package/dist/diagnosticsPresenter.d.ts.map +1 -0
  54. package/dist/diagnosticsPresenter.js +305 -0
  55. package/dist/diagnosticsPresenter.js.map +1 -0
  56. package/dist/document.d.ts +69 -0
  57. package/dist/document.d.ts.map +1 -0
  58. package/dist/document.js +190 -0
  59. package/dist/document.js.map +1 -0
  60. package/dist/documentHighlightController.d.ts +34 -0
  61. package/dist/documentHighlightController.d.ts.map +1 -0
  62. package/dist/documentHighlightController.js +101 -0
  63. package/dist/documentHighlightController.js.map +1 -0
  64. package/dist/documentSnapshot.d.ts +9 -0
  65. package/dist/documentSnapshot.d.ts.map +1 -0
  66. package/dist/documentSnapshot.js +31 -0
  67. package/dist/documentSnapshot.js.map +1 -0
  68. package/dist/documentSync.d.ts +53 -0
  69. package/dist/documentSync.d.ts.map +1 -0
  70. package/dist/documentSync.js +334 -0
  71. package/dist/documentSync.js.map +1 -0
  72. package/dist/documentSyncController.d.ts +26 -0
  73. package/dist/documentSyncController.d.ts.map +1 -0
  74. package/dist/documentSyncController.js +24 -0
  75. package/dist/documentSyncController.js.map +1 -0
  76. package/dist/formatOnType.d.ts +43 -0
  77. package/dist/formatOnType.d.ts.map +1 -0
  78. package/dist/formatOnType.js +198 -0
  79. package/dist/formatOnType.js.map +1 -0
  80. package/dist/formatting.d.ts +18 -0
  81. package/dist/formatting.d.ts.map +1 -0
  82. package/dist/formatting.js +52 -0
  83. package/dist/formatting.js.map +1 -0
  84. package/dist/fuzzyMatch.d.ts +22 -0
  85. package/dist/fuzzyMatch.d.ts.map +1 -0
  86. package/dist/fuzzyMatch.js +216 -0
  87. package/dist/fuzzyMatch.js.map +1 -0
  88. package/dist/hoverParticipant.d.ts +24 -0
  89. package/dist/hoverParticipant.d.ts.map +1 -0
  90. package/dist/hoverParticipant.js +156 -0
  91. package/dist/hoverParticipant.js.map +1 -0
  92. package/dist/index.d.ts +18 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +17 -0
  95. package/dist/keyCommands.d.ts +19 -0
  96. package/dist/keyCommands.d.ts.map +1 -0
  97. package/dist/keyCommands.js +21 -0
  98. package/dist/keyCommands.js.map +1 -0
  99. package/dist/lane.d.ts +28 -0
  100. package/dist/lane.d.ts.map +1 -0
  101. package/dist/lane.js +138 -0
  102. package/dist/lane.js.map +1 -0
  103. package/dist/lspConnection.d.ts +94 -0
  104. package/dist/lspConnection.d.ts.map +1 -0
  105. package/dist/lspConnection.js +195 -0
  106. package/dist/lspConnection.js.map +1 -0
  107. package/dist/lspConnectionPool.d.ts +34 -0
  108. package/dist/lspConnectionPool.d.ts.map +1 -0
  109. package/dist/lspConnectionPool.js +255 -0
  110. package/dist/lspConnectionPool.js.map +1 -0
  111. package/dist/minimalEdits.d.ts +21 -0
  112. package/dist/minimalEdits.d.ts.map +1 -0
  113. package/dist/minimalEdits.js +181 -0
  114. package/dist/minimalEdits.js.map +1 -0
  115. package/dist/paths.d.ts +6 -0
  116. package/dist/paths.d.ts.map +1 -0
  117. package/dist/paths.js +38 -0
  118. package/dist/paths.js.map +1 -0
  119. package/dist/plugin.d.ts +100 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +837 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/plugin.styles.d.ts +31 -0
  124. package/dist/plugin.styles.d.ts.map +1 -0
  125. package/dist/plugin.styles.js +102 -0
  126. package/dist/plugin.styles.js.map +1 -0
  127. package/dist/pluginTypes.d.ts +41 -0
  128. package/dist/pluginTypes.d.ts.map +1 -0
  129. package/dist/pluginTypes.js +0 -0
  130. package/dist/pullDiagnostics.d.ts +30 -0
  131. package/dist/pullDiagnostics.d.ts.map +1 -0
  132. package/dist/pullDiagnostics.js +94 -0
  133. package/dist/pullDiagnostics.js.map +1 -0
  134. package/dist/renameWidget.d.ts +30 -0
  135. package/dist/renameWidget.d.ts.map +1 -0
  136. package/dist/renameWidget.js +117 -0
  137. package/dist/renameWidget.js.map +1 -0
  138. package/dist/semanticTokenDecoder.d.ts +81 -0
  139. package/dist/semanticTokenDecoder.d.ts.map +1 -0
  140. package/dist/semanticTokenDecoder.js +132 -0
  141. package/dist/semanticTokenDecoder.js.map +1 -0
  142. package/dist/semanticTokens.d.ts +47 -0
  143. package/dist/semanticTokens.d.ts.map +1 -0
  144. package/dist/semanticTokens.js +68 -0
  145. package/dist/semanticTokens.js.map +1 -0
  146. package/dist/serverSet.d.ts +69 -0
  147. package/dist/serverSet.d.ts.map +1 -0
  148. package/dist/serverSet.js +331 -0
  149. package/dist/serverSet.js.map +1 -0
  150. package/dist/signatureHelp.d.ts +40 -0
  151. package/dist/signatureHelp.d.ts.map +1 -0
  152. package/dist/signatureHelp.js +108 -0
  153. package/dist/signatureHelp.js.map +1 -0
  154. package/dist/signatureHelpController.d.ts +52 -0
  155. package/dist/signatureHelpController.d.ts.map +1 -0
  156. package/dist/signatureHelpController.js +160 -0
  157. package/dist/signatureHelpController.js.map +1 -0
  158. package/dist/sourceText.d.ts +22 -0
  159. package/dist/sourceText.d.ts.map +1 -0
  160. package/dist/sourceText.js +70 -0
  161. package/dist/sourceText.js.map +1 -0
  162. package/dist/types.d.ts +238 -0
  163. package/dist/types.d.ts.map +1 -0
  164. package/dist/types.js +17 -0
  165. package/dist/types.js.map +1 -0
  166. package/dist/viewDocumentSnapshot.d.ts +5 -0
  167. package/dist/viewDocumentSnapshot.d.ts.map +1 -0
  168. package/dist/viewDocumentSnapshot.js +12 -0
  169. package/dist/viewDocumentSnapshot.js.map +1 -0
  170. package/dist/websocket.d.ts +6 -0
  171. package/dist/websocket.d.ts.map +1 -0
  172. package/dist/websocket.js +4 -0
  173. package/dist/workspaceEdit.d.ts +66 -0
  174. package/dist/workspaceEdit.d.ts.map +1 -0
  175. package/dist/workspaceEdit.js +276 -0
  176. package/dist/workspaceEdit.js.map +1 -0
  177. package/dist/workspaceEditProvenance.d.ts +5 -0
  178. package/dist/workspaceEditProvenance.d.ts.map +1 -0
  179. package/dist/workspaceEditProvenance.js +16 -0
  180. package/dist/workspaceEditProvenance.js.map +1 -0
  181. package/dist/workspaceTextEdits.d.ts +60 -0
  182. package/dist/workspaceTextEdits.d.ts.map +1 -0
  183. package/dist/workspaceTextEdits.js +395 -0
  184. package/dist/workspaceTextEdits.js.map +1 -0
  185. package/package.json +118 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.js","names":[],"sources":["../src/completion.ts"],"sourcesContent":["import type { SelectionAffinity, TextEdit } from '@singapore-editor/core/document'\nimport type {\n EditorContributionChange,\n EditorEditContributionContext,\n EditorSelectionRange,\n} from '@singapore-editor/core/extensions'\nimport { createEditorCapabilityToken } from '@singapore-editor/core/extensions'\nimport { lspPositionToOffsetInSnapshot, type LspTextDocumentSnapshot } from '@singapore-editor/lsp'\nimport type * as lsp from 'vscode-languageserver-protocol'\nimport {\n parseSnippet,\n snippetInitialSelection,\n type ParsedSnippet,\n type SnippetRange,\n} from '@singapore-editor/core/extensions'\n\nimport { createAnchoredSurface } from '@singapore-editor/plugin-ui/anchored-surface'\nimport { rangeAroundOffset, textReadSnapshotOf } from './sourceText'\nimport { fuzzyMatch, looseFuzzyMatch, type FuzzyMatch } from './fuzzyMatch'\n\nexport const LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE_ID = 'editor.lsp-plugin.completion-edit'\n\nexport const COMPLETION_REQUEST_DEBOUNCE_MS = 80\n\nexport type LanguageServerCompletionEditFeature = {\n applyCompletion(application: LanguageServerCompletionApplication): boolean\n}\n\nexport const LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE =\n createEditorCapabilityToken<LanguageServerCompletionEditFeature>(\n LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE_ID,\n )\n\nexport type LanguageServerSnippetMirror = {\n readonly start: number\n readonly end: number\n /** Renders this copy from the stop's text; without one the copy holds that text verbatim. */\n readonly transform?: (value: string) => string\n}\n\nexport type LanguageServerSnippetStop = {\n readonly start: number\n readonly end: number\n readonly mirrors?: readonly LanguageServerSnippetMirror[]\n}\n\nexport type LanguageServerCompletionApplication = {\n readonly edits: readonly TextEdit[]\n readonly selection: EditorSelectionRange\n /**\n * Tab stops to cycle with Tab, each with the copies of it the snippet writes elsewhere, when the\n * accepted item was a snippet the host has something to track.\n */\n readonly snippetStops?: readonly LanguageServerSnippetStop[]\n}\n\n/** `3` is the kind reserved for asking a server again about a list it marked incomplete. */\nexport type LanguageServerCompletionTrigger = {\n readonly triggerKind: 1 | 2 | 3\n readonly triggerCharacter?: string\n}\n\nexport type CompletionWidgetShowOptions = {\n readonly anchor: DOMRect\n readonly items: readonly lsp.CompletionItem[]\n readonly selectedIndex?: number\n}\n\nexport type CompletionWidgetController = {\n show(options: CompletionWidgetShowOptions): void\n /** Moves a list already on screen to a new anchor, without rebuilding its rows. */\n reanchor(anchor: DOMRect): void\n hide(): void\n isVisible(): boolean\n containsTarget(target: EventTarget | null): boolean\n moveSelection(delta: number): void\n selectedItem(): lsp.CompletionItem | null\n dispose(): void\n}\n\nexport type CompletionWidgetOptions = {\n readonly document: Document\n readonly themeSource: HTMLElement\n readonly classNamespace?: string\n onSelect(index: number): void\n}\n\ntype CompletionWidgetClassNames = {\n readonly root: string\n readonly item: string\n readonly match: string\n}\n\nconst COMPLETION_WIDGET_GAP_PX = 4\nconst COMPLETION_WIDGET_WIDTH_PX = 320\nconst COMPLETION_WIDGET_MAX_HEIGHT_PX = 280\nconst COMPLETION_WIDGET_MARGIN_PX = 12\nconst COMPLETION_WIDGET_CLASS_NAMESPACE = 'lsp-plugin'\nconst COMPLETION_TRIGGER_CHARACTERS = new Set(['.', '\"', \"'\", '`', '/', '@', '<', '#'])\nconst COMPLETION_THEME_VARIABLES = [\n '--editor-background',\n '--editor-popup-background',\n '--editor-foreground',\n '--editor-caret-color',\n] as const\n\nexport function createCompletionWidgetController(\n options: CompletionWidgetOptions,\n): CompletionWidgetController {\n const classNames = completionWidgetClassNames(options.classNamespace)\n const element = createCompletionWidgetElement(options.document, classNames)\n options.document.body.append(element)\n const surface = createAnchoredSurface({\n element,\n anchorClassName: `${classNames.root}-anchor`,\n // Below the word, where the next thing typed continues it; above only when there is no room.\n preferredPlacement: 'bottom',\n alignment: 'start',\n gapPx: COMPLETION_WIDGET_GAP_PX,\n viewportMarginPx: COMPLETION_WIDGET_MARGIN_PX,\n maxHeightPx: COMPLETION_WIDGET_MAX_HEIGHT_PX,\n })\n\n let items: readonly lsp.CompletionItem[] = []\n let selectedIndex = 0\n let disposed = false\n\n const render = (): void => {\n element.replaceChildren()\n for (let index = 0; index < items.length; index += 1) {\n element.append(completionRowElement(element.ownerDocument, items[index]!, index, classNames))\n }\n syncSelectedRows(element, selectedIndex, classNames)\n }\n\n const show = (showOptions: CompletionWidgetShowOptions): void => {\n items = showOptions.items\n selectedIndex = clampIndex(showOptions.selectedIndex ?? 0, items.length)\n syncEditorThemeVariables(element, options.themeSource)\n render()\n element.hidden = items.length === 0\n // Placed once the rows are in and the list is visible: a hidden element measures as nothing,\n // and how tall it is decides whether it fits below the word.\n if (element.hidden) surface.release()\n else surface.place(showOptions.anchor)\n }\n\n const reanchor = (anchor: DOMRect): void => {\n if (element.hidden) return\n\n surface.place(anchor)\n }\n\n const hide = (): void => {\n element.hidden = true\n surface.release()\n element.replaceChildren()\n items = []\n selectedIndex = 0\n }\n\n const moveSelection = (delta: number): void => {\n if (items.length === 0) return\n\n selectedIndex = wrapIndex(selectedIndex + delta, items.length)\n syncSelectedRows(element, selectedIndex, classNames)\n }\n\n const selectedItem = (): lsp.CompletionItem | null => items[selectedIndex] ?? null\n\n const handlePointerDown = (event: PointerEvent): void => {\n const row = completionRowTarget(event.target, classNames)\n if (!row) return\n\n event.preventDefault()\n event.stopPropagation()\n selectedIndex = rowIndex(row)\n syncSelectedRows(element, selectedIndex, classNames)\n options.onSelect(selectedIndex)\n }\n\n element.addEventListener('pointerdown', handlePointerDown)\n\n const dispose = (): void => {\n if (disposed) return\n\n disposed = true\n element.removeEventListener('pointerdown', handlePointerDown)\n surface.dispose()\n element.remove()\n }\n\n return {\n show,\n reanchor,\n hide,\n isVisible: () => !element.hidden,\n containsTarget: (target) => target instanceof Node && element.contains(target),\n moveSelection,\n selectedItem,\n dispose,\n }\n}\n\n/**\n * A server's answer, with the one flag on it that outlives the request.\n *\n * `isIncomplete` is the server saying it truncated its answer for the word it was asked about — the\n * large member sets and auto-import lists are always sent this way — so a list that stays on screen\n * while the user keeps typing has to be asked again rather than narrowed.\n */\nexport type CompletionListResult = {\n readonly items: readonly lsp.CompletionItem[]\n readonly isIncomplete: boolean\n}\n\nexport function completionListResult(\n result: lsp.CompletionList | readonly lsp.CompletionItem[] | null,\n): CompletionListResult {\n if (!result) return { items: [], isIncomplete: false }\n if (isCompletionList(result))\n return { items: result.items ?? [], isIncomplete: result.isIncomplete === true }\n return { items: result, isIncomplete: false }\n}\n\nexport function completionTriggerFromChange(\n change: EditorContributionChange | null,\n): LanguageServerCompletionTrigger | null {\n if (!change || change.kind !== 'edit') return null\n if (change.edits.length !== 1) return null\n\n const edit = change.edits[0]\n if (!edit) return null\n\n const character = typedCharacter(edit)\n if (!character) return null\n if (COMPLETION_TRIGGER_CHARACTERS.has(character)) {\n return { triggerKind: 2, triggerCharacter: character }\n }\n if (isIdentifierCharacter(character)) return { triggerKind: 1 }\n return null\n}\n\n/**\n * The character the user typed, out of an edit that may have written more than it.\n *\n * A delimiter that closes itself arrives as the pair in a single insertion with the caret left\n * between the halves — which is exactly the keystroke a path or a member list is wanted for, so\n * reading only the one-character edits would leave the quote triggers unreachable while the editor\n * completes them. An insertion of one trigger character twice over is the whole of that shape;\n * anything else two characters wide is text arriving from somewhere other than the keyboard.\n */\nfunction typedCharacter(edit: TextEdit): string | null {\n if (edit.text.length === 1) return edit.text\n if (edit.from !== edit.to || edit.text.length !== 2) return null\n\n const opener = edit.text[0] ?? ''\n if (opener !== edit.text[1]) return null\n\n return COMPLETION_TRIGGER_CHARACTERS.has(opener) ? opener : null\n}\n\n/** The text a request went out with, which is the frame every range on an item is read in. */\nexport type CompletionApplicationDocument = {\n readonly document: LspTextDocumentSnapshot\n readonly offset: number\n}\n\n/**\n * The document an accepted item is applied against.\n *\n * `document` and `offset` are what the request went out with, and the item's ranges are positions\n * in that text. `caretOffset` and `caretAffinity` are where the caret has reached since — rarely the\n * same position, because the widget stays up while the user keeps typing.\n */\nexport type CompletionApplicationRequest = CompletionApplicationDocument & {\n readonly caretOffset: number\n readonly caretAffinity: SelectionAffinity\n}\n\nexport function completionApplication(\n request: CompletionApplicationRequest,\n item: lsp.CompletionItem,\n): LanguageServerCompletionApplication | null {\n const primary = completionPrimaryEdit(request, item)\n if (!primary) return null\n\n const additional = additionalCompletionEdits(request, item.additionalTextEdits ?? [])\n const edits = [primary.edit, ...additional]\n const head = completionSelectionHead(primary.edit, additional)\n const snippet = primary.snippet\n if (!snippet) {\n return { edits, selection: { anchor: head, head, affinity: request.caretAffinity } }\n }\n\n // A snippet's first placeholder is selected so the next keystroke replaces it; without a stop the\n // caret sits after the insertion exactly as a plain completion leaves it.\n const insertionOffset = head - primary.edit.text.length\n const first = snippetInitialSelection(snippet, insertionOffset)\n return {\n edits,\n selection: { anchor: first.start, head: first.end, affinity: request.caretAffinity },\n snippetStops: snippetStopRanges(snippet, insertionOffset),\n }\n}\n\n/**\n * Whether an item could be applied at all against the text it was answered for.\n *\n * The ranges an item carries are positions in that text, and one that spans lines or that the caret\n * was never inside describes a document this client did not ask about. Nothing downstream can rescue\n * such an item, so a row for it would be one the reader can press Enter on and get nothing for.\n */\nexport function completionItemApplies(\n request: CompletionApplicationDocument,\n item: lsp.CompletionItem,\n): boolean {\n const textEdit = completionTextEdit(item)\n if (!textEdit) return true\n\n return completionEditRange(request, textEdit.range) !== null\n}\n\nexport function completionAnchorRange(\n length: number,\n offset: number,\n): { readonly start: number; readonly end: number } {\n if (length === 0) return { start: 0, end: 0 }\n\n const end = Math.max(1, Math.min(offset, length))\n return { start: end - 1, end }\n}\n\nexport function createCompletionEditFeature(\n context: EditorEditContributionContext,\n timingName: string,\n): LanguageServerCompletionEditFeature {\n return {\n applyCompletion(application: LanguageServerCompletionApplication): boolean {\n if (!context.hasDocument()) return false\n\n context.applyEdits(application.edits, timingName, application.selection)\n // Started after the edit so the ranges refer to the text now in the document. A host without\n // the hook simply leaves the caret on the first placeholder.\n if (application.snippetStops) context.startSnippetSession(application.snippetStops)\n context.focusEditor()\n return true\n },\n }\n}\n\nfunction createCompletionWidgetElement(\n document: Document,\n classNames: CompletionWidgetClassNames,\n): HTMLDivElement {\n const element = document.createElement('div')\n element.className = classNames.root\n element.hidden = true\n element.setAttribute('role', 'listbox')\n Object.assign(element.style, {\n zIndex: '1001',\n width: `${COMPLETION_WIDGET_WIDTH_PX}px`,\n maxWidth: `calc(100vw - ${COMPLETION_WIDGET_MARGIN_PX * 2}px)`,\n maxHeight: `${COMPLETION_WIDGET_MAX_HEIGHT_PX}px`,\n overflowX: 'hidden',\n overflowY: 'auto',\n padding: '3px',\n border: '1px solid color-mix(in srgb, var(--editor-foreground, #d4d4d8) 22%, transparent)',\n borderRadius: '6px',\n boxSizing: 'border-box',\n background:\n 'var(--editor-popup-background, color-mix(in srgb, var(--editor-background, #18181b) 96%, var(--editor-foreground, #e4e4e7) 4%))',\n color: 'var(--editor-foreground, #e4e4e7)',\n boxShadow: '0 12px 30px color-mix(in srgb, var(--editor-background, #000000) 60%, transparent)',\n font: '12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n userSelect: 'none',\n scrollbarGutter: 'stable',\n })\n return element\n}\n\nfunction completionRowElement(\n document: Document,\n item: lsp.CompletionItem,\n index: number,\n classNames: CompletionWidgetClassNames,\n): HTMLDivElement {\n const row = document.createElement('div')\n row.className = classNames.item\n row.dataset.index = String(index)\n row.setAttribute('role', 'option')\n Object.assign(row.style, {\n display: 'grid',\n gridTemplateColumns: '44px minmax(0, 1fr) auto',\n alignItems: 'center',\n gap: '8px',\n minHeight: '24px',\n padding: '2px 7px',\n borderRadius: '4px',\n cursor: 'default',\n whiteSpace: 'nowrap',\n })\n row.append(\n completionKindElement(document, item.kind),\n completionLabelElement(document, item, classNames),\n completionDetailElement(document, item),\n )\n return row\n}\n\nfunction completionKindElement(\n document: Document,\n kind: lsp.CompletionItemKind | undefined,\n): HTMLSpanElement {\n const element = document.createElement('span')\n element.textContent = completionKindLabel(kind)\n Object.assign(element.style, {\n minWidth: '0',\n overflow: 'hidden',\n color: 'color-mix(in srgb, var(--editor-foreground, #e4e4e7) 62%, transparent)',\n fontSize: '10px',\n textTransform: 'uppercase',\n })\n return element\n}\n\nfunction completionLabelElement(\n document: Document,\n item: lsp.CompletionItem,\n classNames: CompletionWidgetClassNames,\n): HTMLSpanElement {\n const element = document.createElement('span')\n Object.assign(element.style, {\n minWidth: '0',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n })\n appendCompletionLabel(element, item.label, completionLabelMatches.get(item) ?? [], classNames)\n const detail = item.labelDetails?.detail\n if (detail) element.append(detail)\n return element\n}\n\n/**\n * The label, with each run of characters the filter matched marked up.\n *\n * A list that reorders itself as the user types owes them the reason it did: with nothing marked, an\n * entry that outranks the one above it looks arbitrary rather than earned.\n */\nfunction appendCompletionLabel(\n element: HTMLElement,\n label: string,\n positions: readonly number[],\n classNames: CompletionWidgetClassNames,\n): void {\n let plainStart = 0\n let cursor = 0\n while (cursor < positions.length) {\n const start = positions[cursor] ?? 0\n let end = start + 1\n while (positions[cursor + 1] === end) {\n end += 1\n cursor += 1\n }\n cursor += 1\n\n if (start > plainStart) element.append(label.slice(plainStart, start))\n element.append(\n completionMatchElement(element.ownerDocument, label.slice(start, end), classNames),\n )\n plainStart = end\n }\n if (plainStart < label.length) element.append(label.slice(plainStart))\n}\n\nfunction completionMatchElement(\n document: Document,\n text: string,\n classNames: CompletionWidgetClassNames,\n): HTMLSpanElement {\n const element = document.createElement('span')\n element.className = classNames.match\n element.textContent = text\n Object.assign(element.style, {\n // The colour the caret is drawn in is the one the theme already means \"you are here\" by.\n color:\n 'color-mix(in srgb, var(--editor-caret-color, #60a5fa) 80%, var(--editor-foreground, #e4e4e7))',\n fontWeight: '600',\n })\n return element\n}\n\nfunction completionDetailElement(document: Document, item: lsp.CompletionItem): HTMLSpanElement {\n const element = document.createElement('span')\n element.textContent = item.labelDetails?.description ?? item.detail ?? ''\n Object.assign(element.style, {\n minWidth: '0',\n maxWidth: '140px',\n overflow: 'hidden',\n color: 'color-mix(in srgb, var(--editor-foreground, #e4e4e7) 58%, transparent)',\n textOverflow: 'ellipsis',\n })\n return element\n}\n\nfunction syncSelectedRows(\n element: HTMLDivElement,\n selectedIndex: number,\n classNames: CompletionWidgetClassNames,\n): void {\n const rows = element.querySelectorAll<HTMLElement>(`.${classNames.item}`)\n for (const row of rows) {\n const selected = rowIndex(row) === selectedIndex\n syncSelectedRow(row, selected)\n if (selected) scrollRowIntoView(element, row)\n }\n}\n\n/**\n * Keeps the focused row inside the box.\n *\n * A hundred rows are offered in a box a dozen of them fit in, and the widget never takes the focus,\n * so there is no scrollport the browser is moving on anyone's behalf: walking off the bottom edge —\n * or wrapping to the end with a single ArrowUp — would leave a list with no highlight in it while\n * Enter commits a row that was never on screen.\n */\nfunction scrollRowIntoView(element: HTMLElement, row: HTMLElement): void {\n const top = row.offsetTop\n const bottom = top + row.offsetHeight\n if (top < element.scrollTop) element.scrollTop = top\n else if (bottom > element.scrollTop + element.clientHeight) {\n element.scrollTop = bottom - element.clientHeight\n }\n}\n\nfunction syncSelectedRow(row: HTMLElement, selected: boolean): void {\n row.setAttribute('aria-selected', selected ? 'true' : 'false')\n row.style.background = selected\n ? 'color-mix(in srgb, var(--editor-caret-color, #60a5fa) 26%, transparent)'\n : 'transparent'\n}\n\nfunction syncEditorThemeVariables(element: HTMLElement, source: HTMLElement): void {\n const style = source.ownerDocument.defaultView?.getComputedStyle(source)\n if (!style) return\n\n for (const variable of COMPLETION_THEME_VARIABLES) {\n const value = style.getPropertyValue(variable)\n if (value) {\n element.style.setProperty(variable, value)\n continue\n }\n element.style.removeProperty(variable)\n }\n}\n\n/** The item's own insertion, and the snippet it was expanded from when the item carried one. */\ntype CompletionInsertion = {\n readonly edit: TextEdit\n readonly snippet: ParsedSnippet | null\n}\n\nfunction completionPrimaryEdit(\n request: CompletionApplicationRequest,\n item: lsp.CompletionItem,\n): CompletionInsertion | null {\n const textEdit = completionTextEdit(item)\n const range = textEdit\n ? completionEditRange(request, textEdit.range)\n : defaultCompletionReplacementRange(request.document, request.offset)\n if (!range) return null\n\n const source = textEdit ? textEdit.newText : (item.insertText ?? item.label)\n // Expanded against the document it is landing in, and once: the text that goes in and the offsets\n // the stops are reported at have to come from the same expansion or the caret lands off the\n // placeholder it is meant to select.\n const snippet =\n item.insertTextFormat === 2\n ? parseSnippet(source, {\n insertion: { textSnapshot: textReadSnapshotOf(request.document), offset: range.start },\n })\n : null\n\n return {\n snippet,\n edit: {\n from: range.start,\n // Whatever was typed since the request sits between the range's end and the caret, so the end\n // travels with the caret while the start — the word the server matched — stays where it is.\n to: Math.max(range.start, range.end + request.caretOffset - request.offset),\n text: snippet?.text ?? source,\n },\n }\n}\n\nfunction completionTextEdit(item: lsp.CompletionItem): lsp.TextEdit | null {\n const textEdit = item.textEdit\n if (!textEdit) return null\n if ('range' in textEdit) return textEdit\n // Of the two ranges an item may carry, `insert` stops at the caret while `replace` covers the\n // whole word it sits in. Insert would leave the tail of the word behind, which is neither what\n // the no-textEdit path below does nor what accepting mid-word is asking for.\n return { range: textEdit.replace, newText: textEdit.newText }\n}\n\n/**\n * The item's replacement range in offsets, or null when it describes a document we never asked\n * about: a range spanning lines, or one the caret was not inside, leaves nothing for the typed\n * characters to be measured against.\n */\nfunction completionEditRange(\n request: CompletionApplicationDocument,\n range: lsp.Range,\n): { readonly start: number; readonly end: number } | null {\n if (range.start.line !== range.end.line) return null\n\n const start = lspPositionToOffsetInSnapshot(request.document, range.start)\n const end = lspPositionToOffsetInSnapshot(request.document, range.end)\n if (start > request.offset || end < request.offset) return null\n\n return { start, end }\n}\n\nfunction additionalCompletionEdits(\n request: CompletionApplicationRequest,\n edits: readonly lsp.TextEdit[],\n): readonly TextEdit[] {\n return edits.map((edit) => {\n const from = lspPositionToOffsetInSnapshot(request.document, edit.range.start)\n const to = lspPositionToOffsetInSnapshot(request.document, edit.range.end)\n // An import edit above the caret is unmoved; one below it addresses text the typing has already\n // pushed along.\n const shift = from < request.offset ? 0 : request.caretOffset - request.offset\n return { from: from + shift, to: to + shift, text: edit.newText }\n })\n}\n\nfunction completionSelectionHead(primary: TextEdit, additional: readonly TextEdit[]): number {\n let head = primary.from + primary.text.length\n for (const edit of additional) {\n if (edit.to > primary.from) continue\n head += edit.text.length - (edit.to - edit.from)\n }\n return Math.max(0, head)\n}\n\nfunction defaultCompletionReplacementRange(\n document: LspTextDocumentSnapshot,\n offset: number,\n): { readonly start: number; readonly end: number } {\n const clamped = Math.max(0, Math.min(offset, document.textSnapshot.length))\n return rangeAroundOffset(document, clamped, identifierRunAt)\n}\n\nfunction identifierRunAt(text: string, offset: number): { start: number; end: number } {\n let start = offset\n while (start > 0 && isIdentifierCharacter(text[start - 1] ?? '')) start -= 1\n\n let end = offset\n while (end < text.length && isIdentifierCharacter(text[end] ?? '')) end += 1\n return { start, end }\n}\n\n/** Every tab stop in visit order and in document offsets, or undefined when there are none. */\nfunction snippetStopRanges(\n snippet: ParsedSnippet,\n insertionOffset: number,\n): readonly LanguageServerSnippetStop[] | undefined {\n const stops = snippet.stops.flatMap((stop) => {\n // The caret is offered the occurrence it can type into — never a transform's output, which is\n // rendered from another. Every further occurrence goes along as a copy, so a stop the snippet\n // writes twice reads the same in both places while the reader is still typing it.\n const caret = stop.ranges.findIndex((candidate) => !candidate.transform)\n const range = stop.ranges[caret]\n if (!range) return []\n\n const mirrors = stop.ranges.flatMap((mirror, index) =>\n index === caret ? [] : [shiftedSnippetRange(mirror, insertionOffset)],\n )\n const shifted = shiftedSnippetRange(range, insertionOffset)\n return [mirrors.length > 0 ? { ...shifted, mirrors } : shifted]\n })\n\n return stops.length > 0 ? stops : undefined\n}\n\nfunction shiftedSnippetRange(\n range: SnippetRange,\n insertionOffset: number,\n): LanguageServerSnippetMirror {\n const shifted = { end: insertionOffset + range.end, start: insertionOffset + range.start }\n return range.transform ? { ...shifted, transform: range.transform } : shifted\n}\n\nfunction completionRowTarget(\n target: EventTarget | null,\n classNames: CompletionWidgetClassNames,\n): HTMLElement | null {\n if (!(target instanceof Element)) return null\n return target.closest<HTMLElement>(`.${classNames.item}`)\n}\n\nfunction completionWidgetClassNames(\n namespace = COMPLETION_WIDGET_CLASS_NAMESPACE,\n): CompletionWidgetClassNames {\n const root = `editor-${namespace}-completion`\n return {\n root,\n item: `${root}-item`,\n match: `${root}-match`,\n }\n}\n\nfunction rowIndex(row: HTMLElement): number {\n const parsed = Number(row.dataset.index)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nfunction clampIndex(index: number, length: number): number {\n if (length <= 0) return 0\n return Math.min(Math.max(0, index), length - 1)\n}\n\nfunction wrapIndex(index: number, length: number): number {\n if (length <= 0) return 0\n return ((index % length) + length) % length\n}\n\nfunction completionKindLabel(kind: lsp.CompletionItemKind | undefined): string {\n if (kind === 2) return 'meth'\n if (kind === 3) return 'fn'\n if (kind === 5) return 'field'\n if (kind === 6) return 'var'\n if (kind === 7) return 'class'\n if (kind === 8) return 'iface'\n if (kind === 9) return 'mod'\n if (kind === 10) return 'prop'\n if (kind === 13) return 'enum'\n if (kind === 14) return 'key'\n if (kind === 20) return 'member'\n return 'text'\n}\n\nfunction isIdentifierCharacter(value: string): boolean {\n return /^[A-Za-z0-9_$]$/.test(value)\n}\n\nfunction isCompletionList(\n value: lsp.CompletionList | readonly lsp.CompletionItem[],\n): value is lsp.CompletionList {\n return !Array.isArray(value)\n}\n\n/**\n * Whether an accepted item still needs a `completionItem/resolve` round-trip.\n *\n * Servers routinely send a list whose items carry only a label, deferring documentation and — the\n * one that matters — the import edit until resolve. Applying such an item directly is what silently\n * drops auto-imports.\n */\nexport function completionNeedsResolve(\n item: lsp.CompletionItem,\n serverCapabilities: lsp.ServerCapabilities | null | undefined,\n): boolean {\n if (item.additionalTextEdits !== undefined) return false\n\n return Boolean(serverCapabilities?.completionProvider?.resolveProvider)\n}\n\n/**\n * Word being typed at `offset`, which is what the list should be filtered against. The server's\n * answer is for the moment the request was sent; the user has usually typed more since.\n */\nexport function completionPrefix(text: string, offset: number): string {\n let start = Math.max(0, Math.min(offset, text.length))\n while (start > 0 && isIdentifierCharacter(text[start - 1] ?? '')) start -= 1\n\n return text.slice(start, Math.max(0, Math.min(offset, text.length)))\n}\n\n/**\n * Where each ranked item's label matched, kept beside the list instead of inside it.\n *\n * The items in a list are the server's own objects, and one of them goes back to the server on a\n * resolve; the runs to mark are this client's reading of them. The widget is handed the items and\n * nothing else, so this is where it reads that from.\n */\nconst completionLabelMatches = new WeakMap<lsp.CompletionItem, readonly number[]>()\n\n/** What the last filter of a list produced, and the word it was produced from. */\ntype CompletionRanking = {\n readonly prefix: string\n readonly ranked: readonly lsp.CompletionItem[]\n}\n\n/**\n * Each server list's last filter, hung on the list itself.\n *\n * A narrowing is an answer about the one list it was computed from, and that list is held by a\n * single session in a single editor. Here it is out of reach of a second editor filtering its own\n * list, and it is released along with the session holding the list; in a variable of its own it\n * would be neither.\n */\nconst completionRankings = new WeakMap<readonly lsp.CompletionItem[], CompletionRanking>()\n\n/**\n * Filters and orders a completion list against what has been typed.\n *\n * An item earns its place by where the typed characters landed in it, so a word start, a camel-case\n * hump and the far side of a separator all outrank the same letters found in the middle of a word.\n * Ties fall back to the server's own `sortText`, which is how a server expresses relevance it knows\n * and the client does not.\n */\nexport function rankCompletionItems(\n items: readonly lsp.CompletionItem[],\n prefix: string,\n): readonly lsp.CompletionItem[] {\n if (prefix.length === 0) {\n completionRankings.delete(items)\n // The runs read a word that is no longer being typed, and the whole list is about to go back on\n // screen — including the items an earlier, longer word had already dropped.\n for (const item of items) completionLabelMatches.delete(item)\n return items\n }\n\n const scored: { item: lsp.CompletionItem; score: number }[] = []\n for (const item of narrowingSource(items, prefix)) {\n const match = fuzzyMatch(prefix, item.filterText ?? item.label)\n if (!match) continue\n\n completionLabelMatches.set(item, completionLabelPositions(item, prefix, match))\n scored.push({ item, score: match.score })\n }\n\n const ranked = scored\n .sort(\n (left, right) => right.score - left.score || compareCompletionOrder(left.item, right.item),\n )\n .map((entry) => entry.item)\n completionRankings.set(items, { prefix, ranked })\n return ranked\n}\n\n/**\n * The items worth scoring for this word: the ones that survived the shorter word, when this word is\n * that word plus what has been typed since.\n *\n * Typing a character can only narrow, never widen — no item the longer word matches was passed over\n * by the shorter one — so a server's list is scored in full once for the word it answered, and for\n * every keystroke the list stays up after that, only where it is still standing.\n */\nfunction narrowingSource(\n items: readonly lsp.CompletionItem[],\n prefix: string,\n): readonly lsp.CompletionItem[] {\n const previous = completionRankings.get(items)\n if (!previous) return items\n if (prefix.length <= previous.prefix.length || !prefix.startsWith(previous.prefix)) return items\n\n return previous.ranked\n}\n\n/**\n * The label positions to mark for an item, given the match its filter text earned.\n *\n * An item may be filtered by one string and displayed as another, and positions in the string that\n * was matched address nothing in the string on screen. Such a label is matched again on its own\n * terms, and loosely: part of what was typed has no reason to appear in it at all, and marking the\n * part that does beats marking nothing. The rank stays the one the filter text earned.\n */\nfunction completionLabelPositions(\n item: lsp.CompletionItem,\n prefix: string,\n match: FuzzyMatch,\n): readonly number[] {\n const filterText = item.filterText\n if (filterText === undefined) return match.positions\n if (filterText.toLowerCase() === item.label.toLowerCase()) return match.positions\n\n return looseFuzzyMatch(prefix, item.label)?.positions ?? []\n}\n\nfunction compareCompletionOrder(left: lsp.CompletionItem, right: lsp.CompletionItem): number {\n const leftKey = left.sortText ?? left.label\n const rightKey = right.sortText ?? right.label\n return leftKey.localeCompare(rightKey)\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type * as lsp from 'vscode-languageserver-protocol';
2
+ import type { LanguageServerCompletionEditFeature } from './completion';
3
+ /**
4
+ * The character just typed, when the focused item asks to be accepted on it.
5
+ *
6
+ * Read off the focused item at the keystroke rather than kept in a set beside it: an item declares
7
+ * two or three of these, so there is nothing to amortize, and a set that outlives the focus it was
8
+ * built from is a set that accepts an item the user is no longer looking at.
9
+ */
10
+ export declare function completionCommitCharacter(event: KeyboardEvent, item: lsp.CompletionItem | null, enabled: boolean): string | null;
11
+ /**
12
+ * The edit feature with `character` folded into whatever acceptance goes through it.
13
+ *
14
+ * One acceptance is one call and so one undo entry, which is the whole reason the character travels
15
+ * inside it: inserted afterwards it would be an entry of its own, and undoing the completion would
16
+ * leave it stranded on the word it was meant to follow.
17
+ */
18
+ export declare function completionCommitFeature(feature: LanguageServerCompletionEditFeature, character: string | null): LanguageServerCompletionEditFeature;
19
+ //# sourceMappingURL=completionCommit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionCommit.d.ts","sourceRoot":"","sources":["../src/completionCommit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,OAAO,KAAK,EAEV,mCAAmC,EACpC,MAAM,cAAc,CAAA;AAErB;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,GAAG,CAAC,cAAc,GAAG,IAAI,EAC/B,OAAO,EAAE,OAAO,GACf,MAAM,GAAG,IAAI,CAQf;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,mCAAmC,EAC5C,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,mCAAmC,CAOrC"}
@@ -0,0 +1,57 @@
1
+ //#region src/completionCommit.ts
2
+ /**
3
+ * The character just typed, when the focused item asks to be accepted on it.
4
+ *
5
+ * Read off the focused item at the keystroke rather than kept in a set beside it: an item declares
6
+ * two or three of these, so there is nothing to amortize, and a set that outlives the focus it was
7
+ * built from is a set that accepts an item the user is no longer looking at.
8
+ */
9
+ function completionCommitCharacter(event, item, enabled) {
10
+ if (!enabled || !item) return null;
11
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
12
+ if (event.key.length !== 1) return null;
13
+ return item.commitCharacters?.includes(event.key) === true ? event.key : null;
14
+ }
15
+ /**
16
+ * The edit feature with `character` folded into whatever acceptance goes through it.
17
+ *
18
+ * One acceptance is one call and so one undo entry, which is the whole reason the character travels
19
+ * inside it: inserted afterwards it would be an entry of its own, and undoing the completion would
20
+ * leave it stranded on the word it was meant to follow.
21
+ */
22
+ function completionCommitFeature(feature, character) {
23
+ if (character === null) return feature;
24
+ return { applyCompletion: (application) => feature.applyCompletion(completionCommitApplication(application, character)) };
25
+ }
26
+ /**
27
+ * The acceptance with the commit character appended to what the item inserts.
28
+ *
29
+ * The character is why the item was accepted, so it has to end up after the insertion exactly as it
30
+ * was typed — `arr.fil` committed on `(` reads `arr.filter(`, and dropping the character is the one
31
+ * outcome that makes the interaction worse than not having it.
32
+ */
33
+ function completionCommitApplication(application, character) {
34
+ const [insertion, ...additional] = application.edits;
35
+ if (!insertion) return application;
36
+ const edits = [{
37
+ ...insertion,
38
+ text: `${insertion.text}${character}`
39
+ }, ...additional];
40
+ if (application.snippetStops) return {
41
+ ...application,
42
+ edits
43
+ };
44
+ const head = application.selection.head + character.length;
45
+ return {
46
+ edits,
47
+ selection: {
48
+ ...application.selection,
49
+ anchor: head,
50
+ head
51
+ }
52
+ };
53
+ }
54
+ //#endregion
55
+ export { completionCommitCharacter, completionCommitFeature };
56
+
57
+ //# sourceMappingURL=completionCommit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionCommit.js","names":[],"sources":["../src/completionCommit.ts"],"sourcesContent":["import type * as lsp from 'vscode-languageserver-protocol'\n\nimport type {\n LanguageServerCompletionApplication,\n LanguageServerCompletionEditFeature,\n} from './completion'\n\n/**\n * The character just typed, when the focused item asks to be accepted on it.\n *\n * Read off the focused item at the keystroke rather than kept in a set beside it: an item declares\n * two or three of these, so there is nothing to amortize, and a set that outlives the focus it was\n * built from is a set that accepts an item the user is no longer looking at.\n */\nexport function completionCommitCharacter(\n event: KeyboardEvent,\n item: lsp.CompletionItem | null,\n enabled: boolean,\n): string | null {\n if (!enabled || !item) return null\n // A modifier makes the keystroke a command — `Ctrl+.` is a binding, not a typed period — and `key`\n // only names the character typed when it names a single character at all.\n if (event.ctrlKey || event.metaKey || event.altKey) return null\n if (event.key.length !== 1) return null\n\n return item.commitCharacters?.includes(event.key) === true ? event.key : null\n}\n\n/**\n * The edit feature with `character` folded into whatever acceptance goes through it.\n *\n * One acceptance is one call and so one undo entry, which is the whole reason the character travels\n * inside it: inserted afterwards it would be an entry of its own, and undoing the completion would\n * leave it stranded on the word it was meant to follow.\n */\nexport function completionCommitFeature(\n feature: LanguageServerCompletionEditFeature,\n character: string | null,\n): LanguageServerCompletionEditFeature {\n if (character === null) return feature\n\n return {\n applyCompletion: (application) =>\n feature.applyCompletion(completionCommitApplication(application, character)),\n }\n}\n\n/**\n * The acceptance with the commit character appended to what the item inserts.\n *\n * The character is why the item was accepted, so it has to end up after the insertion exactly as it\n * was typed — `arr.fil` committed on `(` reads `arr.filter(`, and dropping the character is the one\n * outcome that makes the interaction worse than not having it.\n */\nfunction completionCommitApplication(\n application: LanguageServerCompletionApplication,\n character: string,\n): LanguageServerCompletionApplication {\n const [insertion, ...additional] = application.edits\n if (!insertion) return application\n\n const edits = [{ ...insertion, text: `${insertion.text}${character}` }, ...additional]\n // A snippet's caret belongs on its first placeholder, and the character lands past the end of the\n // insertion, so every stop measured inside it is still where it was.\n if (application.snippetStops) return { ...application, edits }\n\n const head = application.selection.head + character.length\n return { edits, selection: { ...application.selection, anchor: head, head } }\n}\n"],"mappings":";;;;;;;;AAcA,SAAgB,0BACd,OACA,MACA,SACe;CACf,IAAI,CAAC,WAAW,CAAC,MAAM,OAAO;CAG9B,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,QAAQ,OAAO;CAC3D,IAAI,MAAM,IAAI,WAAW,GAAG,OAAO;CAEnC,OAAO,KAAK,kBAAkB,SAAS,MAAM,GAAG,MAAM,OAAO,MAAM,MAAM;AAC3E;;;;;;;;AASA,SAAgB,wBACd,SACA,WACqC;CACrC,IAAI,cAAc,MAAM,OAAO;CAE/B,OAAO,EACL,kBAAkB,gBAChB,QAAQ,gBAAgB,4BAA4B,aAAa,SAAS,CAAC,EAC/E;AACF;;;;;;;;AASA,SAAS,4BACP,aACA,WACqC;CACrC,MAAM,CAAC,WAAW,GAAG,cAAc,YAAY;CAC/C,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,QAAQ,CAAC;EAAE,GAAG;EAAW,MAAM,GAAG,UAAU,OAAO;CAAY,GAAG,GAAG,UAAU;CAGrF,IAAI,YAAY,cAAc,OAAO;EAAE,GAAG;EAAa;CAAM;CAE7D,MAAM,OAAO,YAAY,UAAU,OAAO,UAAU;CACpD,OAAO;EAAE;EAAO,WAAW;GAAE,GAAG,YAAY;GAAW,QAAQ;GAAM;EAAK;CAAE;AAC9E"}
@@ -0,0 +1,89 @@
1
+ import type { EditorContributionChange, EditorCapabilityToken, EditorViewContributionContext, EditorViewContributionUpdateKind, EditorViewSnapshot } from '@singapore-editor/core/extensions';
2
+ import type { CompletionKeyCommand } from './keyCommands';
3
+ import { type LanguageServerCompletionEditFeature } from './completion';
4
+ import type { EditorCompletionSourceSet } from './completionProviders';
5
+ import type { ActiveDocument } from './pluginTypes';
6
+ export type CompletionControllerOptions = {
7
+ readonly context: EditorViewContributionContext;
8
+ readonly completionSources: EditorCompletionSourceSet;
9
+ readonly completionEditFeature?: EditorCapabilityToken<LanguageServerCompletionEditFeature>;
10
+ readonly completionWidgetClassNamespace?: string;
11
+ readonly completionAcceptOnCommitCharacter?: boolean;
12
+ getActiveDocument(): ActiveDocument | null;
13
+ ignorePointerTarget(target: EventTarget | null): boolean;
14
+ onBeforeShow(): void;
15
+ onRequestSuccess?(): void;
16
+ onRequestError(error: unknown): void;
17
+ };
18
+ export declare class CompletionController {
19
+ private readonly options;
20
+ private readonly context;
21
+ private readonly completion;
22
+ private readonly itemSources;
23
+ private completionTimer;
24
+ private completionAbort;
25
+ private completionRequestId;
26
+ private completionSession;
27
+ private selectionChosen;
28
+ private caret;
29
+ private languageId;
30
+ private disposed;
31
+ private visibleKey;
32
+ constructor(options: CompletionControllerOptions);
33
+ update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind, change: EditorContributionChange | null): void;
34
+ hide(): void;
35
+ containsTarget(target: EventTarget | null): boolean;
36
+ dispose(): void;
37
+ private installHandlers;
38
+ private uninstallHandlers;
39
+ /**
40
+ * Re-shows the live list against the word as it now stands, or reports that there is no longer a
41
+ * session to show.
42
+ *
43
+ * The word the request was made for is what a session is about: while the caret stays inside it
44
+ * every further character is more of the same question, so the answer is narrowed rather than
45
+ * asked again. Failing here is the signal to cancel — the caret has left the word, or something
46
+ * other than the typing moved the text the item ranges were measured in.
47
+ */
48
+ private refreshSession;
49
+ /**
50
+ * Moves the live list to where the caret is drawn now, or reports that there is none to move.
51
+ *
52
+ * Reached when the view moved without the text or the caret doing so — a scroll, a resize, an
53
+ * overlay claiming width. The widget is placed in viewport coordinates, so it has to follow;
54
+ * nothing about the items can have changed, so re-filtering and rebuilding the rows on every
55
+ * scroll frame would be work for nothing.
56
+ */
57
+ private reanchorSession;
58
+ private showRankedItems;
59
+ private sessionAnchorRect;
60
+ private scheduleCompletion;
61
+ private requestManualCompletion;
62
+ private requestCompletion;
63
+ private askSource;
64
+ private mergeAnswers;
65
+ private renderCompletionResult;
66
+ private acceptCompletion;
67
+ /**
68
+ * What the accepted item is applied against, or null when the list no longer belongs to the
69
+ * document on screen.
70
+ *
71
+ * Document identity would be too strict a test: every keystroke rebuilds the active document, and
72
+ * typing while the list is up is exactly the case the caret is captured for.
73
+ */
74
+ private completionAcceptance;
75
+ private applyResolvedCompletion;
76
+ private applyCompletionItem;
77
+ private completionEditFeature;
78
+ private cancelCompletionRequest;
79
+ private readonly handleDocumentPointerDown;
80
+ /**
81
+ * The keymap's commands. False when there is no list for them to act on, and an acceptance that
82
+ * could not be applied is false too, so its Enter or Tab still reaches the next binding.
83
+ */
84
+ runKeyCommand(command: CompletionKeyCommand): boolean;
85
+ private readonly handleCommitCharacterKeyDown;
86
+ /** Moving the focus is the reader claiming the row, which a rebuilt list has to honour. */
87
+ private moveSelection;
88
+ }
89
+ //# sourceMappingURL=completionController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionController.d.ts","sourceRoot":"","sources":["../src/completionController.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,wBAAwB,EACxB,qBAAqB,EAErB,6BAA6B,EAC7B,gCAAgC,EAChC,kBAAkB,EACnB,MAAM,mCAAmC,CAAA;AAE1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEzD,OAAO,EAaL,KAAK,mCAAmC,EAEzC,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAIV,yBAAyB,EAC1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AA8CnD,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,OAAO,EAAE,6BAA6B,CAAA;IAC/C,QAAQ,CAAC,iBAAiB,EAAE,yBAAyB,CAAA;IACrD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC,mCAAmC,CAAC,CAAA;IAC3F,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAA;IAChD,QAAQ,CAAC,iCAAiC,CAAC,EAAE,OAAO,CAAA;IACpD,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAAA;IAC1C,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAA;IACxD,YAAY,IAAI,IAAI,CAAA;IACpB,gBAAgB,CAAC,IAAI,IAAI,CAAA;IACzB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;CACrC,CAAA;AASD,qBAAa,oBAAoB;IAmBZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAlB3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;IAGvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4D;IACxF,OAAO,CAAC,eAAe,CAA6C;IACpE,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,iBAAiB,CAAiC;IAI1D,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,UAAU,CAAgC;gBAEd,OAAO,EAAE,2BAA2B;IAgBjE,MAAM,CACX,QAAQ,EAAE,kBAAkB,EAC5B,IAAI,EAAE,gCAAgC,EACtC,MAAM,EAAE,wBAAwB,GAAG,IAAI,GACtC,IAAI;IAiCA,IAAI,IAAI,IAAI;IAOZ,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO;IAInD,OAAO,IAAI,IAAI;IAStB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,sBAAsB;IAiC9B,OAAO,CAAC,gBAAgB;IAyBxB;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;YAad,uBAAuB;IAsBrC,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAMzC;IAED;;;OAGG;IACI,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO;IAkB5D,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAU5C;IAED,2FAA2F;IAC3F,OAAO,CAAC,aAAa;CAItB"}
@@ -0,0 +1,376 @@
1
+ import { LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE, completionAnchorRange, completionApplication, completionItemApplies, completionListResult, completionPrefix, completionTriggerFromChange, createCompletionWidgetController, rankCompletionItems } from "./completion.js";
2
+ import { completionCommitCharacter, completionCommitFeature } from "./completionCommit.js";
3
+ //#region src/completionController.ts
4
+ /** The updates that move where the text is drawn without moving the text or the caret in it. */
5
+ var VIEW_MOVEMENT_KINDS = /* @__PURE__ */ new Set(["layout", "viewport"]);
6
+ /** The word the caret sits in, as the snapshot being handled has it. */
7
+ var COMPLETION_PREFIX_WINDOW = 512;
8
+ var COMPLETION_MOVES = {
9
+ next: 1,
10
+ previous: -1,
11
+ nextPage: 8,
12
+ previousPage: -8
13
+ };
14
+ var CompletionController = class {
15
+ options;
16
+ context;
17
+ completion;
18
+ itemSources = /* @__PURE__ */ new WeakMap();
19
+ completionTimer = null;
20
+ completionAbort = null;
21
+ completionRequestId = 0;
22
+ completionSession = null;
23
+ selectionChosen = false;
24
+ caret = null;
25
+ languageId = null;
26
+ disposed = false;
27
+ visibleKey = null;
28
+ constructor(options) {
29
+ this.options = options;
30
+ this.context = options.context;
31
+ this.completion = createCompletionWidgetController({
32
+ document: this.context.container.ownerDocument,
33
+ themeSource: this.context.scrollElement,
34
+ classNamespace: options.completionWidgetClassNamespace,
35
+ onSelect: () => {
36
+ this.selectionChosen = true;
37
+ this.acceptCompletion();
38
+ }
39
+ });
40
+ this.installHandlers();
41
+ }
42
+ update(snapshot, kind, change) {
43
+ const caret = completionCaret(snapshot);
44
+ this.caret = caret;
45
+ this.languageId = snapshot.languageId;
46
+ if (kind === "document" || kind === "clear") {
47
+ this.hide();
48
+ return;
49
+ }
50
+ if (kind === "tokens") return;
51
+ if (kind !== "content") {
52
+ if (this.reanchorSession()) return;
53
+ if (!this.completionSession && VIEW_MOVEMENT_KINDS.has(kind)) return;
54
+ this.hide();
55
+ return;
56
+ }
57
+ if (this.refreshSession()) return;
58
+ this.hide();
59
+ const trigger = completionTriggerFromChange(change);
60
+ if (!trigger || !caret) return;
61
+ this.scheduleCompletion(caret.offset, trigger);
62
+ }
63
+ hide() {
64
+ this.cancelCompletionRequest();
65
+ this.completionSession = null;
66
+ this.selectionChosen = false;
67
+ this.completion.hide();
68
+ }
69
+ containsTarget(target) {
70
+ return this.completion.containsTarget(target);
71
+ }
72
+ dispose() {
73
+ if (this.disposed) return;
74
+ this.disposed = true;
75
+ this.uninstallHandlers();
76
+ this.hide();
77
+ this.completion.dispose();
78
+ }
79
+ installHandlers() {
80
+ this.visibleKey = this.context.registerKeymapContextKey("suggestWidgetVisible", () => this.completion.isVisible());
81
+ this.context.scrollElement.addEventListener("keydown", this.handleCommitCharacterKeyDown);
82
+ this.context.container.ownerDocument.addEventListener("pointerdown", this.handleDocumentPointerDown, { capture: true });
83
+ }
84
+ uninstallHandlers() {
85
+ this.visibleKey?.dispose();
86
+ this.visibleKey = null;
87
+ this.context.scrollElement.removeEventListener("keydown", this.handleCommitCharacterKeyDown);
88
+ this.context.container.ownerDocument.removeEventListener("pointerdown", this.handleDocumentPointerDown, { capture: true });
89
+ }
90
+ /**
91
+ * Re-shows the live list against the word as it now stands, or reports that there is no longer a
92
+ * session to show.
93
+ *
94
+ * The word the request was made for is what a session is about: while the caret stays inside it
95
+ * every further character is more of the same question, so the answer is narrowed rather than
96
+ * asked again. Failing here is the signal to cancel — the caret has left the word, or something
97
+ * other than the typing moved the text the item ranges were measured in.
98
+ */
99
+ refreshSession() {
100
+ const session = this.completionSession;
101
+ const caret = this.caret;
102
+ if (!session || !caret) return false;
103
+ if (!completionSessionSurvives(session, caret)) return false;
104
+ const ranked = rankCompletionItems(session.items, caret.prefix);
105
+ if (ranked.length === 0) return false;
106
+ if (session.isIncomplete) this.scheduleCompletion(caret.offset, { triggerKind: 3 });
107
+ return this.showRankedItems(ranked, caret);
108
+ }
109
+ /**
110
+ * Moves the live list to where the caret is drawn now, or reports that there is none to move.
111
+ *
112
+ * Reached when the view moved without the text or the caret doing so — a scroll, a resize, an
113
+ * overlay claiming width. The widget is placed in viewport coordinates, so it has to follow;
114
+ * nothing about the items can have changed, so re-filtering and rebuilding the rows on every
115
+ * scroll frame would be work for nothing.
116
+ */
117
+ reanchorSession() {
118
+ const caret = this.caret;
119
+ if (!this.completionSession || !caret) return false;
120
+ if (!completionSessionSurvives(this.completionSession, caret)) return false;
121
+ const rect = this.sessionAnchorRect(caret);
122
+ if (!rect) return false;
123
+ this.completion.reanchor(rect);
124
+ return true;
125
+ }
126
+ showRankedItems(ranked, caret) {
127
+ const rect = this.sessionAnchorRect(caret);
128
+ if (!rect) return false;
129
+ const items = ranked.slice(0, 100);
130
+ this.options.onBeforeShow();
131
+ this.completion.show({
132
+ anchor: rect,
133
+ items,
134
+ selectedIndex: focusedCompletionIndex(items, this.selectionChosen ? this.completion.selectedItem() : null)
135
+ });
136
+ return true;
137
+ }
138
+ sessionAnchorRect(caret) {
139
+ const range = completionAnchorRange(caret.length, caret.offset);
140
+ return this.context.getRangeClientRect(range.start, range.end);
141
+ }
142
+ scheduleCompletion(offset, trigger) {
143
+ const active = this.options.getActiveDocument();
144
+ if (!active) return;
145
+ this.cancelCompletionRequest();
146
+ this.completionTimer = setTimeout(() => {
147
+ this.completionTimer = null;
148
+ this.requestCompletion(active, offset, trigger);
149
+ }, 80);
150
+ }
151
+ requestManualCompletion() {
152
+ const active = this.options.getActiveDocument();
153
+ const caret = this.caret;
154
+ if (!active || !caret) return this.hide();
155
+ this.cancelCompletionRequest();
156
+ this.requestCompletion(active, caret.offset, { triggerKind: 1 });
157
+ }
158
+ requestCompletion(active, offset, trigger) {
159
+ this.completionAbort?.abort();
160
+ const requestId = this.completionRequestId + 1;
161
+ const abort = new AbortController();
162
+ this.completionRequestId = requestId;
163
+ this.completionAbort = abort;
164
+ const sources = this.options.completionSources.forLanguage(this.languageId);
165
+ const request = {
166
+ uri: active.uri,
167
+ document: active,
168
+ offset,
169
+ trigger,
170
+ signal: abort.signal
171
+ };
172
+ Promise.allSettled(sources.map((source) => this.askSource(source, request))).then((answers) => this.renderCompletionResult(requestId, active, offset, this.mergeAnswers(sources, answers)));
173
+ }
174
+ askSource(source, request) {
175
+ try {
176
+ return source.provideCompletionItems(request);
177
+ } catch (error) {
178
+ return Promise.reject(error);
179
+ }
180
+ }
181
+ mergeAnswers(sources, answers) {
182
+ const items = [];
183
+ let isIncomplete = false;
184
+ for (const [index, answer] of answers.entries()) {
185
+ if (answer.status === "rejected") {
186
+ this.options.onRequestError(answer.reason);
187
+ continue;
188
+ }
189
+ this.options.onRequestSuccess?.();
190
+ const result = completionListResult(answer.value ?? null);
191
+ const source = sources[index];
192
+ for (const item of result.items) {
193
+ if (source) this.itemSources.set(item, source);
194
+ items.push(item);
195
+ }
196
+ if (result.isIncomplete) isIncomplete = true;
197
+ }
198
+ return {
199
+ items,
200
+ isIncomplete
201
+ };
202
+ }
203
+ renderCompletionResult(requestId, active, offset, result) {
204
+ if (requestId !== this.completionRequestId) return;
205
+ if (active !== this.options.getActiveDocument()) return;
206
+ if (result.items.length === 0) return this.hide();
207
+ const caret = this.caret;
208
+ if (!caret) return this.hide();
209
+ const requestDocument = {
210
+ document: active,
211
+ offset
212
+ };
213
+ const items = result.items.filter((item) => completionItemApplies(requestDocument, item));
214
+ const ranked = rankCompletionItems(items, caret.prefix);
215
+ if (ranked.length === 0) return this.hide();
216
+ this.completionSession = {
217
+ active,
218
+ offset,
219
+ wordStart: caret.wordStart,
220
+ items,
221
+ isIncomplete: result.isIncomplete
222
+ };
223
+ if (!this.showRankedItems(ranked, caret)) this.hide();
224
+ }
225
+ acceptCompletion(commitCharacter = null) {
226
+ const session = this.completionSession;
227
+ const item = this.completion.selectedItem();
228
+ if (!session || !item) return false;
229
+ const acceptance = this.completionAcceptance(session);
230
+ if (!acceptance) return false;
231
+ const editFeature = this.completionEditFeature();
232
+ if (!editFeature) return false;
233
+ const feature = completionCommitFeature(editFeature, commitCharacter);
234
+ const pending = this.itemSources.get(item)?.resolveCompletionItem?.(item) ?? null;
235
+ this.hide();
236
+ if (pending) {
237
+ this.applyResolvedCompletion(acceptance, item, pending, feature);
238
+ return true;
239
+ }
240
+ return this.applyCompletionItem(acceptance, item, feature);
241
+ }
242
+ /**
243
+ * What the accepted item is applied against, or null when the list no longer belongs to the
244
+ * document on screen.
245
+ *
246
+ * Document identity would be too strict a test: every keystroke rebuilds the active document, and
247
+ * typing while the list is up is exactly the case the caret is captured for.
248
+ */
249
+ completionAcceptance(session) {
250
+ const document = this.options.getActiveDocument();
251
+ if (!document || document.uri !== session.active.uri) return null;
252
+ if (!this.caret) return null;
253
+ return {
254
+ session,
255
+ document,
256
+ caretOffset: this.caret.offset,
257
+ caretAffinity: this.caret.affinity
258
+ };
259
+ }
260
+ async applyResolvedCompletion(acceptance, item, pending, feature) {
261
+ let resolved = item;
262
+ try {
263
+ resolved = await pending;
264
+ this.options.onRequestSuccess?.();
265
+ } catch (error) {
266
+ this.options.onRequestError(error);
267
+ }
268
+ if (acceptance.document !== this.options.getActiveDocument()) return;
269
+ this.applyCompletionItem(acceptance, resolved, feature);
270
+ }
271
+ applyCompletionItem(acceptance, item, feature) {
272
+ const application = completionApplication({
273
+ document: acceptance.session.active,
274
+ offset: acceptance.session.offset,
275
+ caretOffset: acceptance.caretOffset,
276
+ caretAffinity: acceptance.caretAffinity
277
+ }, item);
278
+ if (!application) return false;
279
+ return feature.applyCompletion(application);
280
+ }
281
+ completionEditFeature() {
282
+ const token = this.options.completionEditFeature ?? LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE;
283
+ return this.context.getFeature(token);
284
+ }
285
+ cancelCompletionRequest() {
286
+ if (this.completionTimer) clearTimeout(this.completionTimer);
287
+ this.completionTimer = null;
288
+ this.completionAbort?.abort();
289
+ this.completionAbort = null;
290
+ this.completionRequestId += 1;
291
+ }
292
+ handleDocumentPointerDown = (event) => {
293
+ if (event.button !== 0) return;
294
+ if (this.completion.containsTarget(event.target)) return;
295
+ if (this.options.ignorePointerTarget(event.target)) return;
296
+ this.hide();
297
+ };
298
+ /**
299
+ * The keymap's commands. False when there is no list for them to act on, and an acceptance that
300
+ * could not be applied is false too, so its Enter or Tab still reaches the next binding.
301
+ */
302
+ runKeyCommand(command) {
303
+ if (command === "trigger") {
304
+ this.requestManualCompletion();
305
+ return true;
306
+ }
307
+ if (!this.completion.isVisible()) return false;
308
+ if (command === "accept") return this.acceptCompletion();
309
+ if (command === "hide") {
310
+ this.hide();
311
+ return true;
312
+ }
313
+ this.moveSelection(COMPLETION_MOVES[command]);
314
+ return true;
315
+ }
316
+ handleCommitCharacterKeyDown = (event) => {
317
+ if (!this.completion.isVisible()) return;
318
+ const commitCharacter = completionCommitCharacter(event, this.completion.selectedItem(), this.options.completionAcceptOnCommitCharacter === true);
319
+ if (commitCharacter === null) return;
320
+ if (this.acceptCompletion(commitCharacter)) event.preventDefault();
321
+ };
322
+ /** Moving the focus is the reader claiming the row, which a rebuilt list has to honour. */
323
+ moveSelection(delta) {
324
+ this.selectionChosen = true;
325
+ this.completion.moveSelection(delta);
326
+ }
327
+ };
328
+ /**
329
+ * Where the row the user chose goes in a list that has just been rebuilt, given `focused` only when
330
+ * they chose one at all.
331
+ *
332
+ * A truncated list is asked again while it is on screen, and the answer arrives as items of its own,
333
+ * so the choice has to be recognised by label rather than by identity. Dropping back to the top would
334
+ * take the arrow keys away from the user between one keystroke and the next.
335
+ */
336
+ function focusedCompletionIndex(items, focused) {
337
+ if (!focused) return 0;
338
+ return Math.max(0, items.findIndex((item) => item.label === focused.label));
339
+ }
340
+ function completionCaret(snapshot) {
341
+ const selection = snapshot.selections[0];
342
+ if (!selection) return null;
343
+ if (selection.startOffset !== selection.endOffset) return null;
344
+ const offset = selection.headOffset;
345
+ const source = snapshot.textSnapshot;
346
+ const windowStart = Math.max(0, offset - COMPLETION_PREFIX_WINDOW);
347
+ const before = source.readRange(windowStart, offset);
348
+ const length = source.length;
349
+ const prefix = completionPrefix(before, before.length);
350
+ return {
351
+ length,
352
+ offset,
353
+ affinity: selection.affinity,
354
+ wordStart: offset - prefix.length,
355
+ prefix
356
+ };
357
+ }
358
+ /**
359
+ * Whether the list still describes what the user is doing.
360
+ *
361
+ * A word start that has moved means the caret is in a different word than the one asked about — a
362
+ * new identifier, a trigger character, an outdent, or a foreign edit above the caret. A document
363
+ * whose length no longer accounts for exactly where the caret went means something other than the
364
+ * typing changed the text, and the item ranges — offsets into the text the request captured — would
365
+ * be applied to text that moved underneath them. Backing out of the word entirely ends it too,
366
+ * while backspacing to a shorter prefix inside it does not.
367
+ */
368
+ function completionSessionSurvives(session, caret) {
369
+ if (caret.wordStart !== session.wordStart) return false;
370
+ if (caret.prefix.length === 0 && caret.offset < session.offset) return false;
371
+ return caret.length - session.active.textSnapshot.length === caret.offset - session.offset;
372
+ }
373
+ //#endregion
374
+ export { CompletionController };
375
+
376
+ //# sourceMappingURL=completionController.js.map