@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,26 @@
1
+ import type { LanguageServerDocumentSnapshot } from './types';
2
+ import type { DocumentSyncPoint, TextSnapshot } from '@singapore-editor/core/document';
3
+ import type { EditorDisposable } from '@singapore-editor/core/extensions';
4
+ import type { LspWorkspace } from '@singapore-editor/lsp';
5
+ import type * as lsp from 'vscode-languageserver-protocol';
6
+ import type { DocumentSync } from './documentSync';
7
+ export type LanguageServerDocumentUriTransition = {
8
+ readonly fromUri: lsp.DocumentUri;
9
+ readonly toUri: lsp.DocumentUri;
10
+ readonly textSnapshot: TextSnapshot;
11
+ readonly syncPoint: DocumentSyncPoint;
12
+ };
13
+ export type LanguageServerDocumentSyncControllerRegistration = {
14
+ readonly getSnapshot: () => LanguageServerDocumentSnapshot;
15
+ readonly sync: DocumentSync;
16
+ readonly workspace: LspWorkspace;
17
+ };
18
+ /**
19
+ * Projects a live document identity change through every mounted language-server lane immediately.
20
+ */
21
+ export declare class LanguageServerDocumentSyncController {
22
+ private readonly registrations;
23
+ register(registration: LanguageServerDocumentSyncControllerRegistration): EditorDisposable;
24
+ transitionDocumentUri(transition: LanguageServerDocumentUriTransition): void;
25
+ }
26
+ //# sourceMappingURL=documentSyncController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documentSyncController.d.ts","sourceRoot":"","sources":["../src/documentSyncController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,MAAM,MAAM,mCAAmC,GAAG;IAChD,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,WAAW,CAAA;IAC/B,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,gDAAgD,GAAG;IAC7D,QAAQ,CAAC,WAAW,EAAE,MAAM,8BAA8B,CAAA;IAC1D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAA;CACjC,CAAA;AAED;;GAEG;AACH,qBAAa,oCAAoC;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8D;IAErF,QAAQ,CACb,YAAY,EAAE,gDAAgD,GAC7D,gBAAgB;IAOZ,qBAAqB,CAAC,UAAU,EAAE,mCAAmC,GAAG,IAAI;CAcpF"}
@@ -0,0 +1,24 @@
1
+ //#region src/documentSyncController.ts
2
+ /**
3
+ * Projects a live document identity change through every mounted language-server lane immediately.
4
+ */
5
+ var LanguageServerDocumentSyncController = class {
6
+ registrations = /* @__PURE__ */ new Set();
7
+ register(registration) {
8
+ this.registrations.add(registration);
9
+ return { dispose: () => this.registrations.delete(registration) };
10
+ }
11
+ transitionDocumentUri(transition) {
12
+ const transitionedWorkspaces = /* @__PURE__ */ new Set();
13
+ for (const registration of this.registrations) {
14
+ if (transitionedWorkspaces.has(registration.workspace)) continue;
15
+ if (!registration.sync.transitionDocumentUri(registration.getSnapshot(), transition)) continue;
16
+ if (registration.sync.activeDocument?.uri !== transition.toUri) continue;
17
+ transitionedWorkspaces.add(registration.workspace);
18
+ }
19
+ }
20
+ };
21
+ //#endregion
22
+ export { LanguageServerDocumentSyncController };
23
+
24
+ //# sourceMappingURL=documentSyncController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documentSyncController.js","names":[],"sources":["../src/documentSyncController.ts"],"sourcesContent":["import type { LanguageServerDocumentSnapshot } from './types'\nimport type { DocumentSyncPoint, TextSnapshot } from '@singapore-editor/core/document'\nimport type { EditorDisposable } from '@singapore-editor/core/extensions'\nimport type { LspWorkspace } from '@singapore-editor/lsp'\nimport type * as lsp from 'vscode-languageserver-protocol'\n\nimport type { DocumentSync } from './documentSync'\n\nexport type LanguageServerDocumentUriTransition = {\n readonly fromUri: lsp.DocumentUri\n readonly toUri: lsp.DocumentUri\n readonly textSnapshot: TextSnapshot\n readonly syncPoint: DocumentSyncPoint\n}\n\nexport type LanguageServerDocumentSyncControllerRegistration = {\n readonly getSnapshot: () => LanguageServerDocumentSnapshot\n readonly sync: DocumentSync\n readonly workspace: LspWorkspace\n}\n\n/**\n * Projects a live document identity change through every mounted language-server lane immediately.\n */\nexport class LanguageServerDocumentSyncController {\n private readonly registrations = new Set<LanguageServerDocumentSyncControllerRegistration>()\n\n public register(\n registration: LanguageServerDocumentSyncControllerRegistration,\n ): EditorDisposable {\n this.registrations.add(registration)\n return {\n dispose: () => this.registrations.delete(registration),\n }\n }\n\n public transitionDocumentUri(transition: LanguageServerDocumentUriTransition): void {\n const transitionedWorkspaces = new Set<LspWorkspace>()\n for (const registration of this.registrations) {\n if (transitionedWorkspaces.has(registration.workspace)) continue\n\n const handled = registration.sync.transitionDocumentUri(\n registration.getSnapshot(),\n transition,\n )\n if (!handled) continue\n if (registration.sync.activeDocument?.uri !== transition.toUri) continue\n transitionedWorkspaces.add(registration.workspace)\n }\n }\n}\n"],"mappings":";;;;AAwBA,IAAa,uCAAb,MAAkD;CAChD,gCAAiC,IAAI,IAAsD;CAE3F,SACE,cACkB;EAClB,KAAK,cAAc,IAAI,YAAY;EACnC,OAAO,EACL,eAAe,KAAK,cAAc,OAAO,YAAY,EACvD;CACF;CAEA,sBAA6B,YAAuD;EAClF,MAAM,yCAAyB,IAAI,IAAkB;EACrD,KAAK,MAAM,gBAAgB,KAAK,eAAe;GAC7C,IAAI,uBAAuB,IAAI,aAAa,SAAS,GAAG;GAMxD,IAAI,CAJY,aAAa,KAAK,sBAChC,aAAa,YAAY,GACzB,UAEG,GAAS;GACd,IAAI,aAAa,KAAK,gBAAgB,QAAQ,WAAW,OAAO;GAChE,uBAAuB,IAAI,aAAa,SAAS;EACnD;CACF;AACF"}
@@ -0,0 +1,43 @@
1
+ import { type EditorContributionChange, type EditorCapabilityToken, type EditorViewContributionContext, type EditorViewContributionUpdateKind, type EditorViewSnapshot } from '@singapore-editor/core/extensions';
2
+ import type { LanguageServerCompletionEditFeature } from './completion';
3
+ /**
4
+ * Putting the row the caret is on back at the level its own text asks for, on the keystroke that
5
+ * changed which level that is.
6
+ *
7
+ * A closing delimiter is typed at the depth of the block it ends, one level in from where it
8
+ * belongs, and nothing afterwards moves it — every closed block is left a level too deep until
9
+ * somebody reindents the region by hand. The question "where does this row belong" is the one the
10
+ * reindent commands ask of the language's indentation rules, so it is asked of them here too
11
+ * rather than answered again beside them.
12
+ *
13
+ * Two narrow gates keep it from being a formatter that runs on every keystroke: which characters
14
+ * are watched at all, and whether the answer is still about the document by the time it is written.
15
+ * The second is a gate rather than a certainty because the edit is handed over a turn after it is
16
+ * computed — a row cannot be rewritten from inside the announcement of the keystroke that produced
17
+ * it.
18
+ */
19
+ export type FormatOnTypeControllerOptions = {
20
+ readonly context: EditorViewContributionContext;
21
+ readonly editFeature: EditorCapabilityToken<LanguageServerCompletionEditFeature>;
22
+ };
23
+ export declare class FormatOnTypeController {
24
+ private readonly options;
25
+ private pending;
26
+ private scheduled;
27
+ private disposed;
28
+ constructor(options: FormatOnTypeControllerOptions);
29
+ update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind, change: EditorContributionChange | null): void;
30
+ dispose(): void;
31
+ /**
32
+ * Moves an edit onto the document an announced change has just produced, or drops it.
33
+ *
34
+ * Its range is a pair of offsets into one document. An edit arriving at or before the caret moves
35
+ * them, and replacing what now sits there would take a bite out of text the reader wrote in the
36
+ * meantime. An edit past the caret cannot reach the row's leading whitespace, so the answer still
37
+ * holds — and re-stamping is how it goes on holding for a document that has legitimately moved on.
38
+ */
39
+ private carryPendingForward;
40
+ private schedule;
41
+ private applyPending;
42
+ }
43
+ //# sourceMappingURL=formatOnType.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatOnType.d.ts","sourceRoot":"","sources":["../src/formatOnType.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,wBAAwB,EAG7B,KAAK,qBAAqB,EAE1B,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,EACrC,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAA;AAE1C,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,cAAc,CAAA;AAEvE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,OAAO,EAAE,6BAA6B,CAAA;IAC/C,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,mCAAmC,CAAC,CAAA;CACjF,CAAA;AA4BD,qBAAa,sBAAsB;IAKd,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ3C,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,QAAQ,CAAQ;gBAEY,OAAO,EAAE,6BAA6B;IAEnE,MAAM,CACX,QAAQ,EAAE,kBAAkB,EAC5B,IAAI,EAAE,gCAAgC,EACtC,MAAM,EAAE,wBAAwB,GAAG,IAAI,GACtC,IAAI;IAkBA,OAAO,IAAI,IAAI;IAKtB;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,QAAQ;IAehB,OAAO,CAAC,YAAY;CAmBrB"}
@@ -0,0 +1,198 @@
1
+ import { editorLanguageConfiguration, reindentEditsForRanges } from "@singapore-editor/core/extensions";
2
+ //#region src/formatOnType.ts
3
+ var FORMAT_ON_TYPE_WINDOW = 4096;
4
+ var FormatOnTypeController = class {
5
+ options;
6
+ pending = null;
7
+ scheduled = false;
8
+ disposed = false;
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ update(snapshot, kind, change) {
13
+ if (kind === "document" || kind === "clear") {
14
+ this.pending = null;
15
+ return;
16
+ }
17
+ if (kind !== "content") return;
18
+ this.carryPendingForward(snapshot, change);
19
+ const caretOffset = formatOnTypeCaret(snapshot, change);
20
+ if (caretOffset === null) return;
21
+ const pending = pendingFormatOnType(snapshot, caretOffset);
22
+ if (!pending) return;
23
+ this.pending = pending;
24
+ this.schedule();
25
+ }
26
+ dispose() {
27
+ this.disposed = true;
28
+ this.pending = null;
29
+ }
30
+ /**
31
+ * Moves an edit onto the document an announced change has just produced, or drops it.
32
+ *
33
+ * Its range is a pair of offsets into one document. An edit arriving at or before the caret moves
34
+ * them, and replacing what now sits there would take a bite out of text the reader wrote in the
35
+ * meantime. An edit past the caret cannot reach the row's leading whitespace, so the answer still
36
+ * holds — and re-stamping is how it goes on holding for a document that has legitimately moved on.
37
+ */
38
+ carryPendingForward(snapshot, change) {
39
+ const pending = this.pending;
40
+ if (!pending || !change) return;
41
+ if (change.edits.some((edit) => edit.from <= pending.caretOffset)) {
42
+ this.pending = null;
43
+ return;
44
+ }
45
+ this.pending = {
46
+ ...pending,
47
+ textVersion: snapshot.textVersion
48
+ };
49
+ }
50
+ schedule() {
51
+ if (this.scheduled) return;
52
+ this.scheduled = true;
53
+ /**
54
+ * @justification Coalesces the re-indents a single keystroke can queue into one pass at the end
55
+ * of the turn, so the edit lands before the frame the character was typed in is painted;
56
+ * `scheduled` is what makes a second call in that turn a no-op rather than a second pass.
57
+ */
58
+ queueMicrotask(() => {
59
+ this.scheduled = false;
60
+ this.applyPending();
61
+ });
62
+ }
63
+ applyPending() {
64
+ const pending = this.pending;
65
+ this.pending = null;
66
+ if (!pending || this.disposed) return;
67
+ if (this.options.context.getSnapshot().textVersion !== pending.textVersion) return;
68
+ const feature = this.options.context.getFeature(this.options.editFeature);
69
+ if (!feature) return;
70
+ const head = pending.caretOffset + pending.edit.text.length - (pending.edit.to - pending.edit.from);
71
+ feature.applyCompletion({
72
+ edits: [pending.edit],
73
+ selection: {
74
+ anchor: head,
75
+ head
76
+ }
77
+ });
78
+ }
79
+ };
80
+ /**
81
+ * Where the caret sits after a keystroke that can have changed its row's level, or null for one
82
+ * that cannot have.
83
+ *
84
+ * Several carets, or a caret with a selection behind it, describe an edit whose shape this cannot
85
+ * read, so they ask for nothing.
86
+ */
87
+ function formatOnTypeCaret(snapshot, change) {
88
+ if (!change || change.kind !== "edit") return null;
89
+ if (change.edits.length !== 1) return null;
90
+ const edit = change.edits[0];
91
+ if (!edit || edit.text.length !== 1) return null;
92
+ const configuration = ruledConfiguration(snapshot.languageId);
93
+ if (!configuration) return null;
94
+ if (!triggerCharacters(configuration).has(edit.text)) return null;
95
+ if (snapshot.selections.length !== 1) return null;
96
+ const selection = snapshot.selections[0];
97
+ if (!selection || selection.startOffset !== selection.endOffset) return null;
98
+ const caretOffset = selection.headOffset;
99
+ return caretOffset === edit.from + edit.text.length ? caretOffset : null;
100
+ }
101
+ /** The rules for a language whose rows have levels at all; without them there is nothing to say. */
102
+ function ruledConfiguration(languageId) {
103
+ const configuration = editorLanguageConfiguration(languageId);
104
+ return configuration?.indentationRules ? configuration : null;
105
+ }
106
+ var triggerCharactersByConfiguration = /* @__PURE__ */ new WeakMap();
107
+ /**
108
+ * The characters whose arrival can put a row at a different level than the one it was typed on.
109
+ *
110
+ * A row drops a level by holding the delimiter that closes a block, so the closers the language
111
+ * describes are the whole set — which is also what keeps prose, which describes none, out of this
112
+ * without a list of exemptions. The last character is what finishes a closer, so blocks that end in
113
+ * a word are watched by the keystroke completing the word rather than the one starting it.
114
+ *
115
+ * Every other keystroke stops here, on one lookup, rather than going on to read the row. The set is
116
+ * kept against the record it was read from and not against the language's name, so a host replacing
117
+ * a language's rules gets a set built from the rules it replaced them with; there is no moment at
118
+ * which a stale set can answer.
119
+ */
120
+ function triggerCharacters(configuration) {
121
+ const cached = triggerCharactersByConfiguration.get(configuration);
122
+ if (cached) return cached;
123
+ const characters = /* @__PURE__ */ new Set();
124
+ for (const pair of configuration.brackets) {
125
+ const last = pair.close.at(-1);
126
+ if (last !== void 0) characters.add(last);
127
+ }
128
+ triggerCharactersByConfiguration.set(configuration, characters);
129
+ return characters;
130
+ }
131
+ /**
132
+ * Whether the closer ending at the caret is the whole of what its row holds.
133
+ *
134
+ * A closer that follows anything else was written to end an expression the row is carrying, not the
135
+ * block the row sits in; moving the row would take that expression somewhere it was not put. Which
136
+ * closer it is has to be decided here rather than from the single character typed, because a
137
+ * multi-character delimiter is only itself once the whole of it is on the row.
138
+ */
139
+ function closesItsOwnRow(configuration, window) {
140
+ const rowStart = window.text.lastIndexOf("\n", window.caretOffset - 1) + 1;
141
+ const before = window.text.slice(rowStart, window.caretOffset);
142
+ return configuration.brackets.some((pair) => before.endsWith(pair.close) && isBlank(before.slice(0, before.length - pair.close.length)));
143
+ }
144
+ function isBlank(text) {
145
+ return !/[^\t ]/u.test(text);
146
+ }
147
+ function pendingFormatOnType(snapshot, caretOffset) {
148
+ const configuration = ruledConfiguration(snapshot.languageId);
149
+ if (!configuration) return null;
150
+ const window = formatOnTypeWindow(snapshot, caretOffset);
151
+ if (!window || !closesItsOwnRow(configuration, window)) return null;
152
+ const [edit] = reindentEditsForRanges(window.text, [{
153
+ end: window.caretOffset,
154
+ start: window.caretOffset
155
+ }], {
156
+ languageId: snapshot.languageId,
157
+ tabSize: snapshot.tabSize
158
+ });
159
+ if (!edit) return null;
160
+ return {
161
+ caretOffset,
162
+ edit: {
163
+ from: edit.from + window.start,
164
+ text: edit.text,
165
+ to: edit.to + window.start
166
+ },
167
+ textVersion: snapshot.textVersion
168
+ };
169
+ }
170
+ /**
171
+ * Whole rows back to the bound, and the caret's own row through to its end.
172
+ *
173
+ * The row is read to its end rather than to the caret because a language's rules are patterns over
174
+ * the whole of it, and a closer with anything behind it on the row is a row that describes something
175
+ * other than the block it sits in. A caret standing further than the bound from a row start has
176
+ * nothing above it this can read, and says so rather than measuring against half a row.
177
+ */
178
+ function formatOnTypeWindow(snapshot, caretOffset) {
179
+ const source = snapshot.textSnapshot;
180
+ const length = source.length;
181
+ const read = (start, end) => source.readRange(start, end);
182
+ const from = Math.max(0, caretOffset - FORMAT_ON_TYPE_WINDOW);
183
+ const before = read(from, caretOffset);
184
+ const firstBreak = before.indexOf("\n");
185
+ if (from > 0 && firstBreak === -1) return null;
186
+ const start = from === 0 ? 0 : from + firstBreak + 1;
187
+ const after = read(caretOffset, Math.min(length, caretOffset + FORMAT_ON_TYPE_WINDOW));
188
+ const rowEnd = after.indexOf("\n");
189
+ return {
190
+ text: before.slice(start - from) + (rowEnd === -1 ? after : after.slice(0, rowEnd)),
191
+ start,
192
+ caretOffset: caretOffset - start
193
+ };
194
+ }
195
+ //#endregion
196
+ export { FormatOnTypeController };
197
+
198
+ //# sourceMappingURL=formatOnType.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatOnType.js","names":[],"sources":["../src/formatOnType.ts"],"sourcesContent":["import type { TextEdit } from '@singapore-editor/core/document'\nimport {\n type EditorContributionChange,\n editorLanguageConfiguration,\n reindentEditsForRanges,\n type EditorCapabilityToken,\n type EditorLanguageConfiguration,\n type EditorViewContributionContext,\n type EditorViewContributionUpdateKind,\n type EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\n\nimport type { LanguageServerCompletionEditFeature } from './completion'\n\n/**\n * Putting the row the caret is on back at the level its own text asks for, on the keystroke that\n * changed which level that is.\n *\n * A closing delimiter is typed at the depth of the block it ends, one level in from where it\n * belongs, and nothing afterwards moves it — every closed block is left a level too deep until\n * somebody reindents the region by hand. The question \"where does this row belong\" is the one the\n * reindent commands ask of the language's indentation rules, so it is asked of them here too\n * rather than answered again beside them.\n *\n * Two narrow gates keep it from being a formatter that runs on every keystroke: which characters\n * are watched at all, and whether the answer is still about the document by the time it is written.\n * The second is a gate rather than a certainty because the edit is handed over a turn after it is\n * computed — a row cannot be rewritten from inside the announcement of the keystroke that produced\n * it.\n */\n\nexport type FormatOnTypeControllerOptions = {\n readonly context: EditorViewContributionContext\n readonly editFeature: EditorCapabilityToken<LanguageServerCompletionEditFeature>\n}\n\n/** An edit, and the document it was measured against. */\ntype PendingFormatOnType = {\n readonly caretOffset: number\n readonly edit: TextEdit\n readonly textVersion: number\n}\n\n/**\n * The text one row's level is decided from, and where it sits in the document.\n *\n * A row belongs one level in from the row above it, unless either of them is inside a string or a\n * comment — and which literals are open is only settled by reading from wherever they were opened.\n * Reading from the top of the document scans the whole file on a keystroke and goes on growing with\n * it; reading back a fixed distance costs the same in a file of any size, and the only text the two\n * answer differently for is a literal left open for longer than that distance.\n */\ntype FormatOnTypeWindow = {\n readonly text: string\n /** Where `text` starts, always at a row start so the rows inside it are the document's rows. */\n readonly start: number\n /** The caret, in the window's own offsets. */\n readonly caretOffset: number\n}\n\nconst FORMAT_ON_TYPE_WINDOW = 4096\n\nexport class FormatOnTypeController {\n private pending: PendingFormatOnType | null = null\n private scheduled = false\n private disposed = false\n\n public constructor(private readonly options: FormatOnTypeControllerOptions) {}\n\n public update(\n snapshot: EditorViewSnapshot,\n kind: EditorViewContributionUpdateKind,\n change: EditorContributionChange | null,\n ): void {\n if (kind === 'document' || kind === 'clear') {\n this.pending = null\n return\n }\n if (kind !== 'content') return\n\n this.carryPendingForward(snapshot, change)\n const caretOffset = formatOnTypeCaret(snapshot, change)\n if (caretOffset === null) return\n\n const pending = pendingFormatOnType(snapshot, caretOffset)\n if (!pending) return\n\n this.pending = pending\n this.schedule()\n }\n\n public dispose(): void {\n this.disposed = true\n this.pending = null\n }\n\n /**\n * Moves an edit onto the document an announced change has just produced, or drops it.\n *\n * Its range is a pair of offsets into one document. An edit arriving at or before the caret moves\n * them, and replacing what now sits there would take a bite out of text the reader wrote in the\n * meantime. An edit past the caret cannot reach the row's leading whitespace, so the answer still\n * holds — and re-stamping is how it goes on holding for a document that has legitimately moved on.\n */\n private carryPendingForward(\n snapshot: EditorViewSnapshot,\n change: EditorContributionChange | null,\n ): void {\n const pending = this.pending\n if (!pending || !change) return\n if (change.edits.some((edit) => edit.from <= pending.caretOffset)) {\n this.pending = null\n return\n }\n\n this.pending = { ...pending, textVersion: snapshot.textVersion }\n }\n\n private schedule(): void {\n if (this.scheduled) return\n\n this.scheduled = true\n /**\n * @justification Coalesces the re-indents a single keystroke can queue into one pass at the end\n * of the turn, so the edit lands before the frame the character was typed in is painted;\n * `scheduled` is what makes a second call in that turn a no-op rather than a second pass.\n */\n queueMicrotask(() => {\n this.scheduled = false\n this.applyPending()\n })\n }\n\n private applyPending(): void {\n const pending = this.pending\n this.pending = null\n if (!pending || this.disposed) return\n\n const snapshot = this.options.context.getSnapshot()\n // A pass opened from inside another one is not announced a second time, so the guard above\n // cannot have seen every edit. Reading the range back would only say that some whitespace still\n // sits at those numbers, which an unannounced edit shifting the buffer leaves true of a row the\n // reader never touched; the version is the one reading that counts every edit either way.\n if (snapshot.textVersion !== pending.textVersion) return\n\n const feature = this.options.context.getFeature(this.options.editFeature)\n if (!feature) return\n\n const head =\n pending.caretOffset + pending.edit.text.length - (pending.edit.to - pending.edit.from)\n feature.applyCompletion({ edits: [pending.edit], selection: { anchor: head, head } })\n }\n}\n\n/**\n * Where the caret sits after a keystroke that can have changed its row's level, or null for one\n * that cannot have.\n *\n * Several carets, or a caret with a selection behind it, describe an edit whose shape this cannot\n * read, so they ask for nothing.\n */\nfunction formatOnTypeCaret(\n snapshot: EditorViewSnapshot,\n change: EditorContributionChange | null,\n): number | 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 || edit.text.length !== 1) return null\n\n const configuration = ruledConfiguration(snapshot.languageId)\n if (!configuration) return null\n if (!triggerCharacters(configuration).has(edit.text)) return null\n\n if (snapshot.selections.length !== 1) return null\n const selection = snapshot.selections[0]\n if (!selection || selection.startOffset !== selection.endOffset) return null\n\n const caretOffset = selection.headOffset\n\n return caretOffset === edit.from + edit.text.length ? caretOffset : null\n}\n\n/** The rules for a language whose rows have levels at all; without them there is nothing to say. */\nfunction ruledConfiguration(languageId: string | null): EditorLanguageConfiguration | null {\n const configuration = editorLanguageConfiguration(languageId)\n\n return configuration?.indentationRules ? configuration : null\n}\n\nconst triggerCharactersByConfiguration = new WeakMap<\n EditorLanguageConfiguration,\n ReadonlySet<string>\n>()\n\n/**\n * The characters whose arrival can put a row at a different level than the one it was typed on.\n *\n * A row drops a level by holding the delimiter that closes a block, so the closers the language\n * describes are the whole set — which is also what keeps prose, which describes none, out of this\n * without a list of exemptions. The last character is what finishes a closer, so blocks that end in\n * a word are watched by the keystroke completing the word rather than the one starting it.\n *\n * Every other keystroke stops here, on one lookup, rather than going on to read the row. The set is\n * kept against the record it was read from and not against the language's name, so a host replacing\n * a language's rules gets a set built from the rules it replaced them with; there is no moment at\n * which a stale set can answer.\n */\nfunction triggerCharacters(configuration: EditorLanguageConfiguration): ReadonlySet<string> {\n const cached = triggerCharactersByConfiguration.get(configuration)\n if (cached) return cached\n\n const characters = new Set<string>()\n for (const pair of configuration.brackets) {\n const last = pair.close.at(-1)\n if (last !== undefined) characters.add(last)\n }\n\n triggerCharactersByConfiguration.set(configuration, characters)\n return characters\n}\n\n/**\n * Whether the closer ending at the caret is the whole of what its row holds.\n *\n * A closer that follows anything else was written to end an expression the row is carrying, not the\n * block the row sits in; moving the row would take that expression somewhere it was not put. Which\n * closer it is has to be decided here rather than from the single character typed, because a\n * multi-character delimiter is only itself once the whole of it is on the row.\n */\nfunction closesItsOwnRow(\n configuration: EditorLanguageConfiguration,\n window: FormatOnTypeWindow,\n): boolean {\n const rowStart = window.text.lastIndexOf('\\n', window.caretOffset - 1) + 1\n const before = window.text.slice(rowStart, window.caretOffset)\n\n return configuration.brackets.some(\n (pair) =>\n before.endsWith(pair.close) && isBlank(before.slice(0, before.length - pair.close.length)),\n )\n}\n\nfunction isBlank(text: string): boolean {\n return !/[^\\t ]/u.test(text)\n}\n\nfunction pendingFormatOnType(\n snapshot: EditorViewSnapshot,\n caretOffset: number,\n): PendingFormatOnType | null {\n const configuration = ruledConfiguration(snapshot.languageId)\n if (!configuration) return null\n\n const window = formatOnTypeWindow(snapshot, caretOffset)\n if (!window || !closesItsOwnRow(configuration, window)) return null\n\n const [edit] = reindentEditsForRanges(\n window.text,\n [{ end: window.caretOffset, start: window.caretOffset }],\n { languageId: snapshot.languageId, tabSize: snapshot.tabSize },\n )\n if (!edit) return null\n\n return {\n caretOffset,\n edit: { from: edit.from + window.start, text: edit.text, to: edit.to + window.start },\n textVersion: snapshot.textVersion,\n }\n}\n\n/**\n * Whole rows back to the bound, and the caret's own row through to its end.\n *\n * The row is read to its end rather than to the caret because a language's rules are patterns over\n * the whole of it, and a closer with anything behind it on the row is a row that describes something\n * other than the block it sits in. A caret standing further than the bound from a row start has\n * nothing above it this can read, and says so rather than measuring against half a row.\n */\nfunction formatOnTypeWindow(\n snapshot: EditorViewSnapshot,\n caretOffset: number,\n): FormatOnTypeWindow | null {\n const source = snapshot.textSnapshot\n const length = source.length\n const read = (start: number, end: number): string => source.readRange(start, end)\n\n const from = Math.max(0, caretOffset - FORMAT_ON_TYPE_WINDOW)\n const before = read(from, caretOffset)\n const firstBreak = before.indexOf('\\n')\n if (from > 0 && firstBreak === -1) return null\n\n const start = from === 0 ? 0 : from + firstBreak + 1\n const after = read(caretOffset, Math.min(length, caretOffset + FORMAT_ON_TYPE_WINDOW))\n const rowEnd = after.indexOf('\\n')\n\n return {\n text: before.slice(start - from) + (rowEnd === -1 ? after : after.slice(0, rowEnd)),\n start,\n caretOffset: caretOffset - start,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type * as lsp from 'vscode-languageserver-protocol';
2
+ import type { TextEdit } from '@singapore-editor/core';
3
+ import { type LspTextDocumentSnapshot, type LspTextSnapshot } from '@singapore-editor/lsp';
4
+ /**
5
+ * Converts server formatting edits into buffer edits.
6
+ *
7
+ * Returned in descending order and with overlaps dropped, because the batch applicator rejects
8
+ * overlapping edits outright — a formatter that returns a whole-document edit plus a nested one
9
+ * would otherwise throw rather than format.
10
+ *
11
+ * What survives is re-diffed against the text it replaces, so a reply that rewrites the document to
12
+ * change one line touches only that line.
13
+ */
14
+ export declare function prepareFormattingEdits(document: LspTextDocumentSnapshot, edits: readonly lsp.TextEdit[] | null): TextEdit[];
15
+ /** Whether the edits would leave the document unchanged, so a no-op formatter records no change. */
16
+ export declare function formattingChangesText(text: LspTextSnapshot, edits: readonly TextEdit[]): boolean;
17
+ export declare function formattingOptions(tabSize: number): lsp.FormattingOptions;
18
+ //# sourceMappingURL=formatting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatting.d.ts","sourceRoot":"","sources":["../src/formatting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAA;AAI9B;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,uBAAuB,EACjC,KAAK,EAAE,SAAS,GAAG,CAAC,QAAQ,EAAE,GAAG,IAAI,GACpC,QAAQ,EAAE,CA8CZ;AAOD,oGAAoG;AACpG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,OAAO,CAEhG;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,iBAAiB,CAKxE"}
@@ -0,0 +1,52 @@
1
+ import { REDIFF_LENGTH_LIMIT, minimalReplacementEdits } from "./minimalEdits.js";
2
+ import { lspPositionToOffsetInSnapshot } from "@singapore-editor/lsp";
3
+ //#region src/formatting.ts
4
+ /**
5
+ * Converts server formatting edits into buffer edits.
6
+ *
7
+ * Returned in descending order and with overlaps dropped, because the batch applicator rejects
8
+ * overlapping edits outright — a formatter that returns a whole-document edit plus a nested one
9
+ * would otherwise throw rather than format.
10
+ *
11
+ * What survives is re-diffed against the text it replaces, so a reply that rewrites the document to
12
+ * change one line touches only that line.
13
+ */
14
+ function prepareFormattingEdits(document, edits) {
15
+ if (!edits || edits.length === 0) return [];
16
+ const converted = edits.map((edit) => ({
17
+ from: lspPositionToOffsetInSnapshot(document, edit.range.start),
18
+ text: edit.newText,
19
+ to: lspPositionToOffsetInSnapshot(document, edit.range.end)
20
+ })).filter((edit) => edit.from <= edit.to).sort((left, right) => left.from - right.from || right.to - left.to);
21
+ const applied = [];
22
+ let keptThrough = Number.NEGATIVE_INFINITY;
23
+ for (const edit of converted) {
24
+ if (edit.from < keptThrough) continue;
25
+ const previous = applied[applied.length - 1];
26
+ if (previous && previous.to === edit.from && joinFitsRediff(previous, edit)) applied[applied.length - 1] = {
27
+ from: previous.from,
28
+ text: previous.text + edit.text,
29
+ to: edit.to
30
+ };
31
+ else applied.push(edit);
32
+ keptThrough = edit.to;
33
+ }
34
+ return applied.flatMap((edit) => minimalReplacementEdits(document.textSnapshot.readRange(edit.from, edit.to), edit.text, edit.from)).reverse();
35
+ }
36
+ function joinFitsRediff(left, right) {
37
+ return Math.max(right.to - left.from, left.text.length + right.text.length) <= REDIFF_LENGTH_LIMIT;
38
+ }
39
+ /** Whether the edits would leave the document unchanged, so a no-op formatter records no change. */
40
+ function formattingChangesText(text, edits) {
41
+ return edits.some((edit) => text.readRange(edit.from, edit.to) !== edit.text);
42
+ }
43
+ function formattingOptions(tabSize) {
44
+ return {
45
+ insertSpaces: true,
46
+ tabSize: Math.max(1, tabSize)
47
+ };
48
+ }
49
+ //#endregion
50
+ export { formattingChangesText, formattingOptions, prepareFormattingEdits };
51
+
52
+ //# sourceMappingURL=formatting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatting.js","names":[],"sources":["../src/formatting.ts"],"sourcesContent":["import type * as lsp from 'vscode-languageserver-protocol'\n\nimport type { TextEdit } from '@singapore-editor/core'\nimport {\n lspPositionToOffsetInSnapshot,\n type LspTextDocumentSnapshot,\n type LspTextSnapshot,\n} from '@singapore-editor/lsp'\n\nimport { minimalReplacementEdits, REDIFF_LENGTH_LIMIT } from './minimalEdits'\n\n/**\n * Converts server formatting edits into buffer edits.\n *\n * Returned in descending order and with overlaps dropped, because the batch applicator rejects\n * overlapping edits outright — a formatter that returns a whole-document edit plus a nested one\n * would otherwise throw rather than format.\n *\n * What survives is re-diffed against the text it replaces, so a reply that rewrites the document to\n * change one line touches only that line.\n */\nexport function prepareFormattingEdits(\n document: LspTextDocumentSnapshot,\n edits: readonly lsp.TextEdit[] | null,\n): TextEdit[] {\n if (!edits || edits.length === 0) return []\n\n const converted = edits\n .map((edit) => ({\n from: lspPositionToOffsetInSnapshot(document, edit.range.start),\n text: edit.newText,\n to: lspPositionToOffsetInSnapshot(document, edit.range.end),\n }))\n .filter((edit) => edit.from <= edit.to)\n // Earliest first, and the longest of any that start together: when a formatter sends a\n // whole-document edit alongside a nested one, the outer edit is the real answer and keeping the\n // fragment instead would rewrite a slice of the file with a slice of the result.\n .sort((left, right) => left.from - right.from || right.to - left.to)\n\n const applied: TextEdit[] = []\n let keptThrough = Number.NEGATIVE_INFINITY\n for (const edit of converted) {\n if (edit.from < keptThrough) continue\n\n const previous = applied[applied.length - 1]\n // Edits that meet are joined first: two fragments that cancel each other out are invisible\n // while they are compared apart. Joining stops at the length past which the comparison is\n // skipped anyway, where a wider span would only buy a wider rewrite.\n if (previous && previous.to === edit.from && joinFitsRediff(previous, edit)) {\n applied[applied.length - 1] = {\n from: previous.from,\n text: previous.text + edit.text,\n to: edit.to,\n }\n } else {\n applied.push(edit)\n }\n keptThrough = edit.to\n }\n\n const minimal = applied.flatMap((edit) =>\n minimalReplacementEdits(\n document.textSnapshot.readRange(edit.from, edit.to),\n edit.text,\n edit.from,\n ),\n )\n\n // Descending, so applying them in order cannot shift the offsets of the ones still to come.\n return minimal.reverse()\n}\n\nfunction joinFitsRediff(left: TextEdit, right: TextEdit): boolean {\n const joined = Math.max(right.to - left.from, left.text.length + right.text.length)\n return joined <= REDIFF_LENGTH_LIMIT\n}\n\n/** Whether the edits would leave the document unchanged, so a no-op formatter records no change. */\nexport function formattingChangesText(text: LspTextSnapshot, edits: readonly TextEdit[]): boolean {\n return edits.some((edit) => text.readRange(edit.from, edit.to) !== edit.text)\n}\n\nexport function formattingOptions(tabSize: number): lsp.FormattingOptions {\n return {\n insertSpaces: true,\n tabSize: Math.max(1, tabSize),\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAqBA,SAAgB,uBACd,UACA,OACY;CACZ,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAE1C,MAAM,YAAY,MACf,KAAK,UAAU;EACd,MAAM,8BAA8B,UAAU,KAAK,MAAM,KAAK;EAC9D,MAAM,KAAK;EACX,IAAI,8BAA8B,UAAU,KAAK,MAAM,GAAG;CAC5D,EAAE,CAAC,CACF,QAAQ,SAAS,KAAK,QAAQ,KAAK,EAAE,CAAC,CAItC,MAAM,MAAM,UAAU,KAAK,OAAO,MAAM,QAAQ,MAAM,KAAK,KAAK,EAAE;CAErE,MAAM,UAAsB,CAAC;CAC7B,IAAI,cAAc,OAAO;CACzB,KAAK,MAAM,QAAQ,WAAW;EAC5B,IAAI,KAAK,OAAO,aAAa;EAE7B,MAAM,WAAW,QAAQ,QAAQ,SAAS;EAI1C,IAAI,YAAY,SAAS,OAAO,KAAK,QAAQ,eAAe,UAAU,IAAI,GACxE,QAAQ,QAAQ,SAAS,KAAK;GAC5B,MAAM,SAAS;GACf,MAAM,SAAS,OAAO,KAAK;GAC3B,IAAI,KAAK;EACX;OAEA,QAAQ,KAAK,IAAI;EAEnB,cAAc,KAAK;CACrB;CAWA,OATgB,QAAQ,SAAS,SAC/B,wBACE,SAAS,aAAa,UAAU,KAAK,MAAM,KAAK,EAAE,GAClD,KAAK,MACL,KAAK,IACP,CAIK,CAAA,CAAQ,QAAQ;AACzB;AAEA,SAAS,eAAe,MAAgB,OAA0B;CAEhE,OADe,KAAK,IAAI,MAAM,KAAK,KAAK,MAAM,KAAK,KAAK,SAAS,MAAM,KAAK,MACrE,KAAU;AACnB;;AAGA,SAAgB,sBAAsB,MAAuB,OAAqC;CAChG,OAAO,MAAM,MAAM,SAAS,KAAK,UAAU,KAAK,MAAM,KAAK,EAAE,MAAM,KAAK,IAAI;AAC9E;AAEA,SAAgB,kBAAkB,SAAwC;CACxE,OAAO;EACL,cAAc;EACd,SAAS,KAAK,IAAI,GAAG,OAAO;CAC9B;AACF"}
@@ -0,0 +1,22 @@
1
+ export type FuzzyMatch = {
2
+ readonly score: number;
3
+ /** Indices into the candidate that the pattern matched, ascending. */
4
+ readonly positions: readonly number[];
5
+ };
6
+ /**
7
+ * The match of the whole pattern, or null when there is none.
8
+ *
9
+ * The first character has to land somewhere the candidate begins something — its start, a camel-case
10
+ * hump, the far side of a separator. A pattern that only turns up in the middle of a word is a
11
+ * coincidence rather than what the user was reaching for.
12
+ */
13
+ export declare function fuzzyMatch(pattern: string, candidate: string): FuzzyMatch | null;
14
+ /**
15
+ * The match of the pattern, or of what is left of it after dropping leading characters, at any
16
+ * alignment at all.
17
+ *
18
+ * For a string the pattern was never ranked against there is no reason for all of it to be present,
19
+ * and marking the part that is beats marking nothing.
20
+ */
21
+ export declare function looseFuzzyMatch(pattern: string, candidate: string): FuzzyMatch | null;
22
+ //# sourceMappingURL=fuzzyMatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzyMatch.d.ts","sourceRoot":"","sources":["../src/fuzzyMatch.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CACtC,CAAA;AAqCD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEhF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAUrF"}