@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":"completionController.js","names":[],"sources":["../src/completionController.ts"],"sourcesContent":["import type { SelectionAffinity } from '@singapore-editor/core/document'\nimport type {\n EditorContributionChange,\n EditorCapabilityToken,\n EditorDisposable,\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport type * as lsp from 'vscode-languageserver-protocol'\nimport type { CompletionKeyCommand } from './keyCommands'\n\nimport {\n COMPLETION_REQUEST_DEBOUNCE_MS,\n LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE,\n completionAnchorRange,\n completionApplication,\n completionItemApplies,\n completionListResult,\n completionPrefix,\n completionTriggerFromChange,\n createCompletionWidgetController,\n rankCompletionItems,\n type CompletionListResult,\n type CompletionWidgetController,\n type LanguageServerCompletionEditFeature,\n type LanguageServerCompletionTrigger,\n} from './completion'\nimport { completionCommitCharacter, completionCommitFeature } from './completionCommit'\nimport type {\n EditorCompletionAnswer,\n EditorCompletionRequest,\n EditorCompletionSource,\n EditorCompletionSourceSet,\n} from './completionProviders'\nimport type { ActiveDocument } from './pluginTypes'\n\n/**\n * A list on screen and the document frame its items were measured in.\n *\n * `active` and `offset` stay pinned to the request for as long as the session lives, because an\n * item's ranges are positions in that text and in no other. The word being typed is re-read from\n * each new snapshot instead, so the session can outlive the keystroke that asked for it.\n */\ntype CompletionSession = {\n readonly active: ActiveDocument\n readonly offset: number\n readonly wordStart: number\n readonly items: readonly lsp.CompletionItem[]\n readonly isIncomplete: boolean\n}\n\n/** The updates that move where the text is drawn without moving the text or the caret in it. */\nconst VIEW_MOVEMENT_KINDS: ReadonlySet<EditorViewContributionUpdateKind> = new Set([\n 'layout',\n 'viewport',\n])\n\n/** The word the caret sits in, as the snapshot being handled has it. */\n// Longer than any identifier a completion prefix could be read from, so the window never truncates\n// a word the caret is standing in.\nconst COMPLETION_PREFIX_WINDOW = 512\n\ntype CompletionCaret = {\n // The document's length rather than its text: what the caret is measured against is how far the\n // text extends, and reading the text itself would put a copy of the document on every keystroke.\n readonly length: number\n readonly offset: number\n readonly affinity: SelectionAffinity\n readonly wordStart: number\n readonly prefix: string\n}\n\n/** One accepted item, measured against the document and caret at the moment of acceptance. */\ntype CompletionAcceptance = {\n readonly session: CompletionSession\n readonly document: ActiveDocument\n readonly caretOffset: number\n readonly caretAffinity: SelectionAffinity\n}\n\nexport type CompletionControllerOptions = {\n readonly context: EditorViewContributionContext\n readonly completionSources: EditorCompletionSourceSet\n readonly completionEditFeature?: EditorCapabilityToken<LanguageServerCompletionEditFeature>\n readonly completionWidgetClassNamespace?: string\n readonly completionAcceptOnCommitCharacter?: boolean\n getActiveDocument(): ActiveDocument | null\n ignorePointerTarget(target: EventTarget | null): boolean\n onBeforeShow(): void\n onRequestSuccess?(): void\n onRequestError(error: unknown): void\n}\n\nconst COMPLETION_MOVES = {\n next: 1,\n previous: -1,\n nextPage: 8,\n previousPage: -8,\n} as const satisfies Partial<Record<CompletionKeyCommand, number>>\n\nexport class CompletionController {\n private readonly context: EditorViewContributionContext\n private readonly completion: CompletionWidgetController\n // Which source an item came from, kept beside the list rather than on the items: the items are\n // the sources' own objects, and one of them goes back to its source to be resolved.\n private readonly itemSources = new WeakMap<lsp.CompletionItem, EditorCompletionSource>()\n private completionTimer: ReturnType<typeof setTimeout> | null = null\n private completionAbort: AbortController | null = null\n private completionRequestId = 0\n private completionSession: CompletionSession | null = null\n // Whether the row on the widget is one the reader put there. The list is re-ranked and re-shown on\n // every keystroke, so the item that happened to be on top a moment ago is nobody's choice, and\n // carrying it across a rebuild is how the focus comes to sit on an item the reader watched fall.\n private selectionChosen = false\n private caret: CompletionCaret | null = null\n private languageId: EditorViewSnapshot['languageId'] = null\n private disposed = false\n private visibleKey: EditorDisposable | null = null\n\n public constructor(private readonly options: CompletionControllerOptions) {\n this.context = options.context\n this.completion = createCompletionWidgetController({\n document: this.context.container.ownerDocument,\n themeSource: this.context.scrollElement,\n classNamespace: options.completionWidgetClassNamespace,\n onSelect: () => {\n // The pointer chose the row it landed on, and a list that survives an acceptance it could\n // not apply owes that choice the same standing an arrow key would have given it.\n this.selectionChosen = true\n this.acceptCompletion()\n },\n })\n this.installHandlers()\n }\n\n public update(\n snapshot: EditorViewSnapshot,\n kind: EditorViewContributionUpdateKind,\n change: EditorContributionChange | null,\n ): void {\n // A session's offsets are all read from these snapshots, and the distance between the request\n // and the acceptance is only a distance if both ends were measured on the same clock.\n const caret = completionCaret(snapshot)\n this.caret = caret\n // Which sources answer is the document's language to decide, and a request outlives the update\n // that scheduled it.\n this.languageId = snapshot.languageId\n if (kind === 'document' || kind === 'clear') {\n this.hide()\n return\n }\n if (kind === 'tokens') return\n if (kind !== 'content') {\n if (this.reanchorSession()) return\n // Nothing drawn yet to move, but the reader has already asked: a request still on its way was\n // measured against a caret the view moving does not touch, so cancelling it here is how a\n // list comes to never appear at all for a keystroke that scrolled the caret into place.\n if (!this.completionSession && VIEW_MOVEMENT_KINDS.has(kind)) return\n\n this.hide()\n return\n }\n\n if (this.refreshSession()) return\n\n this.hide()\n const trigger = completionTriggerFromChange(change)\n if (!trigger || !caret) return\n\n this.scheduleCompletion(caret.offset, trigger)\n }\n\n public hide(): void {\n this.cancelCompletionRequest()\n this.completionSession = null\n this.selectionChosen = false\n this.completion.hide()\n }\n\n public containsTarget(target: EventTarget | null): boolean {\n return this.completion.containsTarget(target)\n }\n\n public dispose(): void {\n if (this.disposed) return\n\n this.disposed = true\n this.uninstallHandlers()\n this.hide()\n this.completion.dispose()\n }\n\n private installHandlers(): void {\n this.visibleKey = this.context.registerKeymapContextKey('suggestWidgetVisible', () =>\n this.completion.isVisible(),\n )\n this.context.scrollElement.addEventListener('keydown', this.handleCommitCharacterKeyDown)\n this.context.container.ownerDocument.addEventListener(\n 'pointerdown',\n this.handleDocumentPointerDown,\n { capture: true },\n )\n }\n\n private uninstallHandlers(): void {\n this.visibleKey?.dispose()\n this.visibleKey = null\n this.context.scrollElement.removeEventListener('keydown', this.handleCommitCharacterKeyDown)\n this.context.container.ownerDocument.removeEventListener(\n 'pointerdown',\n this.handleDocumentPointerDown,\n { capture: true },\n )\n }\n\n /**\n * Re-shows the live list against the word as it now stands, or reports that there is no longer a\n * session to show.\n *\n * The word the request was made for is what a session is about: while the caret stays inside it\n * every further character is more of the same question, so the answer is narrowed rather than\n * asked again. Failing here is the signal to cancel — the caret has left the word, or something\n * other than the typing moved the text the item ranges were measured in.\n */\n private refreshSession(): boolean {\n const session = this.completionSession\n const caret = this.caret\n if (!session || !caret) return false\n if (!completionSessionSurvives(session, caret)) return false\n\n const ranked = rankCompletionItems(session.items, caret.prefix)\n if (ranked.length === 0) return false\n\n // Every character narrows a truncated answer further away from being one, so this is the case\n // worth another round-trip — and the list already on screen stays up while it is in flight.\n if (session.isIncomplete) this.scheduleCompletion(caret.offset, { triggerKind: 3 })\n\n return this.showRankedItems(ranked, caret)\n }\n\n /**\n * Moves the live list to where the caret is drawn now, or reports that there is none to move.\n *\n * Reached when the view moved without the text or the caret doing so — a scroll, a resize, an\n * overlay claiming width. The widget is placed in viewport coordinates, so it has to follow;\n * nothing about the items can have changed, so re-filtering and rebuilding the rows on every\n * scroll frame would be work for nothing.\n */\n private reanchorSession(): boolean {\n const caret = this.caret\n if (!this.completionSession || !caret) return false\n if (!completionSessionSurvives(this.completionSession, caret)) return false\n\n const rect = this.sessionAnchorRect(caret)\n if (!rect) return false\n\n this.completion.reanchor(rect)\n return true\n }\n\n private showRankedItems(ranked: readonly lsp.CompletionItem[], caret: CompletionCaret): boolean {\n const rect = this.sessionAnchorRect(caret)\n if (!rect) return false\n\n const items = ranked.slice(0, 100)\n this.options.onBeforeShow()\n this.completion.show({\n anchor: rect,\n items,\n selectedIndex: focusedCompletionIndex(\n items,\n this.selectionChosen ? this.completion.selectedItem() : null,\n ),\n })\n return true\n }\n\n private sessionAnchorRect(caret: CompletionCaret): DOMRect | null {\n const range = completionAnchorRange(caret.length, caret.offset)\n return this.context.getRangeClientRect(range.start, range.end)\n }\n\n private scheduleCompletion(offset: number, trigger: LanguageServerCompletionTrigger): void {\n const active = this.options.getActiveDocument()\n if (!active) return\n\n this.cancelCompletionRequest()\n this.completionTimer = setTimeout(() => {\n this.completionTimer = null\n this.requestCompletion(active, offset, trigger)\n }, COMPLETION_REQUEST_DEBOUNCE_MS)\n }\n\n private requestManualCompletion(): void {\n const active = this.options.getActiveDocument()\n const caret = this.caret\n if (!active || !caret) return this.hide()\n\n this.cancelCompletionRequest()\n this.requestCompletion(active, caret.offset, { triggerKind: 1 })\n }\n\n private requestCompletion(\n active: ActiveDocument,\n offset: number,\n trigger: LanguageServerCompletionTrigger,\n ): void {\n this.completionAbort?.abort()\n const requestId = this.completionRequestId + 1\n const abort = new AbortController()\n this.completionRequestId = requestId\n this.completionAbort = abort\n\n const sources = this.options.completionSources.forLanguage(this.languageId)\n const request: EditorCompletionRequest = {\n uri: active.uri,\n document: active,\n offset,\n trigger,\n signal: abort.signal,\n }\n // Asked at once and shown once every one of them has answered, so the reader is not made to\n // choose from a list that is still growing under the cursor. Settled rather than all: a source\n // that fails takes itself out of the list and nothing else with it.\n void Promise.allSettled(sources.map((source) => this.askSource(source, request))).then(\n (answers) =>\n this.renderCompletionResult(requestId, active, offset, this.mergeAnswers(sources, answers)),\n )\n }\n\n private askSource(\n source: EditorCompletionSource,\n request: EditorCompletionRequest,\n ): PromiseLike<EditorCompletionAnswer> | EditorCompletionAnswer {\n try {\n return source.provideCompletionItems(request)\n } catch (error) {\n // A source that throws where it should have rejected is still only one source failing.\n return Promise.reject(error)\n }\n }\n\n private mergeAnswers(\n sources: readonly EditorCompletionSource[],\n answers: readonly PromiseSettledResult<EditorCompletionAnswer>[],\n ): CompletionListResult {\n const items: lsp.CompletionItem[] = []\n let isIncomplete = false\n for (const [index, answer] of answers.entries()) {\n if (answer.status === 'rejected') {\n this.options.onRequestError(answer.reason)\n continue\n }\n\n this.options.onRequestSuccess?.()\n const result = completionListResult(answer.value ?? null)\n const source = sources[index]\n for (const item of result.items) {\n if (source) this.itemSources.set(item, source)\n items.push(item)\n }\n // One truncated answer is enough to make the merged list one: the word growing sends every\n // source back to the question, not only the one that said so.\n if (result.isIncomplete) isIncomplete = true\n }\n\n return { items, isIncomplete }\n }\n\n private renderCompletionResult(\n requestId: number,\n active: ActiveDocument,\n offset: number,\n result: CompletionListResult,\n ): void {\n if (requestId !== this.completionRequestId) return\n if (active !== this.options.getActiveDocument()) return\n if (result.items.length === 0) return this.hide()\n\n // Every path that moves the text or the caret between the request and its answer cancels the\n // request too, so this caret is the one the request was measured against.\n const caret = this.caret\n if (!caret) return this.hide()\n\n // Judged once, here, rather than at every acceptance: what an item can be applied against is the\n // text the request went out with, and that text is the session's for as long as it lives.\n const requestDocument = { document: active, offset }\n const items = result.items.filter((item) => completionItemApplies(requestDocument, item))\n const ranked = rankCompletionItems(items, caret.prefix)\n if (ranked.length === 0) return this.hide()\n\n this.completionSession = {\n active,\n offset,\n // The word the answer is about, which is what a session lives or dies by from here on.\n wordStart: caret.wordStart,\n items,\n isIncomplete: result.isIncomplete,\n }\n if (!this.showRankedItems(ranked, caret)) this.hide()\n }\n\n private acceptCompletion(commitCharacter: string | null = null): boolean {\n const session = this.completionSession\n const item = this.completion.selectedItem()\n if (!session || !item) return false\n\n const acceptance = this.completionAcceptance(session)\n if (!acceptance) return false\n\n const editFeature = this.completionEditFeature()\n if (!editFeature) return false\n\n const feature = completionCommitFeature(editFeature, commitCharacter)\n // Asked of the source that sent this item, so an item from a source that sends whole ones is\n // applied as it stands even while another source in the same list is still sending stubs.\n const pending = this.itemSources.get(item)?.resolveCompletionItem?.(item) ?? null\n\n this.hide()\n if (pending) {\n void this.applyResolvedCompletion(acceptance, item, pending, feature)\n return true\n }\n\n return this.applyCompletionItem(acceptance, item, feature)\n }\n\n /**\n * What the accepted item is applied against, or null when the list no longer belongs to the\n * document on screen.\n *\n * Document identity would be too strict a test: every keystroke rebuilds the active document, and\n * typing while the list is up is exactly the case the caret is captured for.\n */\n private completionAcceptance(session: CompletionSession): CompletionAcceptance | null {\n const document = this.options.getActiveDocument()\n if (!document || document.uri !== session.active.uri) return null\n if (!this.caret) return null\n\n return {\n session,\n document,\n caretOffset: this.caret.offset,\n caretAffinity: this.caret.affinity,\n }\n }\n\n private async applyResolvedCompletion(\n acceptance: CompletionAcceptance,\n item: lsp.CompletionItem,\n pending: PromiseLike<lsp.CompletionItem>,\n feature: LanguageServerCompletionEditFeature,\n ): Promise<void> {\n let resolved = item\n try {\n resolved = await pending\n this.options.onRequestSuccess?.()\n } catch (error) {\n // A server that cannot resolve should still get its completion applied, unresolved.\n this.options.onRequestError(error)\n }\n\n // The caret was read before the round-trip, so a document that moved during it would be edited\n // against measurements taken of the text it no longer holds.\n if (acceptance.document !== this.options.getActiveDocument()) return\n\n this.applyCompletionItem(acceptance, resolved, feature)\n }\n\n private applyCompletionItem(\n acceptance: CompletionAcceptance,\n item: lsp.CompletionItem,\n feature: LanguageServerCompletionEditFeature,\n ): boolean {\n const application = completionApplication(\n {\n document: acceptance.session.active,\n offset: acceptance.session.offset,\n caretOffset: acceptance.caretOffset,\n caretAffinity: acceptance.caretAffinity,\n },\n item,\n )\n if (!application) return false\n\n return feature.applyCompletion(application)\n }\n\n private completionEditFeature(): LanguageServerCompletionEditFeature | null {\n const token = this.options.completionEditFeature ?? LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE\n return this.context.getFeature(token)\n }\n\n private cancelCompletionRequest(): void {\n if (this.completionTimer) clearTimeout(this.completionTimer)\n this.completionTimer = null\n this.completionAbort?.abort()\n this.completionAbort = null\n this.completionRequestId += 1\n }\n\n private readonly handleDocumentPointerDown = (event: PointerEvent): void => {\n if (event.button !== 0) return\n if (this.completion.containsTarget(event.target)) return\n if (this.options.ignorePointerTarget(event.target)) return\n\n this.hide()\n }\n\n /**\n * The keymap's commands. False when there is no list for them to act on, and an acceptance that\n * could not be applied is false too, so its Enter or Tab still reaches the next binding.\n */\n public runKeyCommand(command: CompletionKeyCommand): boolean {\n if (command === 'trigger') {\n this.requestManualCompletion()\n return true\n }\n if (!this.completion.isVisible()) return false\n if (command === 'accept') return this.acceptCompletion()\n if (command === 'hide') {\n this.hide()\n return true\n }\n\n this.moveSelection(COMPLETION_MOVES[command])\n return true\n }\n\n // Any typed character can commit, so this is a listener rather than a binding. Swallowed only once\n // the item is in: a failed acceptance still owes the reader the character they typed.\n private readonly handleCommitCharacterKeyDown = (event: KeyboardEvent): void => {\n if (!this.completion.isVisible()) return\n\n const commitCharacter = completionCommitCharacter(\n event,\n this.completion.selectedItem(),\n this.options.completionAcceptOnCommitCharacter === true,\n )\n if (commitCharacter === null) return\n if (this.acceptCompletion(commitCharacter)) event.preventDefault()\n }\n\n /** Moving the focus is the reader claiming the row, which a rebuilt list has to honour. */\n private moveSelection(delta: number): void {\n this.selectionChosen = true\n this.completion.moveSelection(delta)\n }\n}\n\n/**\n * Where the row the user chose goes in a list that has just been rebuilt, given `focused` only when\n * they chose one at all.\n *\n * A truncated list is asked again while it is on screen, and the answer arrives as items of its own,\n * so the choice has to be recognised by label rather than by identity. Dropping back to the top would\n * take the arrow keys away from the user between one keystroke and the next.\n */\nfunction focusedCompletionIndex(\n items: readonly lsp.CompletionItem[],\n focused: lsp.CompletionItem | null,\n): number {\n if (!focused) return 0\n\n return Math.max(\n 0,\n items.findIndex((item) => item.label === focused.label),\n )\n}\n\nfunction completionCaret(snapshot: EditorViewSnapshot): CompletionCaret | null {\n const selection = snapshot.selections[0]\n if (!selection) return null\n if (selection.startOffset !== selection.endOffset) return null\n\n const offset = selection.headOffset\n const source = snapshot.textSnapshot\n // A word ends where the caret is, so the window behind it is the whole of what a prefix can be\n // read from. No identifier reaches the end of it.\n const windowStart = Math.max(0, offset - COMPLETION_PREFIX_WINDOW)\n const before = source.readRange(windowStart, offset)\n const length = source.length\n const prefix = completionPrefix(before, before.length)\n return {\n length,\n offset,\n affinity: selection.affinity,\n wordStart: offset - prefix.length,\n prefix,\n }\n}\n\n/**\n * Whether the list still describes what the user is doing.\n *\n * A word start that has moved means the caret is in a different word than the one asked about — a\n * new identifier, a trigger character, an outdent, or a foreign edit above the caret. A document\n * whose length no longer accounts for exactly where the caret went means something other than the\n * typing changed the text, and the item ranges — offsets into the text the request captured — would\n * be applied to text that moved underneath them. Backing out of the word entirely ends it too,\n * while backspacing to a shorter prefix inside it does not.\n */\nfunction completionSessionSurvives(session: CompletionSession, caret: CompletionCaret): boolean {\n if (caret.wordStart !== session.wordStart) return false\n if (caret.prefix.length === 0 && caret.offset < session.offset) return false\n\n return caret.length - session.active.textSnapshot.length === caret.offset - session.offset\n}\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { type EditorDisposable, type EditorViewContributionContext, type EditorViewSnapshot } from '@singapore-editor/core/extensions';
2
+ import { type LspClient, type LspTextDocumentSnapshot } from '@singapore-editor/lsp';
3
+ import type * as lsp from 'vscode-languageserver-protocol';
4
+ import { type LanguageServerCompletionTrigger } from './completion';
5
+ /** What is being completed, in the text the caller has, not in a protocol position. */
6
+ export type EditorCompletionRequest = {
7
+ readonly uri: string;
8
+ readonly document: LspTextDocumentSnapshot;
9
+ readonly offset: number;
10
+ readonly trigger: LanguageServerCompletionTrigger;
11
+ readonly signal: AbortSignal;
12
+ };
13
+ /**
14
+ * What a source hands back: the protocol's own shapes, so a source that already speaks to a server
15
+ * returns the answer as it came and a source that does not can hand over a plain array.
16
+ */
17
+ export type EditorCompletionAnswer = lsp.CompletionList | readonly lsp.CompletionItem[] | null | undefined;
18
+ export type EditorCompletionSource = {
19
+ provideCompletionItems(request: EditorCompletionRequest): PromiseLike<EditorCompletionAnswer> | EditorCompletionAnswer;
20
+ /**
21
+ * The rest of an item, for a source that sends a cheap one first, or null when this item is
22
+ * already whole. Judged here because only the source knows what it left out.
23
+ */
24
+ resolveCompletionItem?(item: lsp.CompletionItem): PromiseLike<lsp.CompletionItem> | null;
25
+ };
26
+ /** The sources a document is answered from, in the order they are to be asked. */
27
+ export type EditorCompletionSourceSet = {
28
+ forLanguage(languageId: EditorViewSnapshot['languageId']): readonly EditorCompletionSource[];
29
+ };
30
+ export declare function createLanguageServerCompletionSource(client: LspClient, isAvailable?: () => boolean, onRequestSuccess?: () => void, onRequestError?: (method: string, error: unknown) => void): EditorCompletionSource;
31
+ /**
32
+ * The sources a completion request is answered from.
33
+ *
34
+ * The server's own goes into the editor's channel rather than being called directly, so that a
35
+ * snippet set or a second server registered for the same feature is asked in one order and lands in
36
+ * one list. The registration is held here because the connection it speaks for is: a source outlives
37
+ * neither.
38
+ *
39
+ * A host that cannot take the registration cannot hand the list back either, and is answered from
40
+ * the server alone — which is what it got before there was a channel to join.
41
+ */
42
+ export declare class LanguageServerCompletionSources implements EditorCompletionSourceSet, EditorDisposable {
43
+ private readonly context;
44
+ private readonly sources;
45
+ private readonly registrations;
46
+ constructor(context: EditorViewContributionContext, sources: readonly EditorCompletionSource[]);
47
+ forLanguage(languageId: EditorViewSnapshot['languageId']): readonly EditorCompletionSource[];
48
+ dispose(): void;
49
+ }
50
+ //# sourceMappingURL=completionProviders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionProviders.d.ts","sourceRoot":"","sources":["../src/completionProviders.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,6BAA6B,EAClC,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,OAAO,EAA0B,KAAK,+BAA+B,EAAE,MAAM,cAAc,CAAA;AAS3F,uFAAuF;AACvF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAA;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,+BAA+B,CAAA;IACjD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC7B,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAC9B,GAAG,CAAC,cAAc,GAClB,SAAS,GAAG,CAAC,cAAc,EAAE,GAC7B,IAAI,GACJ,SAAS,CAAA;AAEb,MAAM,MAAM,sBAAsB,GAAG;IACnC,sBAAsB,CACpB,OAAO,EAAE,uBAAuB,GAC/B,WAAW,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAA;IAC/D;;;OAGG;IACH,qBAAqB,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;CACzF,CAAA;AAMD,kFAAkF;AAClF,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,CAAC,UAAU,EAAE,kBAAkB,CAAC,YAAY,CAAC,GAAG,SAAS,sBAAsB,EAAE,CAAA;CAC7F,CAAA;AAED,wBAAgB,oCAAoC,CAClD,MAAM,EAAE,SAAS,EACjB,WAAW,GAAE,MAAM,OAAoB,EACvC,gBAAgB,CAAC,EAAE,MAAM,IAAI,EAC7B,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACxD,sBAAsB,CA0CxB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,+BACX,YAAW,yBAAyB,EAAE,gBAAgB;IAKpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;gBAGxC,OAAO,EAAE,6BAA6B,EACtC,OAAO,EAAE,SAAS,sBAAsB,EAAE;IActD,WAAW,CAChB,UAAU,EAAE,kBAAkB,CAAC,YAAY,CAAC,GAC3C,SAAS,sBAAsB,EAAE;IAQ7B,OAAO,IAAI,IAAI;CAGvB"}
@@ -0,0 +1,64 @@
1
+ import { completionNeedsResolve } from "./completion.js";
2
+ import { offsetToLspPositionInSnapshot } from "@singapore-editor/lsp";
3
+ import { createEditorLanguageFeatureToken } from "@singapore-editor/core/extensions";
4
+ //#region src/completionProviders.ts
5
+ var EDITOR_COMPLETION_SOURCE = createEditorLanguageFeatureToken("editor.completionSource");
6
+ function createLanguageServerCompletionSource(client, isAvailable = () => true, onRequestSuccess, onRequestError) {
7
+ return {
8
+ provideCompletionItems: (request) => {
9
+ if (!client.initialized || !isAvailable()) return null;
10
+ return client.request("textDocument/completion", {
11
+ textDocument: { uri: request.uri },
12
+ position: offsetToLspPositionInSnapshot(request.document, request.offset),
13
+ context: request.trigger
14
+ }, { signal: request.signal }).then((answer) => {
15
+ onRequestSuccess?.();
16
+ return answer;
17
+ }, (error) => {
18
+ onRequestError?.("textDocument/completion", error);
19
+ return null;
20
+ });
21
+ },
22
+ resolveCompletionItem: (item) => completionNeedsResolve(item, client.serverCapabilities) ? client.request("completionItem/resolve", item).then(void 0, (error) => {
23
+ onRequestError?.("completionItem/resolve", error);
24
+ return item;
25
+ }) : null
26
+ };
27
+ }
28
+ /**
29
+ * The sources a completion request is answered from.
30
+ *
31
+ * The server's own goes into the editor's channel rather than being called directly, so that a
32
+ * snippet set or a second server registered for the same feature is asked in one order and lands in
33
+ * one list. The registration is held here because the connection it speaks for is: a source outlives
34
+ * neither.
35
+ *
36
+ * A host that cannot take the registration cannot hand the list back either, and is answered from
37
+ * the server alone — which is what it got before there was a channel to join.
38
+ */
39
+ var LanguageServerCompletionSources = class {
40
+ context;
41
+ sources;
42
+ registrations;
43
+ constructor(context, sources) {
44
+ this.context = context;
45
+ this.sources = sources;
46
+ this.registrations = sources.flatMap((source) => {
47
+ const registration = context.registerProvider(EDITOR_COMPLETION_SOURCE, { language: "*" }, source);
48
+ return registration ? [registration] : [];
49
+ });
50
+ }
51
+ forLanguage(languageId) {
52
+ const registered = this.context.getProviders(EDITOR_COMPLETION_SOURCE, languageId);
53
+ if (!registered) return this.sources;
54
+ const missing = this.sources.filter((source) => !registered.includes(source));
55
+ return registered.concat(missing);
56
+ }
57
+ dispose() {
58
+ for (const registration of this.registrations) registration.dispose();
59
+ }
60
+ };
61
+ //#endregion
62
+ export { LanguageServerCompletionSources, createLanguageServerCompletionSource };
63
+
64
+ //# sourceMappingURL=completionProviders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completionProviders.js","names":[],"sources":["../src/completionProviders.ts"],"sourcesContent":["import {\n createEditorLanguageFeatureToken,\n type EditorDisposable,\n type EditorViewContributionContext,\n type EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport {\n offsetToLspPositionInSnapshot,\n type LspClient,\n type LspTextDocumentSnapshot,\n} from '@singapore-editor/lsp'\nimport type * as lsp from 'vscode-languageserver-protocol'\n\nimport { completionNeedsResolve, type LanguageServerCompletionTrigger } from './completion'\n\n/**\n * Named rather than imported by everyone who answers it: a snippet set, a path completer or a\n * second server has no reason to depend on this package, and restating the id is how they reach the\n * same list.\n */\nconst EDITOR_COMPLETION_SOURCE_ID = 'editor.completionSource'\n\n/** What is being completed, in the text the caller has, not in a protocol position. */\nexport type EditorCompletionRequest = {\n readonly uri: string\n readonly document: LspTextDocumentSnapshot\n readonly offset: number\n readonly trigger: LanguageServerCompletionTrigger\n readonly signal: AbortSignal\n}\n\n/**\n * What a source hands back: the protocol's own shapes, so a source that already speaks to a server\n * returns the answer as it came and a source that does not can hand over a plain array.\n */\nexport type EditorCompletionAnswer =\n | lsp.CompletionList\n | readonly lsp.CompletionItem[]\n | null\n | undefined\n\nexport type EditorCompletionSource = {\n provideCompletionItems(\n request: EditorCompletionRequest,\n ): PromiseLike<EditorCompletionAnswer> | EditorCompletionAnswer\n /**\n * The rest of an item, for a source that sends a cheap one first, or null when this item is\n * already whole. Judged here because only the source knows what it left out.\n */\n resolveCompletionItem?(item: lsp.CompletionItem): PromiseLike<lsp.CompletionItem> | null\n}\n\nconst EDITOR_COMPLETION_SOURCE = createEditorLanguageFeatureToken<EditorCompletionSource>(\n EDITOR_COMPLETION_SOURCE_ID,\n)\n\n/** The sources a document is answered from, in the order they are to be asked. */\nexport type EditorCompletionSourceSet = {\n forLanguage(languageId: EditorViewSnapshot['languageId']): readonly EditorCompletionSource[]\n}\n\nexport function createLanguageServerCompletionSource(\n client: LspClient,\n isAvailable: () => boolean = () => true,\n onRequestSuccess?: () => void,\n onRequestError?: (method: string, error: unknown) => void,\n): EditorCompletionSource {\n return {\n // The server's answer is handed on as it arrives rather than repackaged, so nothing stands\n // between the reply and the list but the merge the caller does anyway.\n provideCompletionItems: (request) => {\n // Asking a connection that has not finished handshaking would spend a round-trip to be told\n // so; the other sources answer meanwhile.\n if (!client.initialized || !isAvailable()) return null\n\n return client\n .request<EditorCompletionAnswer>(\n 'textDocument/completion',\n {\n textDocument: { uri: request.uri },\n position: offsetToLspPositionInSnapshot(request.document, request.offset),\n context: request.trigger,\n } satisfies lsp.CompletionParams,\n { signal: request.signal },\n )\n .then(\n (answer) => {\n onRequestSuccess?.()\n return answer\n },\n (error: unknown) => {\n onRequestError?.('textDocument/completion', error)\n return null\n },\n )\n },\n // Most servers send an item without its import edit and expect a resolve round-trip; applying\n // the unresolved item is what silently drops auto-imports.\n resolveCompletionItem: (item) =>\n completionNeedsResolve(item, client.serverCapabilities)\n ? client\n .request<lsp.CompletionItem>('completionItem/resolve', item)\n .then(undefined, (error) => {\n onRequestError?.('completionItem/resolve', error)\n return item\n })\n : null,\n }\n}\n\n/**\n * The sources a completion request is answered from.\n *\n * The server's own goes into the editor's channel rather than being called directly, so that a\n * snippet set or a second server registered for the same feature is asked in one order and lands in\n * one list. The registration is held here because the connection it speaks for is: a source outlives\n * neither.\n *\n * A host that cannot take the registration cannot hand the list back either, and is answered from\n * the server alone — which is what it got before there was a channel to join.\n */\nexport class LanguageServerCompletionSources\n implements EditorCompletionSourceSet, EditorDisposable\n{\n private readonly registrations: readonly EditorDisposable[]\n\n public constructor(\n private readonly context: EditorViewContributionContext,\n private readonly sources: readonly EditorCompletionSource[],\n ) {\n // Every document, because which ones a server answers for is a question the plugin settles by\n // opening them, not one a language name can be matched against.\n this.registrations = sources.flatMap((source) => {\n const registration = context.registerProvider(\n EDITOR_COMPLETION_SOURCE,\n { language: '*' },\n source,\n )\n return registration ? [registration] : []\n })\n }\n\n public forLanguage(\n languageId: EditorViewSnapshot['languageId'],\n ): readonly EditorCompletionSource[] {\n const registered = this.context.getProviders(EDITOR_COMPLETION_SOURCE, languageId)\n if (!registered) return this.sources\n\n const missing = this.sources.filter((source) => !registered.includes(source))\n return registered.concat(missing)\n }\n\n public dispose(): void {\n for (const registration of this.registrations) registration.dispose()\n }\n}\n"],"mappings":";;;;AAoDA,IAAM,2BAA2B,iCAC/B,yBACF;AAOA,SAAgB,qCACd,QACA,oBAAmC,MACnC,kBACA,gBACwB;CACxB,OAAO;EAGL,yBAAyB,YAAY;GAGnC,IAAI,CAAC,OAAO,eAAe,CAAC,YAAY,GAAG,OAAO;GAElD,OAAO,OACJ,QACC,2BACA;IACE,cAAc,EAAE,KAAK,QAAQ,IAAI;IACjC,UAAU,8BAA8B,QAAQ,UAAU,QAAQ,MAAM;IACxE,SAAS,QAAQ;GACnB,GACA,EAAE,QAAQ,QAAQ,OAAO,CAC3B,CAAC,CACA,MACE,WAAW;IACV,mBAAmB;IACnB,OAAO;GACT,IACC,UAAmB;IAClB,iBAAiB,2BAA2B,KAAK;IACjD,OAAO;GACT,CACF;EACJ;EAGA,wBAAwB,SACtB,uBAAuB,MAAM,OAAO,kBAAkB,IAClD,OACG,QAA4B,0BAA0B,IAAI,CAAC,CAC3D,KAAK,KAAA,IAAY,UAAU;GAC1B,iBAAiB,0BAA0B,KAAK;GAChD,OAAO;EACT,CAAC,IACH;CACR;AACF;;;;;;;;;;;;AAaA,IAAa,kCAAb,MAEA;CAIqB;CACA;CAJnB;CAEA,YACE,SACA,SACA;EAFiB,KAAA,UAAA;EACA,KAAA,UAAA;EAIjB,KAAK,gBAAgB,QAAQ,SAAS,WAAW;GAC/C,MAAM,eAAe,QAAQ,iBAC3B,0BACA,EAAE,UAAU,IAAI,GAChB,MACF;GACA,OAAO,eAAe,CAAC,YAAY,IAAI,CAAC;EAC1C,CAAC;CACH;CAEA,YACE,YACmC;EACnC,MAAM,aAAa,KAAK,QAAQ,aAAa,0BAA0B,UAAU;EACjF,IAAI,CAAC,YAAY,OAAO,KAAK;EAE7B,MAAM,UAAU,KAAK,QAAQ,QAAQ,WAAW,CAAC,WAAW,SAAS,MAAM,CAAC;EAC5E,OAAO,WAAW,OAAO,OAAO;CAClC;CAEA,UAAuB;EACrB,KAAK,MAAM,gBAAgB,KAAK,eAAe,aAAa,QAAQ;CACtE;AACF"}
@@ -0,0 +1,7 @@
1
+ import type { LspClient, LspWorkspace } from '@singapore-editor/lsp';
2
+ /** The connection a host is handed as soon as it exists. */
3
+ export type LanguageServerConnectionContext = {
4
+ readonly client: LspClient;
5
+ readonly workspace: LspWorkspace;
6
+ };
7
+ //# sourceMappingURL=connectionContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectionContext.d.ts","sourceRoot":"","sources":["../src/connectionContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAEpE,4DAA4D;AAC5D,MAAM,MAAM,+BAA+B,GAAG;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAA;CACjC,CAAA"}
@@ -0,0 +1,54 @@
1
+ import type { EditorViewContributionContext, EditorViewContributionUpdateKind, EditorViewSnapshot } from '@singapore-editor/core/extensions';
2
+ import type { ActiveDocument, LanguageServerNavigationCommand } from './pluginTypes';
3
+ import type { LanguageServerFeatureRouter } from './serverSet';
4
+ import type { LanguageServerDefinitionTarget, LanguageServerNavigationOptions, LanguageServerReferencesResult } from './types';
5
+ export type DefinitionLinkControllerOptions = {
6
+ readonly context: EditorViewContributionContext;
7
+ readonly router: LanguageServerFeatureRouter;
8
+ readonly linkHighlightNameNamespace?: string;
9
+ readonly navigationTimingNamePrefix?: string;
10
+ getActiveDocument(): ActiveDocument | null;
11
+ onDefinitionLinkHover?(target: LanguageServerDefinitionTarget): void;
12
+ onOpenDefinition?(target: LanguageServerDefinitionTarget, options?: LanguageServerNavigationOptions): void | boolean;
13
+ onOpenReferences?(result: LanguageServerReferencesResult): void | boolean;
14
+ onRequestError(error: unknown): void;
15
+ };
16
+ /**
17
+ * Go-to-definition: the modifier-held link under the pointer, the modified click that follows it,
18
+ * and the navigation commands. Hover content is a participant in the shared hover instead; see
19
+ * hoverParticipant.ts.
20
+ */
21
+ export declare class DefinitionLinkController {
22
+ private readonly options;
23
+ private readonly context;
24
+ private readonly router;
25
+ private readonly linkHighlightName;
26
+ private definitionRequestId;
27
+ private definitionHoverRequestId;
28
+ private lastPointerOffset;
29
+ private linkRange;
30
+ private disposed;
31
+ private pressParticipant;
32
+ constructor(options: DefinitionLinkControllerOptions);
33
+ update(_snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void;
34
+ runNavigationCommand(command: LanguageServerNavigationCommand): boolean;
35
+ clearPointerUi(): void;
36
+ dispose(): void;
37
+ private installHandlers;
38
+ private uninstallHandlers;
39
+ private readonly handlePointerMove;
40
+ private readonly handlePointerLeave;
41
+ private readonly claimNavigationPress;
42
+ private readonly handleKeyDown;
43
+ private readonly handleKeyUp;
44
+ private requestNavigationAtOffset;
45
+ private requestDefinitionLink;
46
+ private renderDefinitionLink;
47
+ private handleNavigationResult;
48
+ private handleReferencesResult;
49
+ private openNavigationTarget;
50
+ private navigationTimingName;
51
+ private openDefinitionTarget;
52
+ private clearDefinitionLink;
53
+ }
54
+ //# sourceMappingURL=definitionLinkController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitionLinkController.d.ts","sourceRoot":"","sources":["../src/definitionLinkController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,6BAA6B,EAC7B,gCAAgC,EAChC,kBAAkB,EACnB,MAAM,mCAAmC,CAAA;AAe1C,OAAO,KAAK,EAAE,cAAc,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,KAAK,EACV,8BAA8B,EAE9B,+BAA+B,EAC/B,8BAA8B,EAC/B,MAAM,SAAS,CAAA;AAEhB,MAAM,MAAM,+BAA+B,GAAG;IAC5C,QAAQ,CAAC,OAAO,EAAE,6BAA6B,CAAA;IAC/C,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAA;IAC5C,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IAC5C,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IAC5C,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAAA;IAC1C,qBAAqB,CAAC,CAAC,MAAM,EAAE,8BAA8B,GAAG,IAAI,CAAA;IACpE,gBAAgB,CAAC,CACf,MAAM,EAAE,8BAA8B,EACtC,OAAO,CAAC,EAAE,+BAA+B,GACxC,IAAI,GAAG,OAAO,CAAA;IACjB,gBAAgB,CAAC,CAAC,MAAM,EAAE,8BAA8B,GAAG,IAAI,GAAG,OAAO,CAAA;IACzE,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;CACrC,CAAA;AAED;;;;GAIG;AACH,qBAAa,wBAAwB;IAWhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,wBAAwB,CAAI;IACpC,OAAO,CAAC,iBAAiB,CAAsB;IAC/C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,gBAAgB,CAAgC;gBAEpB,OAAO,EAAE,+BAA+B;IAOrE,MAAM,CAAC,SAAS,EAAE,kBAAkB,EAAE,IAAI,EAAE,gCAAgC,GAAG,IAAI;IAInF,oBAAoB,CAAC,OAAO,EAAE,+BAA+B,GAAG,OAAO;IAMvE,cAAc,IAAI,IAAI;IAItB,OAAO,IAAI,IAAI;IAQtB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAWjC;IAED,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAGlC;IAED,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAUpC;IAED,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK7B;IAED,OAAO,CAAC,QAAQ,CAAC,WAAW,CAI3B;IAED,OAAO,CAAC,yBAAyB;IAuBjC,OAAO,CAAC,qBAAqB;IAsB7B,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,sBAAsB;IAoB9B,OAAO,CAAC,sBAAsB;IAmB9B,OAAO,CAAC,oBAAoB;IAqB5B,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,mBAAmB;CAM5B"}
@@ -0,0 +1,221 @@
1
+ import { rangeAroundOffset } from "./sourceText.js";
2
+ import { LINK_HIGHLIGHT_STYLE } from "./plugin.styles.js";
3
+ import { navigateToTarget, preferredDefinitionTarget, preferredJumpableDefinitionTarget, preferredReferenceTarget, requestDefinition, requestNavigationTargets } from "./definitionNavigation.js";
4
+ import { anchoredSurfaceFollowsUpdate } from "@singapore-editor/plugin-ui/anchored-surface";
5
+ import { hoverTargetRange } from "@singapore-editor/plugin-ui/offset-range";
6
+ //#region src/definitionLinkController.ts
7
+ /**
8
+ * Go-to-definition: the modifier-held link under the pointer, the modified click that follows it,
9
+ * and the navigation commands. Hover content is a participant in the shared hover instead; see
10
+ * hoverParticipant.ts.
11
+ */
12
+ var DefinitionLinkController = class {
13
+ options;
14
+ context;
15
+ router;
16
+ linkHighlightName;
17
+ definitionRequestId = 0;
18
+ definitionHoverRequestId = 0;
19
+ lastPointerOffset = null;
20
+ linkRange = null;
21
+ disposed = false;
22
+ pressParticipant = null;
23
+ constructor(options) {
24
+ this.options = options;
25
+ this.context = options.context;
26
+ this.router = options.router;
27
+ this.linkHighlightName = definitionLinkHighlightName(this.context, options);
28
+ this.installHandlers();
29
+ }
30
+ update(_snapshot, kind) {
31
+ if (shouldClearLink(kind)) this.clearDefinitionLink();
32
+ }
33
+ runNavigationCommand(command) {
34
+ const selection = this.context.getSnapshot().selections[0];
35
+ if (!selection) return false;
36
+ return this.requestNavigationAtOffset(selection.headOffset, command);
37
+ }
38
+ clearPointerUi() {
39
+ this.clearDefinitionLink();
40
+ }
41
+ dispose() {
42
+ if (this.disposed) return;
43
+ this.disposed = true;
44
+ this.uninstallHandlers();
45
+ this.clearDefinitionLink();
46
+ }
47
+ installHandlers() {
48
+ this.context.scrollElement.addEventListener("pointermove", this.handlePointerMove);
49
+ this.context.scrollElement.addEventListener("pointerleave", this.handlePointerLeave);
50
+ this.pressParticipant = this.context.registerPressParticipant(this.claimNavigationPress);
51
+ this.context.container.ownerDocument.addEventListener("keydown", this.handleKeyDown);
52
+ this.context.container.ownerDocument.addEventListener("keyup", this.handleKeyUp);
53
+ }
54
+ uninstallHandlers() {
55
+ this.context.scrollElement.removeEventListener("pointermove", this.handlePointerMove);
56
+ this.context.scrollElement.removeEventListener("pointerleave", this.handlePointerLeave);
57
+ this.pressParticipant?.dispose();
58
+ this.pressParticipant = null;
59
+ this.context.container.ownerDocument.removeEventListener("keydown", this.handleKeyDown);
60
+ this.context.container.ownerDocument.removeEventListener("keyup", this.handleKeyUp);
61
+ }
62
+ handlePointerMove = (event) => {
63
+ if (event.buttons !== 0) return this.clearDefinitionLink();
64
+ if (!this.options.getActiveDocument()) return this.clearDefinitionLink();
65
+ const offset = this.context.textOffsetFromPoint(event.clientX, event.clientY);
66
+ if (offset === null) return this.clearDefinitionLink();
67
+ this.lastPointerOffset = offset;
68
+ if (!isNavigationModifier(event)) return this.clearDefinitionLink();
69
+ this.requestDefinitionLink(offset);
70
+ };
71
+ handlePointerLeave = () => {
72
+ this.lastPointerOffset = null;
73
+ this.clearDefinitionLink();
74
+ };
75
+ claimNavigationPress = (event) => {
76
+ if (event.button !== 0) return false;
77
+ if (!isNavigationModifier(event)) return false;
78
+ const offset = this.context.textOffsetFromPoint(event.clientX, event.clientY);
79
+ if (offset === null) return false;
80
+ this.context.focusEditor();
81
+ this.requestNavigationAtOffset(offset, {
82
+ kind: "definition",
83
+ openMode: "default"
84
+ });
85
+ return true;
86
+ };
87
+ handleKeyDown = (event) => {
88
+ if (!isNavigationModifier(event)) return;
89
+ if (this.lastPointerOffset === null) return;
90
+ this.requestDefinitionLink(this.lastPointerOffset);
91
+ };
92
+ handleKeyUp = (event) => {
93
+ if (event.key !== "Meta" && event.key !== "Control") return;
94
+ this.clearDefinitionLink();
95
+ };
96
+ requestNavigationAtOffset(offset, command) {
97
+ const active = this.options.getActiveDocument();
98
+ if (!active) return false;
99
+ if (!this.router.hasReady("navigation", navigationMethod(command.kind))) return false;
100
+ this.clearDefinitionLink();
101
+ const requestId = this.definitionRequestId + 1;
102
+ this.definitionRequestId = requestId;
103
+ requestNavigationTargets(this.router, {
104
+ uri: active.uri,
105
+ document: active,
106
+ offset,
107
+ kind: command.kind,
108
+ includeDeclaration: command.includeDeclaration
109
+ }).then((result) => this.handleNavigationResult(requestId, active, offset, command, result)).catch((error) => this.options.onRequestError(error));
110
+ return true;
111
+ }
112
+ requestDefinitionLink(offset) {
113
+ const active = this.options.getActiveDocument();
114
+ if (!active) return this.clearDefinitionLink();
115
+ if (!this.router.hasReady("navigation", "textDocument/definition")) return this.clearDefinitionLink();
116
+ if (this.linkRange && offset >= this.linkRange.start && offset < this.linkRange.end) return;
117
+ this.clearDefinitionLink();
118
+ const range = rangeAroundOffset(active, offset, hoverTargetRange);
119
+ const requestId = this.definitionHoverRequestId + 1;
120
+ this.definitionHoverRequestId = requestId;
121
+ requestDefinition(this.router, {
122
+ uri: active.uri,
123
+ document: active,
124
+ offset
125
+ }).then((result) => this.renderDefinitionLink(requestId, active, range, result)).catch((error) => this.options.onRequestError(error));
126
+ }
127
+ renderDefinitionLink(requestId, active, range, result) {
128
+ if (requestId !== this.definitionHoverRequestId) return;
129
+ if (active !== this.options.getActiveDocument()) return;
130
+ const sourceRange = result.sourceRange ?? range;
131
+ const target = preferredJumpableDefinitionTarget(active.uri, active, sourceRange, result);
132
+ if (!target) return this.clearDefinitionLink();
133
+ this.linkRange = sourceRange;
134
+ this.context.setRangeHighlight(this.linkHighlightName, [sourceRange], LINK_HIGHLIGHT_STYLE);
135
+ this.context.scrollElement.style.cursor = "pointer";
136
+ this.options.onDefinitionLinkHover?.(target);
137
+ }
138
+ handleNavigationResult(requestId, active, offset, command, result) {
139
+ if (requestId !== this.definitionRequestId) return;
140
+ if (active !== this.options.getActiveDocument()) return;
141
+ if (command.kind === "references") {
142
+ this.handleReferencesResult(active, offset, result);
143
+ return;
144
+ }
145
+ const target = preferredDefinitionTarget(active.uri, result);
146
+ if (!target) return;
147
+ this.openNavigationTarget(active, target, command);
148
+ }
149
+ handleReferencesResult(active, offset, result) {
150
+ if (this.options.onOpenReferences?.({
151
+ uri: active.uri,
152
+ targets: result.targets
153
+ })) return;
154
+ const target = preferredReferenceTarget(active.uri, active, offset, result);
155
+ if (!target) return;
156
+ this.openNavigationTarget(active, target, {
157
+ kind: "references",
158
+ openMode: "peek"
159
+ });
160
+ }
161
+ openNavigationTarget(active, target, command) {
162
+ if (target.uri !== active.uri || command.openMode !== "default" ? this.openDefinitionTarget(target, command) : false) return;
163
+ if (target.uri !== active.uri) return;
164
+ navigateToTarget(target, {
165
+ document: active,
166
+ setSelection: this.context.setSelection.bind(this.context),
167
+ focusEditor: this.context.focusEditor.bind(this.context)
168
+ }, this.navigationTimingName(command.kind));
169
+ }
170
+ navigationTimingName(kind) {
171
+ const prefix = this.options.navigationTimingNamePrefix ?? "lspPlugin";
172
+ if (kind === "typeDefinition") return `${prefix}.goToTypeDefinition`;
173
+ return `${prefix}.goTo${capitalize(kind)}`;
174
+ }
175
+ openDefinitionTarget(target, command) {
176
+ const options = defaultDefinitionOptions(command);
177
+ if (!options) return this.options.onOpenDefinition?.(target);
178
+ return this.options.onOpenDefinition?.(target, options);
179
+ }
180
+ clearDefinitionLink() {
181
+ this.definitionHoverRequestId += 1;
182
+ this.linkRange = null;
183
+ this.context.clearRangeHighlight(this.linkHighlightName);
184
+ this.context.scrollElement.style.cursor = "";
185
+ }
186
+ };
187
+ function navigationMethod(kind) {
188
+ if (kind === "references") return "textDocument/references";
189
+ if (kind === "implementation") return "textDocument/implementation";
190
+ if (kind === "typeDefinition") return "textDocument/typeDefinition";
191
+ return "textDocument/definition";
192
+ }
193
+ function defaultDefinitionOptions(command) {
194
+ if (command.kind === "definition" && command.openMode === "default") return null;
195
+ return {
196
+ kind: command.kind,
197
+ openMode: command.openMode
198
+ };
199
+ }
200
+ /**
201
+ * A link is keyed to the pointer: once the text has slid out from under a pointer that never
202
+ * moved, the underline points at the wrong word, so it goes with the old frame.
203
+ */
204
+ function shouldClearLink(kind) {
205
+ if (anchoredSurfaceFollowsUpdate(kind)) return true;
206
+ return kind === "content" || kind === "document" || kind === "clear";
207
+ }
208
+ function isNavigationModifier(event) {
209
+ return event.metaKey || event.ctrlKey;
210
+ }
211
+ function capitalize(value) {
212
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
213
+ }
214
+ function definitionLinkHighlightName(context, options) {
215
+ const namespace = options.linkHighlightNameNamespace ?? "lsp-plugin";
216
+ return `${context.highlightPrefix}-${namespace}-definition-link`;
217
+ }
218
+ //#endregion
219
+ export { DefinitionLinkController };
220
+
221
+ //# sourceMappingURL=definitionLinkController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitionLinkController.js","names":[],"sources":["../src/definitionLinkController.ts"],"sourcesContent":["import type {\n EditorDisposable,\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport { anchoredSurfaceFollowsUpdate } from '@singapore-editor/plugin-ui/anchored-surface'\nimport { type OffsetRange, hoverTargetRange } from '@singapore-editor/plugin-ui/offset-range'\n\nimport {\n navigateToTarget,\n preferredDefinitionTarget,\n preferredJumpableDefinitionTarget,\n preferredReferenceTarget,\n requestDefinition,\n requestNavigationTargets,\n type DefinitionResult,\n} from './definitionNavigation'\nimport { LINK_HIGHLIGHT_STYLE } from './plugin.styles'\nimport { rangeAroundOffset } from './sourceText'\nimport type { ActiveDocument, LanguageServerNavigationCommand } from './pluginTypes'\nimport type { LanguageServerFeatureRouter } from './serverSet'\nimport type {\n LanguageServerDefinitionTarget,\n LanguageServerNavigationKind,\n LanguageServerNavigationOptions,\n LanguageServerReferencesResult,\n} from './types'\n\nexport type DefinitionLinkControllerOptions = {\n readonly context: EditorViewContributionContext\n readonly router: LanguageServerFeatureRouter\n readonly linkHighlightNameNamespace?: string\n readonly navigationTimingNamePrefix?: string\n getActiveDocument(): ActiveDocument | null\n onDefinitionLinkHover?(target: LanguageServerDefinitionTarget): void\n onOpenDefinition?(\n target: LanguageServerDefinitionTarget,\n options?: LanguageServerNavigationOptions,\n ): void | boolean\n onOpenReferences?(result: LanguageServerReferencesResult): void | boolean\n onRequestError(error: unknown): void\n}\n\n/**\n * Go-to-definition: the modifier-held link under the pointer, the modified click that follows it,\n * and the navigation commands. Hover content is a participant in the shared hover instead; see\n * hoverParticipant.ts.\n */\nexport class DefinitionLinkController {\n private readonly context: EditorViewContributionContext\n private readonly router: LanguageServerFeatureRouter\n private readonly linkHighlightName: string\n private definitionRequestId = 0\n private definitionHoverRequestId = 0\n private lastPointerOffset: number | null = null\n private linkRange: OffsetRange | null = null\n private disposed = false\n private pressParticipant: EditorDisposable | null = null\n\n public constructor(private readonly options: DefinitionLinkControllerOptions) {\n this.context = options.context\n this.router = options.router\n this.linkHighlightName = definitionLinkHighlightName(this.context, options)\n this.installHandlers()\n }\n\n public update(_snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void {\n if (shouldClearLink(kind)) this.clearDefinitionLink()\n }\n\n public runNavigationCommand(command: LanguageServerNavigationCommand): boolean {\n const selection = this.context.getSnapshot().selections[0]\n if (!selection) return false\n return this.requestNavigationAtOffset(selection.headOffset, command)\n }\n\n public clearPointerUi(): void {\n this.clearDefinitionLink()\n }\n\n public dispose(): void {\n if (this.disposed) return\n\n this.disposed = true\n this.uninstallHandlers()\n this.clearDefinitionLink()\n }\n\n private installHandlers(): void {\n this.context.scrollElement.addEventListener('pointermove', this.handlePointerMove)\n this.context.scrollElement.addEventListener('pointerleave', this.handlePointerLeave)\n this.pressParticipant = this.context.registerPressParticipant(this.claimNavigationPress)\n this.context.container.ownerDocument.addEventListener('keydown', this.handleKeyDown)\n this.context.container.ownerDocument.addEventListener('keyup', this.handleKeyUp)\n }\n\n private uninstallHandlers(): void {\n this.context.scrollElement.removeEventListener('pointermove', this.handlePointerMove)\n this.context.scrollElement.removeEventListener('pointerleave', this.handlePointerLeave)\n this.pressParticipant?.dispose()\n this.pressParticipant = null\n this.context.container.ownerDocument.removeEventListener('keydown', this.handleKeyDown)\n this.context.container.ownerDocument.removeEventListener('keyup', this.handleKeyUp)\n }\n\n private readonly handlePointerMove = (event: PointerEvent): void => {\n if (event.buttons !== 0) return this.clearDefinitionLink()\n if (!this.options.getActiveDocument()) return this.clearDefinitionLink()\n\n const offset = this.context.textOffsetFromPoint(event.clientX, event.clientY)\n if (offset === null) return this.clearDefinitionLink()\n\n this.lastPointerOffset = offset\n if (!isNavigationModifier(event)) return this.clearDefinitionLink()\n\n this.requestDefinitionLink(offset)\n }\n\n private readonly handlePointerLeave = (): void => {\n this.lastPointerOffset = null\n this.clearDefinitionLink()\n }\n\n private readonly claimNavigationPress = (event: MouseEvent): boolean => {\n if (event.button !== 0) return false\n if (!isNavigationModifier(event)) return false\n\n const offset = this.context.textOffsetFromPoint(event.clientX, event.clientY)\n if (offset === null) return false\n\n this.context.focusEditor()\n this.requestNavigationAtOffset(offset, { kind: 'definition', openMode: 'default' })\n return true\n }\n\n private readonly handleKeyDown = (event: KeyboardEvent): void => {\n if (!isNavigationModifier(event)) return\n if (this.lastPointerOffset === null) return\n\n this.requestDefinitionLink(this.lastPointerOffset)\n }\n\n private readonly handleKeyUp = (event: KeyboardEvent): void => {\n if (event.key !== 'Meta' && event.key !== 'Control') return\n\n this.clearDefinitionLink()\n }\n\n private requestNavigationAtOffset(\n offset: number,\n command: LanguageServerNavigationCommand,\n ): boolean {\n const active = this.options.getActiveDocument()\n if (!active) return false\n if (!this.router.hasReady('navigation', navigationMethod(command.kind))) return false\n\n this.clearDefinitionLink()\n const requestId = this.definitionRequestId + 1\n this.definitionRequestId = requestId\n void requestNavigationTargets(this.router, {\n uri: active.uri,\n document: active,\n offset,\n kind: command.kind,\n includeDeclaration: command.includeDeclaration,\n })\n .then((result) => this.handleNavigationResult(requestId, active, offset, command, result))\n .catch((error: unknown) => this.options.onRequestError(error))\n return true\n }\n\n private requestDefinitionLink(offset: number): void {\n const active = this.options.getActiveDocument()\n if (!active) return this.clearDefinitionLink()\n if (!this.router.hasReady('navigation', 'textDocument/definition')) {\n return this.clearDefinitionLink()\n }\n\n if (this.linkRange && offset >= this.linkRange.start && offset < this.linkRange.end) return\n this.clearDefinitionLink()\n const range = rangeAroundOffset(active, offset, hoverTargetRange)\n\n const requestId = this.definitionHoverRequestId + 1\n this.definitionHoverRequestId = requestId\n void requestDefinition(this.router, {\n uri: active.uri,\n document: active,\n offset,\n })\n .then((result) => this.renderDefinitionLink(requestId, active, range, result))\n .catch((error: unknown) => this.options.onRequestError(error))\n }\n\n private renderDefinitionLink(\n requestId: number,\n active: ActiveDocument,\n range: OffsetRange,\n result: DefinitionResult,\n ): void {\n if (requestId !== this.definitionHoverRequestId) return\n if (active !== this.options.getActiveDocument()) return\n const sourceRange = result.sourceRange ?? range\n const target = preferredJumpableDefinitionTarget(active.uri, active, sourceRange, result)\n if (!target) return this.clearDefinitionLink()\n\n this.linkRange = sourceRange\n this.context.setRangeHighlight(this.linkHighlightName, [sourceRange], LINK_HIGHLIGHT_STYLE)\n this.context.scrollElement.style.cursor = 'pointer'\n this.options.onDefinitionLinkHover?.(target)\n }\n\n private handleNavigationResult(\n requestId: number,\n active: ActiveDocument,\n offset: number,\n command: LanguageServerNavigationCommand,\n result: DefinitionResult,\n ): void {\n if (requestId !== this.definitionRequestId) return\n if (active !== this.options.getActiveDocument()) return\n\n if (command.kind === 'references') {\n this.handleReferencesResult(active, offset, result)\n return\n }\n\n const target = preferredDefinitionTarget(active.uri, result)\n if (!target) return\n this.openNavigationTarget(active, target, command)\n }\n\n private handleReferencesResult(\n active: ActiveDocument,\n offset: number,\n result: DefinitionResult,\n ): void {\n const handled = this.options.onOpenReferences?.({\n uri: active.uri,\n targets: result.targets,\n })\n if (handled) return\n\n const target = preferredReferenceTarget(active.uri, active, offset, result)\n if (!target) return\n this.openNavigationTarget(active, target, {\n kind: 'references',\n openMode: 'peek',\n })\n }\n\n private openNavigationTarget(\n active: ActiveDocument,\n target: LanguageServerDefinitionTarget,\n command: LanguageServerNavigationCommand,\n ): void {\n const shouldOfferExternalOpen = target.uri !== active.uri || command.openMode !== 'default'\n const handled = shouldOfferExternalOpen ? this.openDefinitionTarget(target, command) : false\n if (handled) return\n if (target.uri !== active.uri) return\n\n navigateToTarget(\n target,\n {\n document: active,\n setSelection: this.context.setSelection.bind(this.context),\n focusEditor: this.context.focusEditor.bind(this.context),\n },\n this.navigationTimingName(command.kind),\n )\n }\n\n private navigationTimingName(kind: LanguageServerNavigationKind): string {\n const prefix = this.options.navigationTimingNamePrefix ?? 'lspPlugin'\n if (kind === 'typeDefinition') return `${prefix}.goToTypeDefinition`\n return `${prefix}.goTo${capitalize(kind)}`\n }\n\n private openDefinitionTarget(\n target: LanguageServerDefinitionTarget,\n command: LanguageServerNavigationCommand,\n ): void | boolean {\n const options = defaultDefinitionOptions(command)\n if (!options) return this.options.onOpenDefinition?.(target)\n return this.options.onOpenDefinition?.(target, options)\n }\n\n private clearDefinitionLink(): void {\n this.definitionHoverRequestId += 1\n this.linkRange = null\n this.context.clearRangeHighlight(this.linkHighlightName)\n this.context.scrollElement.style.cursor = ''\n }\n}\n\nfunction navigationMethod(kind: LanguageServerNavigationKind): string {\n if (kind === 'references') return 'textDocument/references'\n if (kind === 'implementation') return 'textDocument/implementation'\n if (kind === 'typeDefinition') return 'textDocument/typeDefinition'\n return 'textDocument/definition'\n}\n\nfunction defaultDefinitionOptions(\n command: LanguageServerNavigationCommand,\n): LanguageServerNavigationOptions | null {\n if (command.kind === 'definition' && command.openMode === 'default') return null\n\n return {\n kind: command.kind,\n openMode: command.openMode,\n }\n}\n\n/**\n * A link is keyed to the pointer: once the text has slid out from under a pointer that never\n * moved, the underline points at the wrong word, so it goes with the old frame.\n */\nfunction shouldClearLink(kind: EditorViewContributionUpdateKind): boolean {\n if (anchoredSurfaceFollowsUpdate(kind)) return true\n return kind === 'content' || kind === 'document' || kind === 'clear'\n}\n\nfunction isNavigationModifier(event: {\n readonly metaKey: boolean\n readonly ctrlKey: boolean\n}): boolean {\n return event.metaKey || event.ctrlKey\n}\n\nfunction capitalize(value: string): string {\n return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`\n}\n\nfunction definitionLinkHighlightName(\n context: EditorViewContributionContext,\n options: DefinitionLinkControllerOptions,\n): string {\n const namespace = options.linkHighlightNameNamespace ?? 'lsp-plugin'\n return `${context.highlightPrefix}-${namespace}-definition-link`\n}\n"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import { type LspTextDocumentSnapshot } from '@singapore-editor/lsp';
2
+ import { type OffsetRange } from '@singapore-editor/plugin-ui/offset-range';
3
+ import type { EditorSetSelectionOptions } from '@singapore-editor/core/editor';
4
+ import type * as lsp from 'vscode-languageserver-protocol';
5
+ import type { LanguageServerFeatureRouter } from './serverSet';
6
+ import type { LanguageServerDefinitionTarget, LanguageServerNavigationKind } from './types';
7
+ /**
8
+ * Inputs required to issue a `textDocument/definition` request against the
9
+ * LSP client. The request is expressed in editor-native coordinates
10
+ * (`offset` into `document`) and converted to an LSP `Position` internally.
11
+ */
12
+ export type DefinitionRequest = {
13
+ readonly uri: lsp.DocumentUri;
14
+ readonly document: LspTextDocumentSnapshot;
15
+ readonly offset: number;
16
+ readonly signal?: AbortSignal;
17
+ };
18
+ export type NavigationRequest = DefinitionRequest & {
19
+ readonly kind: LanguageServerNavigationKind;
20
+ readonly includeDeclaration?: boolean;
21
+ };
22
+ /**
23
+ * Normalized response from {@link requestDefinition}. The LSP protocol
24
+ * permits a single `Location`, an array of `Location`, or an array of
25
+ * `LocationLink` (or `null`); callers should not have to care which shape
26
+ * was returned, so the raw result is collapsed into a flat list of
27
+ * resolvable targets.
28
+ */
29
+ export type DefinitionResult = {
30
+ readonly targets: readonly LanguageServerDefinitionTarget[];
31
+ readonly sourceRange?: OffsetRange;
32
+ };
33
+ /**
34
+ * Minimum editor surface required by {@link navigateToDefinition}. Matches
35
+ * the corresponding subset of `EditorViewContributionContext` so the
36
+ * contribution can pass its `context` through directly while keeping this
37
+ * module decoupled from `@singapore-editor/core`'s full contribution surface.
38
+ */
39
+ export type NavigationEditor = {
40
+ readonly document: LspTextDocumentSnapshot;
41
+ setSelection(anchor: number, head: number, timingName: string, options?: EditorSetSelectionOptions): void;
42
+ focusEditor(): void;
43
+ };
44
+ /**
45
+ * Issue a `textDocument/definition` request and normalize the response
46
+ * into a flat list of {@link LanguageServerDefinitionTarget}s. The raw LSP
47
+ * union (`Location`, `Location[]`, `LocationLink[]`, or `null`) is hidden
48
+ * from callers so higher-level orchestration can work with a single shape.
49
+ */
50
+ export declare function requestDefinition(client: LanguageServerFeatureRouter, request: DefinitionRequest): Promise<DefinitionResult>;
51
+ export declare function requestNavigationTargets(client: LanguageServerFeatureRouter, request: NavigationRequest): Promise<DefinitionResult>;
52
+ /**
53
+ * Navigate the editor to `target` by translating its LSP range into
54
+ * offsets in the editor's current text and applying the selection. The
55
+ * caller is responsible for ensuring `editor.document` is the text of the same
56
+ * document `target` refers to (i.e. a same-document jump); cross-document
57
+ * jumps should be routed through `onOpenDefinition`, not this function.
58
+ */
59
+ export declare function navigateToDefinition(target: LanguageServerDefinitionTarget, editor: NavigationEditor): void;
60
+ export declare function navigateToTarget(target: LanguageServerDefinitionTarget, editor: NavigationEditor, timingName: string): void;
61
+ /**
62
+ * Pick the best target to jump to from a definition result. Prefers
63
+ * targets inside the active document, then targets outside `node_modules`,
64
+ * then any target; returns `null` when no targets were returned.
65
+ */
66
+ export declare function preferredDefinitionTarget(activeUri: lsp.DocumentUri, result: DefinitionResult): LanguageServerDefinitionTarget | null;
67
+ export declare function preferredReferenceTarget(activeUri: lsp.DocumentUri, activeDocument: LspTextDocumentSnapshot, sourceOffset: number, result: DefinitionResult): LanguageServerDefinitionTarget | null;
68
+ /**
69
+ * Pick the best target to render a ctrl/cmd-hover definition link for.
70
+ * Same preference rules as {@link preferredDefinitionTarget} but filters
71
+ * out targets that would point back at (or overlap with) the hovered
72
+ * identifier itself — the user would not be able to "jump" to the
73
+ * definition they are already looking at.
74
+ */
75
+ export declare function preferredJumpableDefinitionTarget(activeUri: lsp.DocumentUri, activeDocument: LspTextDocumentSnapshot, sourceRange: OffsetRange, result: DefinitionResult): LanguageServerDefinitionTarget | null;
76
+ //# sourceMappingURL=definitionNavigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitionNavigation.d.ts","sourceRoot":"","sources":["../src/definitionNavigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,KAAK,WAAW,EAA2B,MAAM,0CAA0C,CAAA;AACpG,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAC9E,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAI1D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,KAAK,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAE3F;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,WAAW,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAA;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;IAClD,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAA;IAC3C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CACtC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,8BAA8B,EAAE,CAAA;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CACnC,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAA;IAC1C,YAAY,CACV,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAAA;IACP,WAAW,IAAI,IAAI,CAAA;CACpB,CAAA;AAUD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,2BAA2B,EACnC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,EACnC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAa3B;AAoBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,8BAA8B,EACtC,MAAM,EAAE,gBAAgB,GACvB,IAAI,CAEN;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,8BAA8B,EACtC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,GACjB,IAAI,CAKN;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,GAAG,CAAC,WAAW,EAC1B,MAAM,EAAE,gBAAgB,GACvB,8BAA8B,GAAG,IAAI,CAEvC;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,GAAG,CAAC,WAAW,EAC1B,cAAc,EAAE,uBAAuB,EACvC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,gBAAgB,GACvB,8BAA8B,GAAG,IAAI,CAYvC;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,GAAG,CAAC,WAAW,EAC1B,cAAc,EAAE,uBAAuB,EACvC,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,gBAAgB,GACvB,8BAA8B,GAAG,IAAI,CAKvC"}