@kerebron/extension-lsp 0.4.28 → 0.4.29
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/esm/DiagnosticPlugin.js +1 -0
- package/esm/DiagnosticPlugin.js.map +1 -0
- package/esm/ExtensionLsp.js +1 -0
- package/esm/ExtensionLsp.js.map +1 -0
- package/esm/LSPClient.js +1 -0
- package/esm/LSPClient.js.map +1 -0
- package/esm/LspWebSocketTransport.js +1 -0
- package/esm/LspWebSocketTransport.js.map +1 -0
- package/esm/computeIncrementalChanges.js +1 -0
- package/esm/computeIncrementalChanges.js.map +1 -0
- package/esm/createLspAutocomplete.js +1 -0
- package/esm/createLspAutocomplete.js.map +1 -0
- package/esm/mod.js +1 -0
- package/esm/mod.js.map +1 -0
- package/esm/types.js +1 -0
- package/esm/types.js.map +1 -0
- package/esm/workspace.js +1 -0
- package/esm/workspace.js.map +1 -0
- package/package.json +8 -4
- package/src/DiagnosticPlugin.ts +196 -0
- package/src/ExtensionLsp.ts +118 -0
- package/src/LSPClient.ts +485 -0
- package/src/LspWebSocketTransport.ts +114 -0
- package/src/computeIncrementalChanges.ts +101 -0
- package/src/createLspAutocomplete.ts +95 -0
- package/src/mod.ts +3 -0
- package/src/types.ts +14 -0
- package/src/workspace.ts +192 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DefaultRenderer } from '@kerebron/extension-autocomplete/DefaultRenderer';
|
|
2
|
+
import { type CoreEditor, TextRange } from '@kerebron/editor';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type AutocompleteProps,
|
|
6
|
+
createRegexMatcher,
|
|
7
|
+
} from '@kerebron/extension-autocomplete';
|
|
8
|
+
|
|
9
|
+
import { ExtensionLsp } from './ExtensionLsp.js';
|
|
10
|
+
|
|
11
|
+
interface CompletionItem {
|
|
12
|
+
label: string;
|
|
13
|
+
detail: string;
|
|
14
|
+
insertText: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CustomRenderer extends DefaultRenderer<CompletionItem> {
|
|
18
|
+
override createListItem(item: CompletionItem, cnt: number) { // override
|
|
19
|
+
const li = document.createElement('li');
|
|
20
|
+
if (cnt === this.pos) {
|
|
21
|
+
li.classList.add('active');
|
|
22
|
+
}
|
|
23
|
+
li.innerText = item.label;
|
|
24
|
+
li.title = item.detail;
|
|
25
|
+
li.style.cursor = 'pointer';
|
|
26
|
+
li.addEventListener('click', () => {
|
|
27
|
+
this.command(item);
|
|
28
|
+
});
|
|
29
|
+
return li;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function cleanPlaceholders(input: string): string {
|
|
34
|
+
const regex = /\$\{\d+:([^}]+)}/g;
|
|
35
|
+
return input.replace(regex, '$1');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createLspAutocomplete(extensionLsp: ExtensionLsp) {
|
|
39
|
+
const editor: CoreEditor = extensionLsp.getEditor();
|
|
40
|
+
const renderer = new CustomRenderer(editor);
|
|
41
|
+
|
|
42
|
+
const config = {
|
|
43
|
+
renderer,
|
|
44
|
+
matchers: [createRegexMatcher([/\w+/, /(^|\s)@\w*/, /^#\w*/])],
|
|
45
|
+
getItems: async (query: string, props: AutocompleteProps) => {
|
|
46
|
+
const { mapper } = await extensionLsp.source.getMappedContent();
|
|
47
|
+
|
|
48
|
+
const lspPos = mapper.toRawTextLspPos(props.range.from);
|
|
49
|
+
|
|
50
|
+
const client = extensionLsp.getClient(extensionLsp.mainLang);
|
|
51
|
+
if (client) {
|
|
52
|
+
client.sync();
|
|
53
|
+
try {
|
|
54
|
+
const completions:
|
|
55
|
+
| { items: CompletionItem[] }
|
|
56
|
+
| Array<CompletionItem> = await client.request(
|
|
57
|
+
'textDocument/completion',
|
|
58
|
+
{
|
|
59
|
+
textDocument: { uri: extensionLsp.uri },
|
|
60
|
+
position: lspPos,
|
|
61
|
+
context: { triggerKind: 2, triggerCharacter: query },
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (Array.isArray(completions)) {
|
|
66
|
+
return completions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return completions.items;
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
console.error(err.message);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
onSelect: async (selected: CompletionItem, range: TextRange) => {
|
|
79
|
+
const rawText = cleanPlaceholders(selected.insertText);
|
|
80
|
+
const slice = await extensionLsp.extensionMarkdown.fromMarkdown(rawText);
|
|
81
|
+
|
|
82
|
+
if (slice.content.content.length === 1) {
|
|
83
|
+
const first = slice.content.content[0];
|
|
84
|
+
if (first.isBlock) {
|
|
85
|
+
editor.chain().insertBlockSmart(range.from, first).run();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
editor.chain().replaceRangeSlice(range, slice).run();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return { autocompleteConfig: config };
|
|
95
|
+
}
|
package/src/mod.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type LspPosition = { line: number; character: number };
|
|
2
|
+
export type LspRange = { start: LspPosition; end: LspPosition };
|
|
3
|
+
|
|
4
|
+
export interface Diagnostic {
|
|
5
|
+
range: LspRange;
|
|
6
|
+
severity?: number;
|
|
7
|
+
source?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PublishDiagnosticsParams {
|
|
12
|
+
uri: string;
|
|
13
|
+
diagnostics: Diagnostic[];
|
|
14
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type * as lsp from 'vscode-languageserver-protocol';
|
|
2
|
+
import { TextDocumentSyncKind } from 'vscode-languageserver-protocol';
|
|
3
|
+
|
|
4
|
+
import type { EditorUi } from '@kerebron/editor';
|
|
5
|
+
|
|
6
|
+
import { LSPClient } from './LSPClient.js';
|
|
7
|
+
import { PositionMapper } from '@kerebron/extension-markdown/PositionMapper';
|
|
8
|
+
import { computeIncrementalChanges } from './computeIncrementalChanges.js';
|
|
9
|
+
|
|
10
|
+
export interface LspSource {
|
|
11
|
+
ui: EditorUi;
|
|
12
|
+
getMappedContent(): Promise<{ content: string; mapper: PositionMapper }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkspaceFile {
|
|
16
|
+
uri: string;
|
|
17
|
+
languageId: string;
|
|
18
|
+
version: number;
|
|
19
|
+
content: string;
|
|
20
|
+
source: LspSource;
|
|
21
|
+
getUi(): EditorUi | undefined;
|
|
22
|
+
mapper: PositionMapper;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WorkspaceFileUpdate {
|
|
26
|
+
file: WorkspaceFile;
|
|
27
|
+
prevDoc: string;
|
|
28
|
+
changes: lsp.TextDocumentContentChangeEvent[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export abstract class Workspace {
|
|
32
|
+
abstract files: WorkspaceFile[];
|
|
33
|
+
protected isConnected = false;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
readonly client: LSPClient,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
getFile(uri: string): WorkspaceFile | null {
|
|
40
|
+
return this.files.find((f) => f.uri == uri) || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
abstract syncFiles(): readonly WorkspaceFileUpdate[];
|
|
44
|
+
|
|
45
|
+
requestFile(uri: string): Promise<WorkspaceFile | null> {
|
|
46
|
+
return Promise.resolve(this.getFile(uri));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
abstract openFile(uri: string, languageId: string, source: LspSource): void;
|
|
50
|
+
abstract changedFile(uri: string): void;
|
|
51
|
+
abstract closeFile(uri: string): void;
|
|
52
|
+
|
|
53
|
+
async connected(): Promise<void> {
|
|
54
|
+
this.isConnected = true;
|
|
55
|
+
for await (const file of this.files) {
|
|
56
|
+
const result = await this.client.didOpen(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnected(): void {
|
|
61
|
+
for (const file of this.files) {
|
|
62
|
+
this.client.workspace.closeFile(file.uri);
|
|
63
|
+
}
|
|
64
|
+
this.isConnected = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getUi(uri: string): Promise<EditorUi | undefined> {
|
|
68
|
+
const file = this.getFile(uri);
|
|
69
|
+
return Promise.resolve(file ? file.getUi() : undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class DefaultWorkspaceFile implements WorkspaceFile {
|
|
74
|
+
public syncedContent: string = '';
|
|
75
|
+
constructor(
|
|
76
|
+
readonly uri: string,
|
|
77
|
+
readonly languageId: string,
|
|
78
|
+
public version: number,
|
|
79
|
+
public content: string,
|
|
80
|
+
public mapper: PositionMapper,
|
|
81
|
+
public source: LspSource,
|
|
82
|
+
) {
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getUi() {
|
|
86
|
+
return this.source.ui;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class DefaultWorkspace extends Workspace {
|
|
91
|
+
files: DefaultWorkspaceFile[] = [];
|
|
92
|
+
private fileVersions: { [uri: string]: number } = Object.create(null);
|
|
93
|
+
|
|
94
|
+
nextFileVersion(uri: string) {
|
|
95
|
+
return this.fileVersions[uri] = (this.fileVersions[uri] ?? -1) + 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
syncFiles() {
|
|
99
|
+
if (!this.client.supportSync) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result: WorkspaceFileUpdate[] = [];
|
|
104
|
+
for (const file of this.files) {
|
|
105
|
+
this.changedFile(file.uri);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async changedFile(uri: string) {
|
|
112
|
+
const file = this.files.find((f) => f.uri == uri) || null;
|
|
113
|
+
|
|
114
|
+
if (file) {
|
|
115
|
+
const { content, mapper } = await file.source.getMappedContent();
|
|
116
|
+
|
|
117
|
+
if (!this.isConnected) {
|
|
118
|
+
file.content = content;
|
|
119
|
+
file.mapper = mapper;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
await this.client.notification<lsp.DidChangeTextDocumentParams>(
|
|
125
|
+
'textDocument/didChange',
|
|
126
|
+
{
|
|
127
|
+
textDocument: { uri: file.uri, version: file.version },
|
|
128
|
+
contentChanges: contentChangesFor(
|
|
129
|
+
file,
|
|
130
|
+
content,
|
|
131
|
+
mapper,
|
|
132
|
+
this.client.supportSync == TextDocumentSyncKind.Incremental,
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
) {
|
|
137
|
+
file.syncedContent = file.content;
|
|
138
|
+
file.content = content;
|
|
139
|
+
file.mapper = mapper;
|
|
140
|
+
file.version = this.nextFileVersion(file.uri);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async openFile(uri: string, languageId: string, source: LspSource) {
|
|
146
|
+
// if (uri) {}
|
|
147
|
+
|
|
148
|
+
if (this.getFile(uri)) {
|
|
149
|
+
this.closeFile(uri);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const mappedContent = await source.getMappedContent();
|
|
153
|
+
const { content, mapper } = mappedContent;
|
|
154
|
+
const file = new DefaultWorkspaceFile(
|
|
155
|
+
uri,
|
|
156
|
+
languageId,
|
|
157
|
+
this.nextFileVersion(uri),
|
|
158
|
+
content,
|
|
159
|
+
mapper,
|
|
160
|
+
source,
|
|
161
|
+
);
|
|
162
|
+
this.files.push(file);
|
|
163
|
+
|
|
164
|
+
this.client.didOpen(file);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
closeFile(uri: string) {
|
|
168
|
+
const file = this.getFile(uri);
|
|
169
|
+
if (file) {
|
|
170
|
+
this.files = this.files.filter((f) => f.uri !== uri);
|
|
171
|
+
this.client.didClose(uri);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const enum Sync {
|
|
177
|
+
AlwaysIfSmaller = 1024,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function contentChangesFor(
|
|
181
|
+
file: WorkspaceFile,
|
|
182
|
+
newContent: string,
|
|
183
|
+
mapper: PositionMapper,
|
|
184
|
+
supportInc: boolean,
|
|
185
|
+
): lsp.TextDocumentContentChangeEvent[] {
|
|
186
|
+
if (!supportInc || newContent.length < Sync.AlwaysIfSmaller) {
|
|
187
|
+
return [{ text: newContent }];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const changes = computeIncrementalChanges(file.content, newContent);
|
|
191
|
+
return changes.reverse();
|
|
192
|
+
}
|