@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.
- package/README.md +103 -0
- package/dist/bufferSync.d.ts +12 -0
- package/dist/bufferSync.d.ts.map +1 -0
- package/dist/bufferSync.js +35 -0
- package/dist/bufferSync.js.map +1 -0
- package/dist/codeActions.d.ts +83 -0
- package/dist/codeActions.d.ts.map +1 -0
- package/dist/codeActions.js +294 -0
- package/dist/codeActions.js.map +1 -0
- package/dist/colorAlpha.d.ts +4 -0
- package/dist/colorAlpha.d.ts.map +1 -0
- package/dist/colorAlpha.js +28 -0
- package/dist/colorAlpha.js.map +1 -0
- package/dist/completion.d.ts +124 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +609 -0
- package/dist/completion.js.map +1 -0
- package/dist/completionCommit.d.ts +19 -0
- package/dist/completionCommit.d.ts.map +1 -0
- package/dist/completionCommit.js +57 -0
- package/dist/completionCommit.js.map +1 -0
- package/dist/completionController.d.ts +89 -0
- package/dist/completionController.d.ts.map +1 -0
- package/dist/completionController.js +376 -0
- package/dist/completionController.js.map +1 -0
- package/dist/completionProviders.d.ts +50 -0
- package/dist/completionProviders.d.ts.map +1 -0
- package/dist/completionProviders.js +64 -0
- package/dist/completionProviders.js.map +1 -0
- package/dist/connectionContext.d.ts +7 -0
- package/dist/connectionContext.d.ts.map +1 -0
- package/dist/definitionLinkController.d.ts +54 -0
- package/dist/definitionLinkController.d.ts.map +1 -0
- package/dist/definitionLinkController.js +221 -0
- package/dist/definitionLinkController.js.map +1 -0
- package/dist/definitionNavigation.d.ts +76 -0
- package/dist/definitionNavigation.d.ts.map +1 -0
- package/dist/definitionNavigation.js +143 -0
- package/dist/definitionNavigation.js.map +1 -0
- package/dist/diagnosticNotes.d.ts +7 -0
- package/dist/diagnosticNotes.d.ts.map +1 -0
- package/dist/diagnosticNotes.js +37 -0
- package/dist/diagnosticNotes.js.map +1 -0
- package/dist/diagnosticProjection.d.ts +31 -0
- package/dist/diagnosticProjection.d.ts.map +1 -0
- package/dist/diagnosticProjection.js +79 -0
- package/dist/diagnosticProjection.js.map +1 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +94 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/diagnosticsPresenter.d.ts +64 -0
- package/dist/diagnosticsPresenter.d.ts.map +1 -0
- package/dist/diagnosticsPresenter.js +305 -0
- package/dist/diagnosticsPresenter.js.map +1 -0
- package/dist/document.d.ts +69 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +190 -0
- package/dist/document.js.map +1 -0
- package/dist/documentHighlightController.d.ts +34 -0
- package/dist/documentHighlightController.d.ts.map +1 -0
- package/dist/documentHighlightController.js +101 -0
- package/dist/documentHighlightController.js.map +1 -0
- package/dist/documentSnapshot.d.ts +9 -0
- package/dist/documentSnapshot.d.ts.map +1 -0
- package/dist/documentSnapshot.js +31 -0
- package/dist/documentSnapshot.js.map +1 -0
- package/dist/documentSync.d.ts +53 -0
- package/dist/documentSync.d.ts.map +1 -0
- package/dist/documentSync.js +334 -0
- package/dist/documentSync.js.map +1 -0
- package/dist/documentSyncController.d.ts +26 -0
- package/dist/documentSyncController.d.ts.map +1 -0
- package/dist/documentSyncController.js +24 -0
- package/dist/documentSyncController.js.map +1 -0
- package/dist/formatOnType.d.ts +43 -0
- package/dist/formatOnType.d.ts.map +1 -0
- package/dist/formatOnType.js +198 -0
- package/dist/formatOnType.js.map +1 -0
- package/dist/formatting.d.ts +18 -0
- package/dist/formatting.d.ts.map +1 -0
- package/dist/formatting.js +52 -0
- package/dist/formatting.js.map +1 -0
- package/dist/fuzzyMatch.d.ts +22 -0
- package/dist/fuzzyMatch.d.ts.map +1 -0
- package/dist/fuzzyMatch.js +216 -0
- package/dist/fuzzyMatch.js.map +1 -0
- package/dist/hoverParticipant.d.ts +24 -0
- package/dist/hoverParticipant.d.ts.map +1 -0
- package/dist/hoverParticipant.js +156 -0
- package/dist/hoverParticipant.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/keyCommands.d.ts +19 -0
- package/dist/keyCommands.d.ts.map +1 -0
- package/dist/keyCommands.js +21 -0
- package/dist/keyCommands.js.map +1 -0
- package/dist/lane.d.ts +28 -0
- package/dist/lane.d.ts.map +1 -0
- package/dist/lane.js +138 -0
- package/dist/lane.js.map +1 -0
- package/dist/lspConnection.d.ts +94 -0
- package/dist/lspConnection.d.ts.map +1 -0
- package/dist/lspConnection.js +195 -0
- package/dist/lspConnection.js.map +1 -0
- package/dist/lspConnectionPool.d.ts +34 -0
- package/dist/lspConnectionPool.d.ts.map +1 -0
- package/dist/lspConnectionPool.js +255 -0
- package/dist/lspConnectionPool.js.map +1 -0
- package/dist/minimalEdits.d.ts +21 -0
- package/dist/minimalEdits.d.ts.map +1 -0
- package/dist/minimalEdits.js +181 -0
- package/dist/minimalEdits.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +38 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin.d.ts +100 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +837 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.styles.d.ts +31 -0
- package/dist/plugin.styles.d.ts.map +1 -0
- package/dist/plugin.styles.js +102 -0
- package/dist/plugin.styles.js.map +1 -0
- package/dist/pluginTypes.d.ts +41 -0
- package/dist/pluginTypes.d.ts.map +1 -0
- package/dist/pluginTypes.js +0 -0
- package/dist/pullDiagnostics.d.ts +30 -0
- package/dist/pullDiagnostics.d.ts.map +1 -0
- package/dist/pullDiagnostics.js +94 -0
- package/dist/pullDiagnostics.js.map +1 -0
- package/dist/renameWidget.d.ts +30 -0
- package/dist/renameWidget.d.ts.map +1 -0
- package/dist/renameWidget.js +117 -0
- package/dist/renameWidget.js.map +1 -0
- package/dist/semanticTokenDecoder.d.ts +81 -0
- package/dist/semanticTokenDecoder.d.ts.map +1 -0
- package/dist/semanticTokenDecoder.js +132 -0
- package/dist/semanticTokenDecoder.js.map +1 -0
- package/dist/semanticTokens.d.ts +47 -0
- package/dist/semanticTokens.d.ts.map +1 -0
- package/dist/semanticTokens.js +68 -0
- package/dist/semanticTokens.js.map +1 -0
- package/dist/serverSet.d.ts +69 -0
- package/dist/serverSet.d.ts.map +1 -0
- package/dist/serverSet.js +331 -0
- package/dist/serverSet.js.map +1 -0
- package/dist/signatureHelp.d.ts +40 -0
- package/dist/signatureHelp.d.ts.map +1 -0
- package/dist/signatureHelp.js +108 -0
- package/dist/signatureHelp.js.map +1 -0
- package/dist/signatureHelpController.d.ts +52 -0
- package/dist/signatureHelpController.d.ts.map +1 -0
- package/dist/signatureHelpController.js +160 -0
- package/dist/signatureHelpController.js.map +1 -0
- package/dist/sourceText.d.ts +22 -0
- package/dist/sourceText.d.ts.map +1 -0
- package/dist/sourceText.js +70 -0
- package/dist/sourceText.js.map +1 -0
- package/dist/types.d.ts +238 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/viewDocumentSnapshot.d.ts +5 -0
- package/dist/viewDocumentSnapshot.d.ts.map +1 -0
- package/dist/viewDocumentSnapshot.js +12 -0
- package/dist/viewDocumentSnapshot.js.map +1 -0
- package/dist/websocket.d.ts +6 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +4 -0
- package/dist/workspaceEdit.d.ts +66 -0
- package/dist/workspaceEdit.d.ts.map +1 -0
- package/dist/workspaceEdit.js +276 -0
- package/dist/workspaceEdit.js.map +1 -0
- package/dist/workspaceEditProvenance.d.ts +5 -0
- package/dist/workspaceEditProvenance.d.ts.map +1 -0
- package/dist/workspaceEditProvenance.js +16 -0
- package/dist/workspaceEditProvenance.js.map +1 -0
- package/dist/workspaceTextEdits.d.ts +60 -0
- package/dist/workspaceTextEdits.d.ts.map +1 -0
- package/dist/workspaceTextEdits.js +395 -0
- package/dist/workspaceTextEdits.js.map +1 -0
- package/package.json +118 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @singapore-editor/lsp-plugin
|
|
2
|
+
|
|
3
|
+
Editor integration layer for LSP-backed Singapore language features.
|
|
4
|
+
|
|
5
|
+
This package adapts `@singapore-editor/lsp` transports to `@singapore-editor/core` plugins for diagnostics,
|
|
6
|
+
completion, hover, definition navigation, references, and document synchronization.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @singapore-editor/core @singapore-editor/lsp @singapore-editor/lsp-plugin
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
createLanguageServerAdapterPlugin,
|
|
19
|
+
createWebSocketLspTransportFactory,
|
|
20
|
+
} from '@singapore-editor/lsp-plugin'
|
|
21
|
+
|
|
22
|
+
const plugin = createLanguageServerAdapterPlugin({
|
|
23
|
+
name: 'typescript-lsp',
|
|
24
|
+
createTransport: createWebSocketLspTransportFactory('ws://localhost:3000/lsp'),
|
|
25
|
+
documentSync: {
|
|
26
|
+
shouldSyncLanguageId: (languageId) => languageId === 'typescript',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Exports
|
|
32
|
+
|
|
33
|
+
- `createLanguageServerAdapterPlugin` creates the full editor integration.
|
|
34
|
+
- `createLanguageServerPlugin` exposes a lower-level plugin factory.
|
|
35
|
+
- `createWebSocketLspTransportFactory` and `createWorkerLspTransportFactory` create transport
|
|
36
|
+
factories.
|
|
37
|
+
- Diagnostic, path, markdown tooltip, completion, and document-sync helpers are available through
|
|
38
|
+
subpath exports.
|
|
39
|
+
|
|
40
|
+
## Keys
|
|
41
|
+
|
|
42
|
+
The completion list and the signature hint are driven by editor commands, not by listening to
|
|
43
|
+
keys: `editor.action.triggerSuggest` (Ctrl+Space), `selectNextSuggestion`, `selectPrevSuggestion`,
|
|
44
|
+
`selectNextPageSuggestion`, `selectPrevPageSuggestion`, `acceptSelectedSuggestion`,
|
|
45
|
+
`hideSuggestWidget`, `closeParameterHints`, `showNextParameterHint` and `showPrevParameterHint`.
|
|
46
|
+
The core's `suggest` keymap pack binds them under the context keys this plugin registers
|
|
47
|
+
(`suggestWidgetVisible`, `parameterHintsVisible`, `parameterHintsMultipleSignatures`). A host that
|
|
48
|
+
turns the editor's keymap off, or drops that pack, binds these commands itself or the list cannot
|
|
49
|
+
be driven from the keyboard. They are part of `commands`, so `commands: []` registers none of them.
|
|
50
|
+
|
|
51
|
+
## Document lifetime
|
|
52
|
+
|
|
53
|
+
The connection-only API owns its document session automatically. Dispose the editor to close it:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const plugin = createLanguageServerPlugin({
|
|
57
|
+
webSocketRoute: 'ws://localhost:3001/lsp',
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For tabs or split editors, create a session beside the text buffer and pass it to each view:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
createLanguageServerDocument,
|
|
66
|
+
createLanguageServerPlugin,
|
|
67
|
+
} from '@singapore-editor/lsp-plugin'
|
|
68
|
+
|
|
69
|
+
const document = createLanguageServerDocument({
|
|
70
|
+
buffer,
|
|
71
|
+
uri: 'file:///project/main.ts',
|
|
72
|
+
languageId: 'typescript',
|
|
73
|
+
lanes: [
|
|
74
|
+
{
|
|
75
|
+
id: 'typescript',
|
|
76
|
+
features: { diagnostics: 0, completion: 0, hover: 0, navigation: 0 },
|
|
77
|
+
webSocketRoute: 'ws://localhost:3001/lsp',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const plugin = createLanguageServerPlugin({ document })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The session synchronizes buffer changes and receives diagnostics even with no mounted view.
|
|
86
|
+
Each view renders the current diagnostics on attachment. Disposing a view unsubscribes it;
|
|
87
|
+
it does not close the document. The host calls `document.dispose()` when the document closes.
|
|
88
|
+
Multiple views share the session, with independent completion, hover, and selection UI.
|
|
89
|
+
|
|
90
|
+
Use `documentId` when the editor uses an opaque identity different from the URI. Connection
|
|
91
|
+
options and an explicit `document` are mutually exclusive plugin inputs. Both forms use the
|
|
92
|
+
same synchronization and diagnostic implementation.
|
|
93
|
+
|
|
94
|
+
For shared sessions, pass `onApplyWorkspaceEdit` to `createLanguageServerDocument`.
|
|
95
|
+
The document uses it both to advertise workspace-edit support during initialization and to
|
|
96
|
+
apply edits requested by its views. It cannot be supplied on a plugin borrowing that document.
|
|
97
|
+
The connection-only plugin still accepts `onApplyWorkspaceEdit` directly.
|
|
98
|
+
|
|
99
|
+
### Diagnostic hover actions
|
|
100
|
+
|
|
101
|
+
`getDiagnosticActions({ documentUri, textVersion, diagnostic })` supplies actions beside each diagnostic note. It is supported by the server, server-set and adapter plugins, including `createTypeScriptLspPlugin`. Return an empty array when no action applies. Each action has a `label` and `run(): void | Promise<void>`.
|
|
102
|
+
|
|
103
|
+
The tooltip prevents repeated invocation while an action is pending, preserves keyboard focus, and shows a rejected action's message beside its button. Progressive hover replies retain pending actions and their errors. An action refuses to run after its document, text version or diagnostic changes; the user can reopen the hover for a fresh action. The host owns application behavior and async operation state.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EditorTextBuffer } from '@singapore-editor/core/document';
|
|
2
|
+
import type { EditorDisposable } from '@singapore-editor/core/extensions';
|
|
3
|
+
import type { LanguageServerConnectionContext } from './connectionContext';
|
|
4
|
+
import type { LanguageServerDocumentSyncController } from './documentSyncController';
|
|
5
|
+
/** Gives a connection the same live buffer and edit provenance as a mounted editor. */
|
|
6
|
+
export declare function synchronizeLanguageServerBuffer(context: LanguageServerConnectionContext, options: {
|
|
7
|
+
readonly buffer: EditorTextBuffer;
|
|
8
|
+
readonly uri: string;
|
|
9
|
+
readonly languageId: string;
|
|
10
|
+
readonly controller?: LanguageServerDocumentSyncController;
|
|
11
|
+
}): EditorDisposable;
|
|
12
|
+
//# sourceMappingURL=bufferSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bufferSync.d.ts","sourceRoot":"","sources":["../src/bufferSync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,0BAA0B,CAAA;AAKpF,uFAAuF;AACvF,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,+BAA+B,EACxC,OAAO,EAAE;IACP,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,oCAAoC,CAAA;CAC3D,GACA,gBAAgB,CA0BlB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { bufferDocumentSnapshot } from "./documentSnapshot.js";
|
|
2
|
+
import { DocumentSync } from "./documentSync.js";
|
|
3
|
+
import { logicalRevisionScopeFor } from "./lane.js";
|
|
4
|
+
//#region src/bufferSync.ts
|
|
5
|
+
/** Gives a connection the same live buffer and edit provenance as a mounted editor. */
|
|
6
|
+
function synchronizeLanguageServerBuffer(context, options) {
|
|
7
|
+
const getSnapshot = () => bufferDocumentSnapshot({
|
|
8
|
+
...options,
|
|
9
|
+
uri: sync.activeDocument?.uri ?? options.uri
|
|
10
|
+
});
|
|
11
|
+
const sync = new DocumentSync(context.workspace, {
|
|
12
|
+
clear() {},
|
|
13
|
+
render() {},
|
|
14
|
+
publishSummary() {}
|
|
15
|
+
}, {
|
|
16
|
+
logicalRevisionScope: logicalRevisionScopeFor(context.workspace),
|
|
17
|
+
onDocumentClosed() {}
|
|
18
|
+
});
|
|
19
|
+
const registration = options.controller?.register({
|
|
20
|
+
workspace: context.workspace,
|
|
21
|
+
sync,
|
|
22
|
+
getSnapshot
|
|
23
|
+
});
|
|
24
|
+
sync.sync(getSnapshot(), null);
|
|
25
|
+
const unsubscribe = options.buffer.subscribe(({ change }) => sync.sync(getSnapshot(), change));
|
|
26
|
+
return { dispose() {
|
|
27
|
+
unsubscribe();
|
|
28
|
+
registration?.dispose();
|
|
29
|
+
sync.close();
|
|
30
|
+
} };
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { synchronizeLanguageServerBuffer };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=bufferSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bufferSync.js","names":[],"sources":["../src/bufferSync.ts"],"sourcesContent":["import type { EditorTextBuffer } from '@singapore-editor/core/document'\nimport type { EditorDisposable } from '@singapore-editor/core/extensions'\nimport type { LanguageServerConnectionContext } from './connectionContext'\nimport type { LanguageServerDocumentSyncController } from './documentSyncController'\nimport { bufferDocumentSnapshot } from './documentSnapshot'\nimport { DocumentSync } from './documentSync'\nimport { logicalRevisionScopeFor } from './lane'\n\n/** Gives a connection the same live buffer and edit provenance as a mounted editor. */\nexport function synchronizeLanguageServerBuffer(\n context: LanguageServerConnectionContext,\n options: {\n readonly buffer: EditorTextBuffer\n readonly uri: string\n readonly languageId: string\n readonly controller?: LanguageServerDocumentSyncController\n },\n): EditorDisposable {\n const getSnapshot = () =>\n bufferDocumentSnapshot({ ...options, uri: sync.activeDocument?.uri ?? options.uri })\n const sync = new DocumentSync(\n context.workspace,\n {\n clear() {},\n render() {},\n publishSummary() {},\n },\n { logicalRevisionScope: logicalRevisionScopeFor(context.workspace), onDocumentClosed() {} },\n )\n const registration = options.controller?.register({\n workspace: context.workspace,\n sync,\n getSnapshot,\n })\n sync.sync(getSnapshot(), null)\n const unsubscribe = options.buffer.subscribe(({ change }) => sync.sync(getSnapshot(), change))\n return {\n dispose() {\n unsubscribe()\n registration?.dispose()\n sync.close()\n },\n }\n}\n"],"mappings":";;;;;AASA,SAAgB,gCACd,SACA,SAMkB;CAClB,MAAM,oBACJ,uBAAuB;EAAE,GAAG;EAAS,KAAK,KAAK,gBAAgB,OAAO,QAAQ;CAAI,CAAC;CACrF,MAAM,OAAO,IAAI,aACf,QAAQ,WACR;EACE,QAAQ,CAAC;EACT,SAAS,CAAC;EACV,iBAAiB,CAAC;CACpB,GACA;EAAE,sBAAsB,wBAAwB,QAAQ,SAAS;EAAG,mBAAmB,CAAC;CAAE,CAC5F;CACA,MAAM,eAAe,QAAQ,YAAY,SAAS;EAChD,WAAW,QAAQ;EACnB;EACA;CACF,CAAC;CACD,KAAK,KAAK,YAAY,GAAG,IAAI;CAC7B,MAAM,cAAc,QAAQ,OAAO,WAAW,EAAE,aAAa,KAAK,KAAK,YAAY,GAAG,MAAM,CAAC;CAC7F,OAAO,EACL,UAAU;EACR,YAAY;EACZ,cAAc,QAAQ;EACtB,KAAK,MAAM;CACb,EACF;AACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { EditorViewContributionContext, EditorViewContributionUpdateKind } from '@singapore-editor/core/extensions';
|
|
2
|
+
import { type LspTextDocumentSnapshot } from '@singapore-editor/lsp';
|
|
3
|
+
import type * as lsp from 'vscode-languageserver-protocol';
|
|
4
|
+
import type { OffsetRange } from '@singapore-editor/plugin-ui/offset-range';
|
|
5
|
+
import type { ActiveDocument } from './pluginTypes';
|
|
6
|
+
import { type LanguageServerCodeActionRouter } from './serverSet';
|
|
7
|
+
export type CodeActionResponse = readonly (lsp.Command | lsp.CodeAction)[] | null;
|
|
8
|
+
/**
|
|
9
|
+
* The one action `editor.action.autoFix` applies, or null when the answer holds none.
|
|
10
|
+
*
|
|
11
|
+
* Nothing else in the answer is kept. There is no menu to put a second candidate in, and a ranked
|
|
12
|
+
* list no surface can draw would be a promise the editor does not keep.
|
|
13
|
+
*
|
|
14
|
+
* `isPreferred` is the server naming the action it would pick itself, and the first such is taken
|
|
15
|
+
* rather than re-ordered: the order the answer arrived in is the only other ranking it gave us.
|
|
16
|
+
* `resolvable` says whether the server fills an action's edit in on request — without that, one
|
|
17
|
+
* that arrived carrying only a command is a fix this client has no way to carry out, and taking it
|
|
18
|
+
* would swallow the chord to do nothing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function preferredQuickFix(response: CodeActionResponse, resolvable: boolean): lsp.CodeAction | null;
|
|
21
|
+
/**
|
|
22
|
+
* The range an automatic request asks about, or null when there is nothing at the caret to ask
|
|
23
|
+
* about.
|
|
24
|
+
*
|
|
25
|
+
* A caret sitting between two blanks is inside indentation or trailing space, where no server has an
|
|
26
|
+
* action to offer; asking anyway would spend a request per settled keystroke while a line is being
|
|
27
|
+
* indented. A drawn selection always asks, because the user pointed at something.
|
|
28
|
+
*/
|
|
29
|
+
export declare function codeActionAutoTriggerRange(document: LspTextDocumentSnapshot, start: number, end: number): OffsetRange | null;
|
|
30
|
+
export type CodeActionControllerOptions = {
|
|
31
|
+
readonly router: LanguageServerCodeActionRouter;
|
|
32
|
+
readonly context: EditorViewContributionContext;
|
|
33
|
+
readonly getActiveDocument: () => ActiveDocument | null;
|
|
34
|
+
readonly getDiagnostics: () => readonly lsp.Diagnostic[];
|
|
35
|
+
readonly onRequestError: (error: unknown) => void;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Keeps the fix available at the caret, and applies it on request.
|
|
39
|
+
*
|
|
40
|
+
* It is refreshed by a debounced oracle rather than fetched when the fix is asked for, because the
|
|
41
|
+
* two events that change the answer — the caret moving and the server republishing diagnostics —
|
|
42
|
+
* are exactly the two the user is waiting on: by the time a squiggle is visible under the caret, its
|
|
43
|
+
* fix is already in hand. Anything that could invalidate it drops it first, so a stale answer is
|
|
44
|
+
* never applied to text it was not computed against.
|
|
45
|
+
*/
|
|
46
|
+
export declare class CodeActionController {
|
|
47
|
+
private readonly options;
|
|
48
|
+
private fix;
|
|
49
|
+
private seenDiagnostics;
|
|
50
|
+
private timer;
|
|
51
|
+
private abort;
|
|
52
|
+
private applicationAbort;
|
|
53
|
+
private requestId;
|
|
54
|
+
private disposed;
|
|
55
|
+
constructor(options: CodeActionControllerOptions);
|
|
56
|
+
update(kind: EditorViewContributionUpdateKind): void;
|
|
57
|
+
/**
|
|
58
|
+
* Reruns the oracle when the server republished the active document's diagnostics.
|
|
59
|
+
*
|
|
60
|
+
* Publishes the document sync refused — another file, or a version the buffer has moved past — leave
|
|
61
|
+
* the list identical, and re-asking on those would answer for a document nobody is looking at.
|
|
62
|
+
*/
|
|
63
|
+
diagnosticsChanged(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Applies the preferred quick fix at the caret.
|
|
66
|
+
*
|
|
67
|
+
* Reports handled as soon as the edit is on its way: an action the server has not filled in yet
|
|
68
|
+
* needs a round trip, and returning false would let the keystroke fall through to another binding
|
|
69
|
+
* after the fix was already committed to.
|
|
70
|
+
*/
|
|
71
|
+
applyAutoFix(): boolean;
|
|
72
|
+
dispose(): void;
|
|
73
|
+
private schedule;
|
|
74
|
+
private clear;
|
|
75
|
+
private request;
|
|
76
|
+
private runAction;
|
|
77
|
+
private resolvesActions;
|
|
78
|
+
private dispatchAction;
|
|
79
|
+
private isCurrentApplication;
|
|
80
|
+
private reportUnsupportedCommand;
|
|
81
|
+
private reportHostFailure;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=codeActions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeActions.d.ts","sourceRoot":"","sources":["../src/codeActions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gCAAgC,EACjC,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,KAAK,GAAG,MAAM,gCAAgC,CAAA;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAA;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,aAAa,CAAA;AAgBpB,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,CAAA;AAEjF;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,UAAU,EAAE,OAAO,GAClB,GAAG,CAAC,UAAU,GAAG,IAAI,CAevB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,uBAAuB,EACjC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,WAAW,GAAG,IAAI,CAYpB;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAA;IAC/C,QAAQ,CAAC,OAAO,EAAE,6BAA6B,CAAA;IAC/C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAA;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,SAAS,GAAG,CAAC,UAAU,EAAE,CAAA;IACxD,QAAQ,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAClD,CAAA;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IASZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR3C,OAAO,CAAC,GAAG,CAA8B;IACzC,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAQ;gBAEY,OAAO,EAAE,2BAA2B;IAEjE,MAAM,CAAC,IAAI,EAAE,gCAAgC,GAAG,IAAI;IAU3D;;;;;OAKG;IACI,kBAAkB,IAAI,IAAI;IAQjC;;;;;;OAMG;IACI,YAAY,IAAI,OAAO;IAmBvB,OAAO,IAAI,IAAI;IAOtB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,KAAK;YAWC,OAAO;YA8CP,SAAS;IA8BvB,OAAO,CAAC,eAAe;YAIT,cAAc;IA0C5B,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,iBAAiB;CAI1B"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { characterAt, rowBounds } from "./sourceText.js";
|
|
2
|
+
import { parseWorkspaceEdit } from "./workspaceEdit.js";
|
|
3
|
+
import { currentWorkspaceEditOrigin } from "./workspaceEditProvenance.js";
|
|
4
|
+
import { lspPositionToOffsetInSnapshot, offsetToLspPositionInSnapshot } from "@singapore-editor/lsp";
|
|
5
|
+
//#region src/codeActions.ts
|
|
6
|
+
/**
|
|
7
|
+
* Long enough that a held arrow key or a burst of typing asks once, short enough that the answer is
|
|
8
|
+
* already there when the hand leaves the keyboard and reaches for the fix.
|
|
9
|
+
*/
|
|
10
|
+
var CODE_ACTION_DEBOUNCE_MS = 250;
|
|
11
|
+
var CODE_ACTION_TRIGGER_AUTOMATIC = 2;
|
|
12
|
+
/** Root of the kind subtree the auto fix draws from. */
|
|
13
|
+
var CODE_ACTION_QUICK_FIX_KIND = "quickfix";
|
|
14
|
+
/**
|
|
15
|
+
* The one action `editor.action.autoFix` applies, or null when the answer holds none.
|
|
16
|
+
*
|
|
17
|
+
* Nothing else in the answer is kept. There is no menu to put a second candidate in, and a ranked
|
|
18
|
+
* list no surface can draw would be a promise the editor does not keep.
|
|
19
|
+
*
|
|
20
|
+
* `isPreferred` is the server naming the action it would pick itself, and the first such is taken
|
|
21
|
+
* rather than re-ordered: the order the answer arrived in is the only other ranking it gave us.
|
|
22
|
+
* `resolvable` says whether the server fills an action's edit in on request — without that, one
|
|
23
|
+
* that arrived carrying only a command is a fix this client has no way to carry out, and taking it
|
|
24
|
+
* would swallow the chord to do nothing.
|
|
25
|
+
*/
|
|
26
|
+
function preferredQuickFix(response, resolvable) {
|
|
27
|
+
if (!response) return null;
|
|
28
|
+
for (const entry of response) {
|
|
29
|
+
if (isCommand(entry)) continue;
|
|
30
|
+
if (entry.isPreferred !== true || entry.disabled) continue;
|
|
31
|
+
if (!resolvable && !entry.edit) continue;
|
|
32
|
+
if (!isQuickFixKind(entry.kind)) continue;
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The range an automatic request asks about, or null when there is nothing at the caret to ask
|
|
39
|
+
* about.
|
|
40
|
+
*
|
|
41
|
+
* A caret sitting between two blanks is inside indentation or trailing space, where no server has an
|
|
42
|
+
* action to offer; asking anyway would spend a request per settled keystroke while a line is being
|
|
43
|
+
* indented. A drawn selection always asks, because the user pointed at something.
|
|
44
|
+
*/
|
|
45
|
+
function codeActionAutoTriggerRange(document, start, end) {
|
|
46
|
+
if (start !== end) return {
|
|
47
|
+
start,
|
|
48
|
+
end
|
|
49
|
+
};
|
|
50
|
+
const line = rowBounds(document, start);
|
|
51
|
+
if (line.end === line.start) return null;
|
|
52
|
+
if (start === line.start) return isWhitespace(characterAt(document, start)) ? null : {
|
|
53
|
+
start,
|
|
54
|
+
end
|
|
55
|
+
};
|
|
56
|
+
if (start === line.end) return isWhitespace(characterAt(document, start - 1)) ? null : {
|
|
57
|
+
start,
|
|
58
|
+
end
|
|
59
|
+
};
|
|
60
|
+
const around = document.textSnapshot.readRange(start - 1, start + 1);
|
|
61
|
+
return isWhitespace(around[0]) && isWhitespace(around[1]) ? null : {
|
|
62
|
+
start,
|
|
63
|
+
end
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Keeps the fix available at the caret, and applies it on request.
|
|
68
|
+
*
|
|
69
|
+
* It is refreshed by a debounced oracle rather than fetched when the fix is asked for, because the
|
|
70
|
+
* two events that change the answer — the caret moving and the server republishing diagnostics —
|
|
71
|
+
* are exactly the two the user is waiting on: by the time a squiggle is visible under the caret, its
|
|
72
|
+
* fix is already in hand. Anything that could invalidate it drops it first, so a stale answer is
|
|
73
|
+
* never applied to text it was not computed against.
|
|
74
|
+
*/
|
|
75
|
+
var CodeActionController = class {
|
|
76
|
+
options;
|
|
77
|
+
fix = null;
|
|
78
|
+
seenDiagnostics = null;
|
|
79
|
+
timer = null;
|
|
80
|
+
abort = null;
|
|
81
|
+
applicationAbort = null;
|
|
82
|
+
requestId = 0;
|
|
83
|
+
disposed = false;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.options = options;
|
|
86
|
+
}
|
|
87
|
+
update(kind) {
|
|
88
|
+
if (kind === "document" || kind === "clear") {
|
|
89
|
+
this.clear();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (kind !== "selection" && kind !== "content") return;
|
|
93
|
+
this.schedule();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reruns the oracle when the server republished the active document's diagnostics.
|
|
97
|
+
*
|
|
98
|
+
* Publishes the document sync refused — another file, or a version the buffer has moved past — leave
|
|
99
|
+
* the list identical, and re-asking on those would answer for a document nobody is looking at.
|
|
100
|
+
*/
|
|
101
|
+
diagnosticsChanged() {
|
|
102
|
+
const diagnostics = this.options.getDiagnostics();
|
|
103
|
+
if (diagnostics === this.seenDiagnostics) return;
|
|
104
|
+
this.seenDiagnostics = diagnostics;
|
|
105
|
+
this.schedule();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Applies the preferred quick fix at the caret.
|
|
109
|
+
*
|
|
110
|
+
* Reports handled as soon as the edit is on its way: an action the server has not filled in yet
|
|
111
|
+
* needs a round trip, and returning false would let the keystroke fall through to another binding
|
|
112
|
+
* after the fix was already committed to.
|
|
113
|
+
*/
|
|
114
|
+
applyAutoFix() {
|
|
115
|
+
if (this.disposed) return false;
|
|
116
|
+
const active = this.options.getActiveDocument();
|
|
117
|
+
if (!active) return false;
|
|
118
|
+
const fix = this.fix;
|
|
119
|
+
if (!fix) return false;
|
|
120
|
+
const provenance = this.options.router.provenanceOf(fix);
|
|
121
|
+
if (!provenance) return false;
|
|
122
|
+
this.applicationAbort?.abort();
|
|
123
|
+
const abort = new AbortController();
|
|
124
|
+
this.applicationAbort = abort;
|
|
125
|
+
this.runAction(active, fix, provenance, abort);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
dispose() {
|
|
129
|
+
if (this.disposed) return;
|
|
130
|
+
this.disposed = true;
|
|
131
|
+
this.clear();
|
|
132
|
+
}
|
|
133
|
+
schedule() {
|
|
134
|
+
this.clear();
|
|
135
|
+
/**
|
|
136
|
+
* @justification Debounces a question to the language server against the caret and the
|
|
137
|
+
* diagnostics, not against rendering, and `clear()` withdraws it on every further change — so a
|
|
138
|
+
* fix is never computed against text the reader has already moved past.
|
|
139
|
+
*/
|
|
140
|
+
this.timer = setTimeout(() => {
|
|
141
|
+
this.timer = null;
|
|
142
|
+
this.request();
|
|
143
|
+
}, CODE_ACTION_DEBOUNCE_MS);
|
|
144
|
+
}
|
|
145
|
+
clear() {
|
|
146
|
+
if (this.timer) clearTimeout(this.timer);
|
|
147
|
+
this.timer = null;
|
|
148
|
+
this.abort?.abort();
|
|
149
|
+
this.abort = null;
|
|
150
|
+
this.applicationAbort?.abort();
|
|
151
|
+
this.applicationAbort = null;
|
|
152
|
+
this.requestId += 1;
|
|
153
|
+
this.fix = null;
|
|
154
|
+
}
|
|
155
|
+
async request() {
|
|
156
|
+
const active = this.options.getActiveDocument();
|
|
157
|
+
if (!active) return;
|
|
158
|
+
if (!this.options.router.hasReady("codeActions", "textDocument/codeAction")) return;
|
|
159
|
+
const selection = this.options.context.getSnapshot().selections[0];
|
|
160
|
+
if (!selection) return;
|
|
161
|
+
const range = codeActionAutoTriggerRange(active, selection.startOffset, selection.endOffset);
|
|
162
|
+
if (!range) return;
|
|
163
|
+
const abort = new AbortController();
|
|
164
|
+
const requestId = this.requestId + 1;
|
|
165
|
+
this.requestId = requestId;
|
|
166
|
+
this.abort = abort;
|
|
167
|
+
try {
|
|
168
|
+
const response = await this.options.router.request("textDocument/codeAction", {
|
|
169
|
+
context: {
|
|
170
|
+
diagnostics: diagnosticsOverlapping(active, this.options.getDiagnostics(), range),
|
|
171
|
+
only: [CODE_ACTION_QUICK_FIX_KIND],
|
|
172
|
+
triggerKind: CODE_ACTION_TRIGGER_AUTOMATIC
|
|
173
|
+
},
|
|
174
|
+
range: {
|
|
175
|
+
end: offsetToLspPositionInSnapshot(active, range.end),
|
|
176
|
+
start: offsetToLspPositionInSnapshot(active, range.start)
|
|
177
|
+
},
|
|
178
|
+
textDocument: { uri: active.uri }
|
|
179
|
+
}, { signal: abort.signal });
|
|
180
|
+
if (requestId !== this.requestId) return;
|
|
181
|
+
if (this.disposed) return;
|
|
182
|
+
if (active !== this.options.getActiveDocument()) return;
|
|
183
|
+
this.fix = preferredQuickFix(response, this.resolvesActions());
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!isAbortError(error)) this.options.onRequestError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async runAction(active, action, provenance, abort) {
|
|
189
|
+
try {
|
|
190
|
+
if (action.command) {
|
|
191
|
+
this.reportUnsupportedCommand(action);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const owned = action.edit ? {
|
|
195
|
+
action,
|
|
196
|
+
...provenance
|
|
197
|
+
} : await this.options.router.resolveOwnedCodeAction(action, { signal: abort.signal });
|
|
198
|
+
if (!owned) return;
|
|
199
|
+
if (!this.isCurrentApplication(active, abort)) return;
|
|
200
|
+
if (owned.action.command) {
|
|
201
|
+
this.reportUnsupportedCommand(owned.action);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await this.dispatchAction(active, owned.action, owned, abort.signal);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!isAbortError(error)) this.options.onRequestError(error);
|
|
207
|
+
} finally {
|
|
208
|
+
if (this.applicationAbort === abort) this.applicationAbort = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
resolvesActions() {
|
|
212
|
+
return this.options.router.canResolveCodeActions();
|
|
213
|
+
}
|
|
214
|
+
async dispatchAction(active, action, provenance, signal) {
|
|
215
|
+
if (!action.edit) {
|
|
216
|
+
this.reportUnsupportedCommand(action);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const parsed = parseWorkspaceEdit(action.edit);
|
|
220
|
+
if (!parsed.ok) {
|
|
221
|
+
this.options.onRequestError(new Error(parsed.error.reason));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const origin = currentWorkspaceEditOrigin(provenance.guard, active, parsed.value);
|
|
225
|
+
if (!origin) return;
|
|
226
|
+
if (signal.aborted) return;
|
|
227
|
+
const apply = provenance.lane.onApplyWorkspaceEdit;
|
|
228
|
+
if (!apply) {
|
|
229
|
+
this.options.onRequestError(/* @__PURE__ */ new Error(`"${action.title}" cannot be applied without a workspace edit host.`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const result = await apply({
|
|
233
|
+
guard: provenance.guard,
|
|
234
|
+
label: action.title,
|
|
235
|
+
logicalRevisionScope: provenance.lane.connection.logicalRevisionScope,
|
|
236
|
+
originUri: active.uri,
|
|
237
|
+
originVersion: origin.version,
|
|
238
|
+
plan: parsed.value,
|
|
239
|
+
serverId: provenance.lane.id,
|
|
240
|
+
signal,
|
|
241
|
+
source: "code-action"
|
|
242
|
+
});
|
|
243
|
+
this.reportHostFailure(result);
|
|
244
|
+
}
|
|
245
|
+
isCurrentApplication(active, abort) {
|
|
246
|
+
if (this.disposed) return false;
|
|
247
|
+
if (abort.signal.aborted) return false;
|
|
248
|
+
return active === this.options.getActiveDocument();
|
|
249
|
+
}
|
|
250
|
+
reportUnsupportedCommand(action) {
|
|
251
|
+
this.options.onRequestError(/* @__PURE__ */ new Error(`"${action.title}" includes a server command, which this editor cannot run.`));
|
|
252
|
+
}
|
|
253
|
+
reportHostFailure(result) {
|
|
254
|
+
if (result.status !== "failed") return;
|
|
255
|
+
this.options.onRequestError(/* @__PURE__ */ new Error(`${result.code}: ${result.message}`));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
/** A literal's own `command` is an object; only the bare form names its command with a string. */
|
|
259
|
+
function isCommand(entry) {
|
|
260
|
+
return typeof entry.command === "string";
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Whether `kind` is the quick-fix kind or sits beneath it in the dotted hierarchy.
|
|
264
|
+
*
|
|
265
|
+
* Containment, not equality: a server that names its actions precisely answers `quickfix.import`,
|
|
266
|
+
* and comparing the strings whole would reject every one of those. An action that named no kind at
|
|
267
|
+
* all placed itself nowhere, so it is not in this subtree either.
|
|
268
|
+
*/
|
|
269
|
+
function isQuickFixKind(kind) {
|
|
270
|
+
if (kind === void 0) return false;
|
|
271
|
+
return kind === CODE_ACTION_QUICK_FIX_KIND || kind.startsWith(`${CODE_ACTION_QUICK_FIX_KIND}.`);
|
|
272
|
+
}
|
|
273
|
+
function diagnosticsOverlapping(document, diagnostics, range) {
|
|
274
|
+
return diagnostics.filter((diagnostic) => {
|
|
275
|
+
const start = lspPositionToOffsetInSnapshot(document, diagnostic.range.start);
|
|
276
|
+
const end = lspPositionToOffsetInSnapshot(document, diagnostic.range.end);
|
|
277
|
+
return start <= range.end && end >= range.start;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function isWhitespace(character) {
|
|
281
|
+
return character !== void 0 && /\s/.test(character);
|
|
282
|
+
}
|
|
283
|
+
function isAbortError(error) {
|
|
284
|
+
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
285
|
+
if (!isRecord(error)) return false;
|
|
286
|
+
return error.name === "LspRequestCancelledError";
|
|
287
|
+
}
|
|
288
|
+
function isRecord(value) {
|
|
289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
export { CodeActionController, codeActionAutoTriggerRange, preferredQuickFix };
|
|
293
|
+
|
|
294
|
+
//# sourceMappingURL=codeActions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeActions.js","names":[],"sources":["../src/codeActions.ts"],"sourcesContent":["import type {\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n} from '@singapore-editor/core/extensions'\nimport {\n lspPositionToOffsetInSnapshot,\n offsetToLspPositionInSnapshot,\n type LspTextDocumentSnapshot,\n} from '@singapore-editor/lsp'\nimport type * as lsp from 'vscode-languageserver-protocol'\n\nimport type { OffsetRange } from '@singapore-editor/plugin-ui/offset-range'\nimport type { ActiveDocument } from './pluginTypes'\nimport { characterAt, rowBounds } from './sourceText'\nimport {\n type LanguageServerCodeActionProvenance,\n type LanguageServerCodeActionRouter,\n} from './serverSet'\nimport type { ApplyWorkspaceEditResult } from './types'\nimport { parseWorkspaceEdit } from './workspaceEdit'\nimport { currentWorkspaceEditOrigin } from './workspaceEditProvenance'\n\n/**\n * Long enough that a held arrow key or a burst of typing asks once, short enough that the answer is\n * already there when the hand leaves the keyboard and reaches for the fix.\n */\nconst CODE_ACTION_DEBOUNCE_MS = 250\n\nconst CODE_ACTION_TRIGGER_AUTOMATIC = 2\n\n/** Root of the kind subtree the auto fix draws from. */\nconst CODE_ACTION_QUICK_FIX_KIND = 'quickfix'\n\nexport type CodeActionResponse = readonly (lsp.Command | lsp.CodeAction)[] | null\n\n/**\n * The one action `editor.action.autoFix` applies, or null when the answer holds none.\n *\n * Nothing else in the answer is kept. There is no menu to put a second candidate in, and a ranked\n * list no surface can draw would be a promise the editor does not keep.\n *\n * `isPreferred` is the server naming the action it would pick itself, and the first such is taken\n * rather than re-ordered: the order the answer arrived in is the only other ranking it gave us.\n * `resolvable` says whether the server fills an action's edit in on request — without that, one\n * that arrived carrying only a command is a fix this client has no way to carry out, and taking it\n * would swallow the chord to do nothing.\n */\nexport function preferredQuickFix(\n response: CodeActionResponse,\n resolvable: boolean,\n): lsp.CodeAction | null {\n if (!response) return null\n\n for (const entry of response) {\n // A server that declines the literal form answers with bare commands, which carry neither a\n // kind nor a preference and so can never be the action asked for here.\n if (isCommand(entry)) continue\n if (entry.isPreferred !== true || entry.disabled) continue\n if (!resolvable && !entry.edit) continue\n if (!isQuickFixKind(entry.kind)) continue\n\n return entry\n }\n\n return null\n}\n\n/**\n * The range an automatic request asks about, or null when there is nothing at the caret to ask\n * about.\n *\n * A caret sitting between two blanks is inside indentation or trailing space, where no server has an\n * action to offer; asking anyway would spend a request per settled keystroke while a line is being\n * indented. A drawn selection always asks, because the user pointed at something.\n */\nexport function codeActionAutoTriggerRange(\n document: LspTextDocumentSnapshot,\n start: number,\n end: number,\n): OffsetRange | null {\n if (start !== end) return { start, end }\n\n const line = rowBounds(document, start)\n if (line.end === line.start) return null\n if (start === line.start)\n return isWhitespace(characterAt(document, start)) ? null : { start, end }\n if (start === line.end)\n return isWhitespace(characterAt(document, start - 1)) ? null : { start, end }\n\n const around = document.textSnapshot.readRange(start - 1, start + 1)\n return isWhitespace(around[0]) && isWhitespace(around[1]) ? null : { start, end }\n}\n\nexport type CodeActionControllerOptions = {\n readonly router: LanguageServerCodeActionRouter\n readonly context: EditorViewContributionContext\n readonly getActiveDocument: () => ActiveDocument | null\n readonly getDiagnostics: () => readonly lsp.Diagnostic[]\n readonly onRequestError: (error: unknown) => void\n}\n\n/**\n * Keeps the fix available at the caret, and applies it on request.\n *\n * It is refreshed by a debounced oracle rather than fetched when the fix is asked for, because the\n * two events that change the answer — the caret moving and the server republishing diagnostics —\n * are exactly the two the user is waiting on: by the time a squiggle is visible under the caret, its\n * fix is already in hand. Anything that could invalidate it drops it first, so a stale answer is\n * never applied to text it was not computed against.\n */\nexport class CodeActionController {\n private fix: lsp.CodeAction | null = null\n private seenDiagnostics: readonly lsp.Diagnostic[] | null = null\n private timer: ReturnType<typeof setTimeout> | null = null\n private abort: AbortController | null = null\n private applicationAbort: AbortController | null = null\n private requestId = 0\n private disposed = false\n\n public constructor(private readonly options: CodeActionControllerOptions) {}\n\n public update(kind: EditorViewContributionUpdateKind): void {\n if (kind === 'document' || kind === 'clear') {\n this.clear()\n return\n }\n if (kind !== 'selection' && kind !== 'content') return\n\n this.schedule()\n }\n\n /**\n * Reruns the oracle when the server republished the active document's diagnostics.\n *\n * Publishes the document sync refused — another file, or a version the buffer has moved past — leave\n * the list identical, and re-asking on those would answer for a document nobody is looking at.\n */\n public diagnosticsChanged(): void {\n const diagnostics = this.options.getDiagnostics()\n if (diagnostics === this.seenDiagnostics) return\n\n this.seenDiagnostics = diagnostics\n this.schedule()\n }\n\n /**\n * Applies the preferred quick fix at the caret.\n *\n * Reports handled as soon as the edit is on its way: an action the server has not filled in yet\n * needs a round trip, and returning false would let the keystroke fall through to another binding\n * after the fix was already committed to.\n */\n public applyAutoFix(): boolean {\n if (this.disposed) return false\n\n const active = this.options.getActiveDocument()\n if (!active) return false\n\n const fix = this.fix\n if (!fix) return false\n\n const provenance = this.options.router.provenanceOf(fix)\n if (!provenance) return false\n\n this.applicationAbort?.abort()\n const abort = new AbortController()\n this.applicationAbort = abort\n void this.runAction(active, fix, provenance, abort)\n return true\n }\n\n public dispose(): void {\n if (this.disposed) return\n\n this.disposed = true\n this.clear()\n }\n\n private schedule(): void {\n this.clear()\n /**\n * @justification Debounces a question to the language server against the caret and the\n * diagnostics, not against rendering, and `clear()` withdraws it on every further change — so a\n * fix is never computed against text the reader has already moved past.\n */\n this.timer = setTimeout(() => {\n this.timer = null\n void this.request()\n }, CODE_ACTION_DEBOUNCE_MS)\n }\n\n private clear(): void {\n if (this.timer) clearTimeout(this.timer)\n this.timer = null\n this.abort?.abort()\n this.abort = null\n this.applicationAbort?.abort()\n this.applicationAbort = null\n this.requestId += 1\n this.fix = null\n }\n\n private async request(): Promise<void> {\n const active = this.options.getActiveDocument()\n if (!active) return\n if (!this.options.router.hasReady('codeActions', 'textDocument/codeAction')) return\n\n const selection = this.options.context.getSnapshot().selections[0]\n if (!selection) return\n\n const range = codeActionAutoTriggerRange(active, selection.startOffset, selection.endOffset)\n if (!range) return\n\n const abort = new AbortController()\n const requestId = this.requestId + 1\n this.requestId = requestId\n this.abort = abort\n\n try {\n const response = await this.options.router.request<CodeActionResponse>(\n 'textDocument/codeAction',\n {\n context: {\n diagnostics: diagnosticsOverlapping(active, this.options.getDiagnostics(), range),\n // The auto fix can apply nothing else, so asking for the wider hierarchy would make\n // every settled keystroke pay for refactors that are thrown away on arrival.\n only: [CODE_ACTION_QUICK_FIX_KIND],\n triggerKind: CODE_ACTION_TRIGGER_AUTOMATIC,\n },\n range: {\n end: offsetToLspPositionInSnapshot(active, range.end),\n start: offsetToLspPositionInSnapshot(active, range.start),\n },\n textDocument: { uri: active.uri },\n },\n { signal: abort.signal },\n )\n if (requestId !== this.requestId) return\n if (this.disposed) return\n if (active !== this.options.getActiveDocument()) return\n\n // Narrowed again on arrival, because `only` is a hint the server is free to ignore.\n this.fix = preferredQuickFix(response, this.resolvesActions())\n } catch (error) {\n if (!isAbortError(error)) this.options.onRequestError(error)\n }\n }\n\n private async runAction(\n active: ActiveDocument,\n action: lsp.CodeAction,\n provenance: LanguageServerCodeActionProvenance,\n abort: AbortController,\n ): Promise<void> {\n try {\n if (action.command) {\n this.reportUnsupportedCommand(action)\n return\n }\n\n const owned = action.edit\n ? { action, ...provenance }\n : await this.options.router.resolveOwnedCodeAction(action, { signal: abort.signal })\n if (!owned) return\n if (!this.isCurrentApplication(active, abort)) return\n if (owned.action.command) {\n this.reportUnsupportedCommand(owned.action)\n return\n }\n\n await this.dispatchAction(active, owned.action, owned, abort.signal)\n } catch (error) {\n if (!isAbortError(error)) this.options.onRequestError(error)\n } finally {\n if (this.applicationAbort === abort) this.applicationAbort = null\n }\n }\n\n private resolvesActions(): boolean {\n return this.options.router.canResolveCodeActions()\n }\n\n private async dispatchAction(\n active: ActiveDocument,\n action: lsp.CodeAction,\n provenance: LanguageServerCodeActionProvenance,\n signal: AbortSignal,\n ): Promise<void> {\n if (!action.edit) {\n this.reportUnsupportedCommand(action)\n return\n }\n\n const parsed = parseWorkspaceEdit(action.edit)\n if (!parsed.ok) {\n this.options.onRequestError(new Error(parsed.error.reason))\n return\n }\n const origin = currentWorkspaceEditOrigin(provenance.guard, active, parsed.value)\n if (!origin) return\n if (signal.aborted) return\n\n const apply = provenance.lane.onApplyWorkspaceEdit\n if (!apply) {\n this.options.onRequestError(\n new Error(`\"${action.title}\" cannot be applied without a workspace edit host.`),\n )\n return\n }\n\n const result = await apply({\n guard: provenance.guard,\n label: action.title,\n logicalRevisionScope: provenance.lane.connection.logicalRevisionScope,\n originUri: active.uri,\n originVersion: origin.version,\n plan: parsed.value,\n serverId: provenance.lane.id,\n signal,\n source: 'code-action',\n })\n this.reportHostFailure(result)\n }\n\n private isCurrentApplication(active: ActiveDocument, abort: AbortController): boolean {\n if (this.disposed) return false\n if (abort.signal.aborted) return false\n return active === this.options.getActiveDocument()\n }\n\n private reportUnsupportedCommand(action: lsp.CodeAction): void {\n this.options.onRequestError(\n new Error(`\"${action.title}\" includes a server command, which this editor cannot run.`),\n )\n }\n\n private reportHostFailure(result: ApplyWorkspaceEditResult): void {\n if (result.status !== 'failed') return\n this.options.onRequestError(new Error(`${result.code}: ${result.message}`))\n }\n}\n\n/** A literal's own `command` is an object; only the bare form names its command with a string. */\nfunction isCommand(entry: lsp.Command | lsp.CodeAction): entry is lsp.Command {\n return typeof (entry as lsp.Command).command === 'string'\n}\n\n/**\n * Whether `kind` is the quick-fix kind or sits beneath it in the dotted hierarchy.\n *\n * Containment, not equality: a server that names its actions precisely answers `quickfix.import`,\n * and comparing the strings whole would reject every one of those. An action that named no kind at\n * all placed itself nowhere, so it is not in this subtree either.\n */\nfunction isQuickFixKind(kind: string | undefined): boolean {\n if (kind === undefined) return false\n\n return kind === CODE_ACTION_QUICK_FIX_KIND || kind.startsWith(`${CODE_ACTION_QUICK_FIX_KIND}.`)\n}\n\nfunction diagnosticsOverlapping(\n document: LspTextDocumentSnapshot,\n diagnostics: readonly lsp.Diagnostic[],\n range: OffsetRange,\n): lsp.Diagnostic[] {\n return diagnostics.filter((diagnostic) => {\n const start = lspPositionToOffsetInSnapshot(document, diagnostic.range.start)\n const end = lspPositionToOffsetInSnapshot(document, diagnostic.range.end)\n return start <= range.end && end >= range.start\n })\n}\n\nfunction isWhitespace(character: string | undefined): boolean {\n return character !== undefined && /\\s/.test(character)\n}\n\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true\n if (!isRecord(error)) return false\n return error.name === 'LspRequestCancelledError'\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Resolve registered CSS colors against the editor that supplies their theme variables. */
|
|
2
|
+
export declare function readColorAlpha(source: HTMLElement, color: string): number;
|
|
3
|
+
export declare function colorAlpha(color: string): number;
|
|
4
|
+
//# sourceMappingURL=colorAlpha.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colorAlpha.d.ts","sourceRoot":"","sources":["../src/colorAlpha.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAWzE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWhD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/colorAlpha.ts
|
|
2
|
+
/** Resolve registered CSS colors against the editor that supplies their theme variables. */
|
|
3
|
+
function readColorAlpha(source, color) {
|
|
4
|
+
const probe = source.ownerDocument.createElement("span");
|
|
5
|
+
probe.style.cssText = "display:none";
|
|
6
|
+
probe.style.color = color;
|
|
7
|
+
source.append(probe);
|
|
8
|
+
try {
|
|
9
|
+
return colorAlpha(source.ownerDocument.defaultView?.getComputedStyle(probe).color ?? color);
|
|
10
|
+
} finally {
|
|
11
|
+
probe.remove();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function colorAlpha(color) {
|
|
15
|
+
if (color === "transparent") return 0;
|
|
16
|
+
if (/^#[\da-f]{4}$/i.test(color)) return Number.parseInt(color[4] + color[4], 16) / 255;
|
|
17
|
+
if (/^#[\da-f]{8}$/i.test(color)) return Number.parseInt(color.slice(7), 16) / 255;
|
|
18
|
+
let component = null;
|
|
19
|
+
if (color.includes("/")) component = color.slice(color.lastIndexOf("/") + 1, -1).trim();
|
|
20
|
+
if (/^rgba?\(/.test(color) && color.split(",").length === 4) component = color.slice(color.lastIndexOf(",") + 1, -1).trim();
|
|
21
|
+
if (component === null) return 1;
|
|
22
|
+
const alpha = Number.parseFloat(component) / (component.endsWith("%") ? 100 : 1);
|
|
23
|
+
return Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { colorAlpha, readColorAlpha };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=colorAlpha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colorAlpha.js","names":[],"sources":["../src/colorAlpha.ts"],"sourcesContent":["/** Resolve registered CSS colors against the editor that supplies their theme variables. */\nexport function readColorAlpha(source: HTMLElement, color: string): number {\n const probe = source.ownerDocument.createElement('span')\n probe.style.cssText = 'display:none'\n probe.style.color = color\n source.append(probe)\n try {\n const resolved = source.ownerDocument.defaultView?.getComputedStyle(probe).color ?? color\n return colorAlpha(resolved)\n } finally {\n probe.remove()\n }\n}\n\nexport function colorAlpha(color: string): number {\n if (color === 'transparent') return 0\n if (/^#[\\da-f]{4}$/i.test(color)) return Number.parseInt(color[4]! + color[4]!, 16) / 255\n if (/^#[\\da-f]{8}$/i.test(color)) return Number.parseInt(color.slice(7), 16) / 255\n let component: string | null = null\n if (color.includes('/')) component = color.slice(color.lastIndexOf('/') + 1, -1).trim()\n if (/^rgba?\\(/.test(color) && color.split(',').length === 4)\n component = color.slice(color.lastIndexOf(',') + 1, -1).trim()\n if (component === null) return 1\n const alpha = Number.parseFloat(component) / (component.endsWith('%') ? 100 : 1)\n return Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1\n}\n"],"mappings":";;AACA,SAAgB,eAAe,QAAqB,OAAuB;CACzE,MAAM,QAAQ,OAAO,cAAc,cAAc,MAAM;CACvD,MAAM,MAAM,UAAU;CACtB,MAAM,MAAM,QAAQ;CACpB,OAAO,OAAO,KAAK;CACnB,IAAI;EAEF,OAAO,WADU,OAAO,cAAc,aAAa,iBAAiB,KAAK,CAAC,CAAC,SAAS,KAC1D;CAC5B,UAAU;EACR,MAAM,OAAO;CACf;AACF;AAEA,SAAgB,WAAW,OAAuB;CAChD,IAAI,UAAU,eAAe,OAAO;CACpC,IAAI,iBAAiB,KAAK,KAAK,GAAG,OAAO,OAAO,SAAS,MAAM,KAAM,MAAM,IAAK,EAAE,IAAI;CACtF,IAAI,iBAAiB,KAAK,KAAK,GAAG,OAAO,OAAO,SAAS,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI;CAC/E,IAAI,YAA2B;CAC/B,IAAI,MAAM,SAAS,GAAG,GAAG,YAAY,MAAM,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,KAAK;CACtF,IAAI,WAAW,KAAK,KAAK,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,WAAW,GACxD,YAAY,MAAM,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,KAAK;CAC/D,IAAI,cAAc,MAAM,OAAO;CAC/B,MAAM,QAAQ,OAAO,WAAW,SAAS,KAAK,UAAU,SAAS,GAAG,IAAI,MAAM;CAC9E,OAAO,OAAO,SAAS,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI;AACpE"}
|