@platformos/codemirror-language-client 0.0.2
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/CHANGELOG.md +7 -0
- package/README.md +91 -0
- package/dist/esm/CodeMirrorLanguageClient.d.ts +60 -0
- package/dist/esm/CodeMirrorLanguageClient.js +80 -0
- package/dist/esm/CodeMirrorLanguageClient.js.map +1 -0
- package/dist/esm/LanguageClient.d.ts +112 -0
- package/dist/esm/LanguageClient.js +187 -0
- package/dist/esm/LanguageClient.js.map +1 -0
- package/dist/esm/extensions/client.d.ts +6 -0
- package/dist/esm/extensions/client.js +12 -0
- package/dist/esm/extensions/client.js.map +1 -0
- package/dist/esm/extensions/complete.d.ts +17 -0
- package/dist/esm/extensions/complete.js +202 -0
- package/dist/esm/extensions/complete.js.map +1 -0
- package/dist/esm/extensions/complete.spec.d.ts +1 -0
- package/dist/esm/extensions/complete.spec.js +189 -0
- package/dist/esm/extensions/complete.spec.js.map +1 -0
- package/dist/esm/extensions/documentHighlights.d.ts +14 -0
- package/dist/esm/extensions/documentHighlights.js +78 -0
- package/dist/esm/extensions/documentHighlights.js.map +1 -0
- package/dist/esm/extensions/documentHighlights.spec.d.ts +1 -0
- package/dist/esm/extensions/documentHighlights.spec.js +99 -0
- package/dist/esm/extensions/documentHighlights.spec.js.map +1 -0
- package/dist/esm/extensions/hover.d.ts +16 -0
- package/dist/esm/extensions/hover.js +49 -0
- package/dist/esm/extensions/hover.js.map +1 -0
- package/dist/esm/extensions/hover.spec.d.ts +1 -0
- package/dist/esm/extensions/hover.spec.js +59 -0
- package/dist/esm/extensions/hover.spec.js.map +1 -0
- package/dist/esm/extensions/index.d.ts +6 -0
- package/dist/esm/extensions/index.js +7 -0
- package/dist/esm/extensions/index.js.map +1 -0
- package/dist/esm/extensions/lspLinter.d.ts +23 -0
- package/dist/esm/extensions/lspLinter.js +104 -0
- package/dist/esm/extensions/lspLinter.js.map +1 -0
- package/dist/esm/extensions/lspLinter.spec.d.ts +1 -0
- package/dist/esm/extensions/lspLinter.spec.js +141 -0
- package/dist/esm/extensions/lspLinter.spec.js.map +1 -0
- package/dist/esm/extensions/snippet.d.ts +19 -0
- package/dist/esm/extensions/snippet.js +25 -0
- package/dist/esm/extensions/snippet.js.map +1 -0
- package/dist/esm/extensions/snippet.spec.d.ts +1 -0
- package/dist/esm/extensions/snippet.spec.js +23 -0
- package/dist/esm/extensions/snippet.spec.js.map +1 -0
- package/dist/esm/extensions/textDocumentSync.d.ts +4 -0
- package/dist/esm/extensions/textDocumentSync.js +109 -0
- package/dist/esm/extensions/textDocumentSync.js.map +1 -0
- package/dist/esm/extensions/textDocumentSync.spec.d.ts +1 -0
- package/dist/esm/extensions/textDocumentSync.spec.js +163 -0
- package/dist/esm/extensions/textDocumentSync.spec.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/test/MockClient.d.ts +18 -0
- package/dist/esm/test/MockClient.js +63 -0
- package/dist/esm/test/MockClient.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildInfo +1 -0
- package/dist/esm/utils/simpleStateField.d.ts +3 -0
- package/dist/esm/utils/simpleStateField.js +17 -0
- package/dist/esm/utils/simpleStateField.js.map +1 -0
- package/dist/umd/CodeMirrorLanguageClient.d.ts +60 -0
- package/dist/umd/CodeMirrorLanguageClient.js +94 -0
- package/dist/umd/CodeMirrorLanguageClient.js.map +1 -0
- package/dist/umd/LanguageClient.d.ts +112 -0
- package/dist/umd/LanguageClient.js +202 -0
- package/dist/umd/LanguageClient.js.map +1 -0
- package/dist/umd/extensions/client.d.ts +6 -0
- package/dist/umd/extensions/client.js +25 -0
- package/dist/umd/extensions/client.js.map +1 -0
- package/dist/umd/extensions/complete.d.ts +17 -0
- package/dist/umd/extensions/complete.js +217 -0
- package/dist/umd/extensions/complete.js.map +1 -0
- package/dist/umd/extensions/documentHighlights.d.ts +14 -0
- package/dist/umd/extensions/documentHighlights.js +93 -0
- package/dist/umd/extensions/documentHighlights.js.map +1 -0
- package/dist/umd/extensions/hover.d.ts +16 -0
- package/dist/umd/extensions/hover.js +64 -0
- package/dist/umd/extensions/hover.js.map +1 -0
- package/dist/umd/extensions/index.d.ts +6 -0
- package/dist/umd/extensions/index.js +36 -0
- package/dist/umd/extensions/index.js.map +1 -0
- package/dist/umd/extensions/lspLinter.d.ts +23 -0
- package/dist/umd/extensions/lspLinter.js +119 -0
- package/dist/umd/extensions/lspLinter.js.map +1 -0
- package/dist/umd/extensions/snippet.d.ts +19 -0
- package/dist/umd/extensions/snippet.js +38 -0
- package/dist/umd/extensions/snippet.js.map +1 -0
- package/dist/umd/extensions/textDocumentSync.d.ts +4 -0
- package/dist/umd/extensions/textDocumentSync.js +122 -0
- package/dist/umd/extensions/textDocumentSync.js.map +1 -0
- package/dist/umd/index.d.ts +2 -0
- package/dist/umd/index.js +29 -0
- package/dist/umd/index.js.map +1 -0
- package/dist/umd/test/MockClient.d.ts +18 -0
- package/dist/umd/test/MockClient.js +77 -0
- package/dist/umd/test/MockClient.js.map +1 -0
- package/dist/umd/tsconfig.tsbuildInfo +1 -0
- package/dist/umd/utils/simpleStateField.d.ts +3 -0
- package/dist/umd/utils/simpleStateField.js +30 -0
- package/dist/umd/utils/simpleStateField.js.map +1 -0
- package/package.json +67 -0
- package/playground/src/index.html +10 -0
- package/playground/src/language-server-worker.ts +82 -0
- package/playground/src/playground.ts +251 -0
- package/playground/tsconfig.json +26 -0
- package/playground/webpack.config.js +85 -0
- package/src/CodeMirrorLanguageClient.ts +179 -0
- package/src/LanguageClient.ts +329 -0
- package/src/extensions/client.ts +17 -0
- package/src/extensions/complete.spec.ts +200 -0
- package/src/extensions/complete.ts +274 -0
- package/src/extensions/documentHighlights.spec.ts +111 -0
- package/src/extensions/documentHighlights.ts +91 -0
- package/src/extensions/hover.spec.ts +68 -0
- package/src/extensions/hover.ts +66 -0
- package/src/extensions/index.ts +19 -0
- package/src/extensions/lspLinter.spec.ts +156 -0
- package/src/extensions/lspLinter.ts +154 -0
- package/src/extensions/snippet.spec.ts +31 -0
- package/src/extensions/snippet.ts +42 -0
- package/src/extensions/textDocumentSync.spec.ts +188 -0
- package/src/extensions/textDocumentSync.ts +138 -0
- package/src/index.ts +2 -0
- package/src/test/MockClient.ts +96 -0
- package/src/utils/simpleStateField.ts +22 -0
- package/tsconfig.json +18 -0
- package/tsconfig.umd.json +9 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { EditorState, Extension } from '@codemirror/state';
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { MockClient } from '../test/MockClient';
|
|
6
|
+
import { clientFacet, fileUriFacet } from './client';
|
|
7
|
+
import { hover, hoverRendererFacet, lspHover } from './hover';
|
|
8
|
+
import { textDocumentSync } from './textDocumentSync';
|
|
9
|
+
import { Hover } from 'vscode-languageserver-protocol';
|
|
10
|
+
|
|
11
|
+
describe('Module: hover', () => {
|
|
12
|
+
const fileUri = 'browser://input.liquid';
|
|
13
|
+
let client: MockClient;
|
|
14
|
+
let extensions: Extension;
|
|
15
|
+
let view: EditorView;
|
|
16
|
+
let hoverRenderer: any;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
client = new MockClient();
|
|
20
|
+
hoverRenderer = vi.fn().mockReturnValue(null);
|
|
21
|
+
extensions = [
|
|
22
|
+
clientFacet.of(client),
|
|
23
|
+
fileUriFacet.of(fileUri),
|
|
24
|
+
textDocumentSync,
|
|
25
|
+
hoverRendererFacet.of(hoverRenderer),
|
|
26
|
+
lspHover(),
|
|
27
|
+
];
|
|
28
|
+
view = new EditorView({
|
|
29
|
+
state: EditorState.create({
|
|
30
|
+
doc: 'hello world',
|
|
31
|
+
extensions,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should translate LSP hover requests into CodeMirror hover tooltips', async () => {
|
|
37
|
+
const promise = hover(view, /* doesnt matter for unit test */ 0, 1);
|
|
38
|
+
|
|
39
|
+
const hoverResponse: Hover = {
|
|
40
|
+
contents: {
|
|
41
|
+
kind: 'markdown',
|
|
42
|
+
value: '### echo hello world',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
client.resolveRequest(hoverResponse);
|
|
47
|
+
const result = await promise;
|
|
48
|
+
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
pos: 0,
|
|
51
|
+
end: 5,
|
|
52
|
+
above: true,
|
|
53
|
+
arrow: false,
|
|
54
|
+
create: expect.any(Function),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
result!.create(view);
|
|
58
|
+
|
|
59
|
+
expect(hoverRenderer).toHaveBeenCalledWith(view, hoverResponse);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return null if nothing comes back', async () => {
|
|
63
|
+
const promise = hover(view, 0, 1);
|
|
64
|
+
client.resolveRequest(null);
|
|
65
|
+
const result = await promise;
|
|
66
|
+
expect(result).to.eql(null);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Hover, HoverRequest } from 'vscode-languageserver-protocol';
|
|
2
|
+
import { clientFacet, fileUriFacet } from './client';
|
|
3
|
+
import { textDocumentField } from './textDocumentSync';
|
|
4
|
+
import { Facet } from '@codemirror/state';
|
|
5
|
+
import { EditorView, Tooltip, TooltipView, hoverTooltip } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
type SecondArgType<F> = F extends (_: any, arg: infer A) => any ? A : never;
|
|
8
|
+
export type HoverOptions = Partial<SecondArgType<typeof hoverTooltip>>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A HoverRenderer would be equivalent to the Hover window in VS Code. It
|
|
12
|
+
* shows the documentation for the symbol under the cursor.
|
|
13
|
+
*
|
|
14
|
+
* Takes a LSP Hover as argument and must return a TooltipView (type {} and autocomplete for required params).
|
|
15
|
+
*/
|
|
16
|
+
export type HoverRenderer = (view: EditorView, hover: Hover) => TooltipView;
|
|
17
|
+
|
|
18
|
+
export const hoverRendererFacet = Facet.define<
|
|
19
|
+
HoverRenderer | undefined,
|
|
20
|
+
HoverRenderer | undefined
|
|
21
|
+
>({
|
|
22
|
+
static: true,
|
|
23
|
+
combine: (values) => values[0] ?? undefined,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const lspHover = (overrides: HoverOptions = {}) =>
|
|
27
|
+
hoverTooltip(hover, {
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export async function hover(view: EditorView, pos: number, _side: -1 | 1): Promise<Tooltip | null> {
|
|
32
|
+
const client = view.state.facet(clientFacet.reader);
|
|
33
|
+
const fileUri = view.state.facet(fileUriFacet.reader);
|
|
34
|
+
const hoverRenderer = view.state.facet(hoverRendererFacet.reader);
|
|
35
|
+
const textDocument = view.state.field(textDocumentField);
|
|
36
|
+
|
|
37
|
+
if (!hoverRenderer) return null;
|
|
38
|
+
|
|
39
|
+
const result = await client.sendRequest(HoverRequest.type, {
|
|
40
|
+
textDocument: { uri: fileUri },
|
|
41
|
+
position: textDocument.positionAt(pos),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// No results
|
|
45
|
+
if (result === null) return null;
|
|
46
|
+
|
|
47
|
+
let { from, to, text } = view.state.doc.lineAt(pos);
|
|
48
|
+
let start = pos;
|
|
49
|
+
let end = pos;
|
|
50
|
+
if (result.range) {
|
|
51
|
+
start = textDocument.offsetAt(result.range.start);
|
|
52
|
+
end = textDocument.offsetAt(result.range.end);
|
|
53
|
+
} else {
|
|
54
|
+
// basic "current word" algo
|
|
55
|
+
while (start > from && /\w/.test(text[start - from - 1])) start--;
|
|
56
|
+
while (end < to && /\w/.test(text[end - from])) end++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
pos: start,
|
|
61
|
+
end: end,
|
|
62
|
+
above: true,
|
|
63
|
+
create: (view) => hoverRenderer(view, result),
|
|
64
|
+
arrow: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { clientFacet, fileUriFacet, serverCapabilitiesFacet } from './client';
|
|
2
|
+
export {
|
|
3
|
+
AutocompleteOptions,
|
|
4
|
+
InfoRenderer,
|
|
5
|
+
complete,
|
|
6
|
+
infoRendererFacet,
|
|
7
|
+
lspComplete,
|
|
8
|
+
} from './complete';
|
|
9
|
+
export { documentHighlightsClass, lspDocumentHighlights } from './documentHighlights';
|
|
10
|
+
export { HoverOptions, HoverRenderer, hover, lspHover } from './hover';
|
|
11
|
+
export {
|
|
12
|
+
DiagnosticRenderer,
|
|
13
|
+
LinterOptions,
|
|
14
|
+
diagnosticRendererFacet,
|
|
15
|
+
diagnosticsFacet,
|
|
16
|
+
lspDiagnosticsField,
|
|
17
|
+
lspLinter,
|
|
18
|
+
} from './lspLinter';
|
|
19
|
+
export { textDocumentField, textDocumentSync } from './textDocumentSync';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { EditorState, Extension } from '@codemirror/state';
|
|
4
|
+
import { EditorView } from '@codemirror/view';
|
|
5
|
+
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint';
|
|
6
|
+
import { diagnosticsFacet, lspLinter } from './lspLinter';
|
|
7
|
+
import { clientFacet, fileUriFacet } from './client';
|
|
8
|
+
import { MockClient } from '../test/MockClient';
|
|
9
|
+
import { DiagnosticSeverity, PublishDiagnosticsNotification } from 'vscode-languageserver-protocol';
|
|
10
|
+
|
|
11
|
+
describe('Module: lspLinter', () => {
|
|
12
|
+
const fileUri = 'browser://input.liquid';
|
|
13
|
+
let client: MockClient;
|
|
14
|
+
let extensions: Extension;
|
|
15
|
+
let view: EditorView;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
client = new MockClient();
|
|
19
|
+
extensions = [clientFacet.of(client), fileUriFacet.of(fileUri), lspLinter()];
|
|
20
|
+
view = new EditorView({
|
|
21
|
+
state: EditorState.create({
|
|
22
|
+
doc: 'hello world',
|
|
23
|
+
extensions,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
view.destroy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('When it receives up to date diagnostics from the server', () => {
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
client.triggerNotification(PublishDiagnosticsNotification.type, {
|
|
35
|
+
uri: fileUri,
|
|
36
|
+
version: 0,
|
|
37
|
+
diagnostics: [
|
|
38
|
+
{
|
|
39
|
+
message: 'hello not accepted',
|
|
40
|
+
severity: DiagnosticSeverity.Error,
|
|
41
|
+
range: {
|
|
42
|
+
start: {
|
|
43
|
+
line: 0,
|
|
44
|
+
character: 0,
|
|
45
|
+
},
|
|
46
|
+
end: {
|
|
47
|
+
line: 0,
|
|
48
|
+
character: 'hello'.length,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should make them available in the diagnosticState', () => {
|
|
57
|
+
const codeMirrorDiagnostics = view.state.facet(diagnosticsFacet.reader);
|
|
58
|
+
expect(codeMirrorDiagnostics).to.have.lengthOf(1);
|
|
59
|
+
const expectedDiagnostic: CodeMirrorDiagnostic = {
|
|
60
|
+
from: 0,
|
|
61
|
+
to: 'hello'.length,
|
|
62
|
+
message: 'hello not accepted',
|
|
63
|
+
renderMessage: undefined,
|
|
64
|
+
severity: 'error',
|
|
65
|
+
};
|
|
66
|
+
expect(codeMirrorDiagnostics[0]).to.eql(expectedDiagnostic);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('When it receives out of date diagnostics from the server', () => {
|
|
71
|
+
beforeEach(async () => {
|
|
72
|
+
// Here we dispatch a doc change, this internally increments the
|
|
73
|
+
// version value of the textDocument by 1.
|
|
74
|
+
view.dispatch(
|
|
75
|
+
view.state.update({
|
|
76
|
+
changes: {
|
|
77
|
+
from: 0,
|
|
78
|
+
to: 'hello world'.indexOf(' '),
|
|
79
|
+
insert: 'hi',
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Here we dispatch a set of diagnostics for the old state
|
|
85
|
+
client.triggerNotification(PublishDiagnosticsNotification.type, {
|
|
86
|
+
uri: fileUri,
|
|
87
|
+
version: 0, // Note: since the file was modified, those diagnostics are out of date
|
|
88
|
+
diagnostics: [
|
|
89
|
+
{
|
|
90
|
+
message: 'hello not accepted',
|
|
91
|
+
severity: DiagnosticSeverity.Error,
|
|
92
|
+
range: {
|
|
93
|
+
start: {
|
|
94
|
+
line: 0,
|
|
95
|
+
character: 0,
|
|
96
|
+
},
|
|
97
|
+
end: {
|
|
98
|
+
line: 0,
|
|
99
|
+
character: 'hello'.length,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should discard the stale diagnostics', () => {
|
|
108
|
+
const codeMirrorDiagnostics = view.state.facet(diagnosticsFacet.reader);
|
|
109
|
+
expect(codeMirrorDiagnostics).to.have.lengthOf(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('When it changes the document and the diagnostics are suddenly stale', () => {
|
|
114
|
+
beforeEach(async () => {
|
|
115
|
+
// Here we dispatch a set of diagnostics for the state, they are up to date
|
|
116
|
+
client.triggerNotification(PublishDiagnosticsNotification.type, {
|
|
117
|
+
uri: fileUri,
|
|
118
|
+
version: 0,
|
|
119
|
+
diagnostics: [
|
|
120
|
+
{
|
|
121
|
+
message: 'hello not accepted',
|
|
122
|
+
severity: DiagnosticSeverity.Error,
|
|
123
|
+
range: {
|
|
124
|
+
start: {
|
|
125
|
+
line: 0,
|
|
126
|
+
character: 0,
|
|
127
|
+
},
|
|
128
|
+
end: {
|
|
129
|
+
line: 0,
|
|
130
|
+
character: 'hello'.length,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Here we dispatch a doc change. This internally increments the
|
|
138
|
+
// version value of the textDocument by 1. The diagnostics are now
|
|
139
|
+
// stale.
|
|
140
|
+
view.dispatch(
|
|
141
|
+
view.state.update({
|
|
142
|
+
changes: {
|
|
143
|
+
from: 0,
|
|
144
|
+
to: 'hello world'.indexOf(' '),
|
|
145
|
+
insert: 'hi',
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should discard the stale diagnostics', () => {
|
|
152
|
+
const codeMirrorDiagnostics = view.state.facet(diagnosticsFacet.reader);
|
|
153
|
+
expect(codeMirrorDiagnostics).to.have.lengthOf(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Diagnostic as CodeMirrorDiagnostic, linter } from '@codemirror/lint';
|
|
2
|
+
import { Extension, Facet, StateEffect } from '@codemirror/state';
|
|
3
|
+
import { EditorView, PluginValue, ViewPlugin } from '@codemirror/view';
|
|
4
|
+
import {
|
|
5
|
+
Disposable,
|
|
6
|
+
Diagnostic as LSPDiagnostic,
|
|
7
|
+
DiagnosticSeverity as LSPSeverity,
|
|
8
|
+
PublishDiagnosticsNotification,
|
|
9
|
+
} from 'vscode-languageserver-protocol';
|
|
10
|
+
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
11
|
+
|
|
12
|
+
import { simpleStateField } from '../utils/simpleStateField';
|
|
13
|
+
import { clientFacet, fileUriFacet } from './client';
|
|
14
|
+
import { textDocumentField } from './textDocumentSync';
|
|
15
|
+
|
|
16
|
+
class DiagnosticsPlugin implements PluginValue {
|
|
17
|
+
private handlers: Disposable[];
|
|
18
|
+
|
|
19
|
+
constructor(view: EditorView) {
|
|
20
|
+
const client = view.state.facet(clientFacet.reader);
|
|
21
|
+
const fileUri = view.state.facet(fileUriFacet.reader);
|
|
22
|
+
this.handlers = [];
|
|
23
|
+
this.handlers.push(
|
|
24
|
+
client.onNotification(PublishDiagnosticsNotification.type, (params) => {
|
|
25
|
+
if (params.uri !== fileUri) return;
|
|
26
|
+
view.dispatch({
|
|
27
|
+
effects: [
|
|
28
|
+
setLSPDiagnosticsVersion.of(params.version ?? null),
|
|
29
|
+
setLSPDiagnostics.of(params.diagnostics),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
destroy() {
|
|
37
|
+
this.handlers.forEach((disposable) => disposable.dispose());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const setLSPDiagnosticsVersion = StateEffect.define<number | null>();
|
|
42
|
+
export const lspDiagnosticsVersionField = simpleStateField(setLSPDiagnosticsVersion, null);
|
|
43
|
+
|
|
44
|
+
export const setLSPDiagnostics = StateEffect.define<LSPDiagnostic[]>();
|
|
45
|
+
export const lspDiagnosticsField = simpleStateField(setLSPDiagnostics, []);
|
|
46
|
+
|
|
47
|
+
export const diagnosticsFacet = Facet.define<CodeMirrorDiagnostic[], CodeMirrorDiagnostic[]>({
|
|
48
|
+
combine: (values) => values[0],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type DiagnosticRenderer = (lspDiagnostic: LSPDiagnostic) => Node;
|
|
52
|
+
|
|
53
|
+
export const diagnosticRendererFacet = Facet.define<
|
|
54
|
+
DiagnosticRenderer | undefined,
|
|
55
|
+
DiagnosticRenderer | undefined
|
|
56
|
+
>({
|
|
57
|
+
combine: (values) => (values.length > 0 ? values[0] : undefined),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const computedCodeMirrorDiagnosticsValueProvider = diagnosticsFacet.compute(
|
|
61
|
+
[textDocumentField, lspDiagnosticsField, lspDiagnosticsVersionField],
|
|
62
|
+
(state) => {
|
|
63
|
+
const doc = state.field(textDocumentField);
|
|
64
|
+
const lspDiagnosticsVersion = state.field(lspDiagnosticsVersionField);
|
|
65
|
+
|
|
66
|
+
// If the diagnostics version and doc version don't match, it means
|
|
67
|
+
// the diagnostics are stale.
|
|
68
|
+
if (lspDiagnosticsVersion !== doc.version) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lspDiagnostics = state.field(lspDiagnosticsField);
|
|
73
|
+
const diagnosticRenderer = state.facet(diagnosticRendererFacet.reader);
|
|
74
|
+
return lspDiagnostics.map((diagnostic) =>
|
|
75
|
+
lspToCodeMirrorDiagnostic(diagnostic, doc, diagnosticRenderer),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
type SecondArgType<F> = F extends (_: any, second: infer A) => any ? A : never;
|
|
81
|
+
|
|
82
|
+
export type LinterOptions = SecondArgType<typeof linter>;
|
|
83
|
+
|
|
84
|
+
export const diagnosticsLinter = (overrides: LinterOptions) =>
|
|
85
|
+
linter(
|
|
86
|
+
(view) => {
|
|
87
|
+
const diagnostics = view.state.facet(diagnosticsFacet.reader);
|
|
88
|
+
return diagnostics;
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
delay: 100,
|
|
92
|
+
needsRefresh(update) {
|
|
93
|
+
const currVersion = update.state.field(lspDiagnosticsVersionField);
|
|
94
|
+
const prevVersion = update.startState.field(lspDiagnosticsVersionField);
|
|
95
|
+
|
|
96
|
+
// Checking against any kind of changes otherwise the squiggly line disappears!!
|
|
97
|
+
return (
|
|
98
|
+
update.geometryChanged ||
|
|
99
|
+
update.viewportChanged ||
|
|
100
|
+
update.heightChanged ||
|
|
101
|
+
update.focusChanged ||
|
|
102
|
+
update.docChanged ||
|
|
103
|
+
update.transactions[0]?.reconfigured ||
|
|
104
|
+
prevVersion !== currVersion
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
...overrides,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
export const diagnosticsPlugin = ViewPlugin.fromClass(DiagnosticsPlugin);
|
|
111
|
+
|
|
112
|
+
export const lspLinter = (linterOptions: LinterOptions = {}): Extension => [
|
|
113
|
+
textDocumentField,
|
|
114
|
+
lspDiagnosticsField,
|
|
115
|
+
lspDiagnosticsVersionField,
|
|
116
|
+
computedCodeMirrorDiagnosticsValueProvider,
|
|
117
|
+
diagnosticsLinter(linterOptions),
|
|
118
|
+
diagnosticsPlugin,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
type CodeMirrorSeverity = 'info' | 'warning' | 'error';
|
|
122
|
+
function lspToCodeMirrorSeverity(severity: LSPSeverity | undefined): CodeMirrorSeverity {
|
|
123
|
+
switch (severity) {
|
|
124
|
+
case LSPSeverity.Error: {
|
|
125
|
+
return 'error';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case LSPSeverity.Warning: {
|
|
129
|
+
return 'warning';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case LSPSeverity.Hint:
|
|
133
|
+
case LSPSeverity.Information:
|
|
134
|
+
default: {
|
|
135
|
+
return 'info';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function lspToCodeMirrorDiagnostic(
|
|
141
|
+
lspDiagnostic: LSPDiagnostic,
|
|
142
|
+
textDocument: TextDocument,
|
|
143
|
+
diagnosticRenderer: DiagnosticRenderer | undefined,
|
|
144
|
+
): CodeMirrorDiagnostic {
|
|
145
|
+
const { range, message, severity } = lspDiagnostic;
|
|
146
|
+
const { start, end } = range;
|
|
147
|
+
return {
|
|
148
|
+
from: textDocument.offsetAt(start),
|
|
149
|
+
to: textDocument.offsetAt(end),
|
|
150
|
+
message,
|
|
151
|
+
severity: lspToCodeMirrorSeverity(severity),
|
|
152
|
+
renderMessage: diagnosticRenderer ? () => diagnosticRenderer(lspDiagnostic) : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { translateSnippet } from './snippet';
|
|
3
|
+
|
|
4
|
+
describe('Module: snippets', () => {
|
|
5
|
+
describe('Unit: translateSnippet', () => {
|
|
6
|
+
it('translate $0 to $99', () => {
|
|
7
|
+
expect(translateSnippet('$0')).to.equal('${99}');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('handles $1, $2, $3 propertly', () => {
|
|
11
|
+
expect(translateSnippet('def $1, $2, $3, $0 end')).to.equal(
|
|
12
|
+
'def ${1}, ${2}, ${3}, ${99} end',
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('handles placeholders properly', () => {
|
|
17
|
+
expect(translateSnippet('def ${1:hi}, ${2:hi}, $0 end')).to.equal(
|
|
18
|
+
'def ${1:hi}, ${2:hi}, ${99} end',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('appends $99 at the end of the string if $0 is not present', () => {
|
|
23
|
+
expect(translateSnippet('def $1 end')).to.equal('def ${1} end${99}');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// CodeMirror doesn't support VS Code's ${2:$1} syntax, so we gotta do something about it.
|
|
27
|
+
it('applies a fallback strategy when placeholders refer to other placeholders', () => {
|
|
28
|
+
expect(translateSnippet('${1:hi} ${2:oh$1no}')).to.equal('${1:hi} ${2:ohno}${99}');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MapMode, RangeSet, RangeValue, StateEffect, StateField, Range } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syntax is $1, ${2}, ${3:foo}, $0
|
|
5
|
+
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
|
|
6
|
+
*/
|
|
7
|
+
export type LSPSnippet = string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Syntax is ${placeholder} or ${1:placeholder} or #{} or ${}
|
|
11
|
+
* https://codemirror.net/docs/ref/#autocomplete.snippet
|
|
12
|
+
*/
|
|
13
|
+
export type CodeMirrorSnippet = string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function takes a snippet from the language server and transforms it
|
|
17
|
+
* into the format that CodeMirror expects.
|
|
18
|
+
*
|
|
19
|
+
* There are small nuances w.r.t. $0 but that shouldn't be too much of a
|
|
20
|
+
* problem. ($0 is the "end" cursor position in LSP, CM doesn't have that... so
|
|
21
|
+
* we convert $0 to ${99} :D)
|
|
22
|
+
*/
|
|
23
|
+
export function translateSnippet(snippet: LSPSnippet): CodeMirrorSnippet {
|
|
24
|
+
let fixed = snippet.replace(/\$(\d)+/g, (_match, index) => {
|
|
25
|
+
return '${' + (Number(index) === 0 ? '99' : index) + '}';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Remove references to other placeholders in placeholders. CodeMirror doesn't
|
|
29
|
+
// support those and it's a PITA to implement differently.
|
|
30
|
+
fixed = fixed.replace(
|
|
31
|
+
/\$\{(\d+):([^}$]*)(\$\{\d+\})([^}$]*)\}/g,
|
|
32
|
+
(_match, index, pre, ref, post) => {
|
|
33
|
+
return '${' + index + ':' + pre + post + '}';
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (fixed.includes('${99}')) {
|
|
38
|
+
return fixed;
|
|
39
|
+
} else {
|
|
40
|
+
return fixed + '${99}';
|
|
41
|
+
}
|
|
42
|
+
}
|