@difizen/libro-codemirror 0.1.1 → 0.1.3
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/es/auto-complete/filter.d.ts.map +1 -1
- package/es/auto-complete/filter.js +1 -7
- package/es/auto-complete/index.d.ts.map +1 -1
- package/es/auto-complete/index.js +5 -9
- package/es/auto-complete/snippet.d.ts.map +1 -1
- package/es/auto-complete/snippet.js +2 -2
- package/es/auto-complete/state.d.ts.map +1 -1
- package/es/auto-complete/state.js +5 -16
- package/es/auto-complete/view.d.ts +5 -0
- package/es/auto-complete/view.d.ts.map +1 -1
- package/es/auto-complete/view.js +2 -13
- package/es/completion.js +1 -1
- package/es/config.d.ts +15 -10
- package/es/config.d.ts.map +1 -1
- package/es/config.js +15 -6
- package/es/editor-contribution.d.ts +8 -0
- package/es/editor-contribution.d.ts.map +1 -0
- package/es/editor-contribution.js +30 -0
- package/es/editor.d.ts +40 -10
- package/es/editor.d.ts.map +1 -1
- package/es/editor.js +241 -45
- package/es/indentation-markers/map.d.ts +9 -9
- package/es/indentation-markers/map.d.ts.map +1 -1
- package/es/index.d.ts +3 -1
- package/es/index.d.ts.map +1 -1
- package/es/index.js +3 -1
- package/es/libro-icon.d.ts +2 -2
- package/es/libro-icon.d.ts.map +1 -1
- package/es/libro-icon.js +2 -2
- package/es/lsp/completion.d.ts +5 -0
- package/es/lsp/completion.d.ts.map +1 -0
- package/es/lsp/completion.js +245 -0
- package/es/lsp/format.d.ts +7 -0
- package/es/lsp/format.d.ts.map +1 -0
- package/es/lsp/format.js +193 -0
- package/es/lsp/index.d.ts +7 -0
- package/es/lsp/index.d.ts.map +1 -0
- package/es/lsp/index.js +6 -0
- package/es/lsp/lint.d.ts +3 -0
- package/es/lsp/lint.d.ts.map +1 -0
- package/es/lsp/lint.js +114 -0
- package/es/lsp/protocol.d.ts +7 -0
- package/es/lsp/protocol.d.ts.map +1 -0
- package/es/lsp/protocol.js +1 -0
- package/es/lsp/tooltip.d.ts +3 -0
- package/es/lsp/tooltip.d.ts.map +1 -0
- package/es/lsp/tooltip.js +113 -0
- package/es/lsp/util.d.ts +15 -0
- package/es/lsp/util.d.ts.map +1 -0
- package/es/lsp/util.js +58 -0
- package/es/mode.d.ts.map +1 -1
- package/es/module.d.ts +3 -0
- package/es/module.d.ts.map +1 -0
- package/es/module.js +4 -0
- package/es/theme.js +4 -5
- package/es/tooltip.d.ts.map +1 -1
- package/es/tooltip.js +2 -4
- package/package.json +7 -5
- package/src/auto-complete/filter.ts +5 -7
- package/src/auto-complete/index.ts +6 -7
- package/src/auto-complete/snippet.ts +8 -2
- package/src/auto-complete/state.ts +13 -18
- package/src/auto-complete/view.ts +7 -13
- package/src/completion.ts +2 -2
- package/src/config.ts +40 -28
- package/src/editor-contribution.ts +17 -0
- package/src/editor.ts +226 -50
- package/src/hyperlink.ts +1 -1
- package/src/indentation-markers/index.ts +3 -3
- package/src/indentation-markers/map.ts +9 -9
- package/src/index.ts +4 -1
- package/src/libro-icon.tsx +4 -0
- package/src/lsp/completion.ts +175 -0
- package/src/lsp/format.ts +144 -0
- package/src/lsp/index.ts +6 -0
- package/src/lsp/lint.ts +125 -0
- package/src/lsp/protocol.ts +8 -0
- package/src/lsp/tooltip.ts +76 -0
- package/src/lsp/util.ts +69 -0
- package/src/mode.ts +1 -1
- package/src/module.ts +8 -0
- package/src/theme.ts +4 -4
- package/src/tooltip.ts +2 -4
- package/src/libro-icon.ts +0 -4
|
@@ -22,19 +22,19 @@ export interface IndentEntry {
|
|
|
22
22
|
*/
|
|
23
23
|
export class IndentationMap {
|
|
24
24
|
/** The {@link EditorState} indentation is derived from. */
|
|
25
|
-
|
|
25
|
+
protected state: EditorState;
|
|
26
26
|
|
|
27
27
|
/** The set of lines that are used as an entrypoint. */
|
|
28
|
-
|
|
28
|
+
protected lines: Set<Line>;
|
|
29
29
|
|
|
30
30
|
/** The internal mapping of line numbers to {@link IndentEntry} objects. */
|
|
31
|
-
|
|
31
|
+
protected map: Map<number, IndentEntry>;
|
|
32
32
|
|
|
33
33
|
/** The width of the editor's indent unit. */
|
|
34
|
-
|
|
34
|
+
protected unitWidth: number;
|
|
35
35
|
|
|
36
36
|
/** The type of indentation to use (terminate at end of scope vs last non-empty line in scope) */
|
|
37
|
-
|
|
37
|
+
protected markerType: 'fullScope' | 'codeOnly';
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @param lines - The set of lines to get the indentation map for.
|
|
@@ -96,7 +96,7 @@ export class IndentationMap {
|
|
|
96
96
|
* @param col - The visual beginning whitespace width of the line.
|
|
97
97
|
* @param level - The indentation level of the line.
|
|
98
98
|
*/
|
|
99
|
-
|
|
99
|
+
protected set(line: Line, col: number, level: number) {
|
|
100
100
|
const empty = !line.text.trim().length;
|
|
101
101
|
const entry: IndentEntry = { line, col, level, empty };
|
|
102
102
|
this.map.set(entry.line.number, entry);
|
|
@@ -109,7 +109,7 @@ export class IndentationMap {
|
|
|
109
109
|
*
|
|
110
110
|
* @param line - The {@link Line} to add to the map.
|
|
111
111
|
*/
|
|
112
|
-
|
|
112
|
+
protected add(line: Line) {
|
|
113
113
|
if (this.has(line)) {
|
|
114
114
|
return this.get(line);
|
|
115
115
|
}
|
|
@@ -165,7 +165,7 @@ export class IndentationMap {
|
|
|
165
165
|
* @param from - The {@link Line} to start from.
|
|
166
166
|
* @param dir - The direction to search in. Either `1` or `-1`.
|
|
167
167
|
*/
|
|
168
|
-
|
|
168
|
+
protected closestNonEmpty(from: Line, dir: -1 | 1) {
|
|
169
169
|
let lineNo = from.number + dir;
|
|
170
170
|
|
|
171
171
|
while (dir === -1 ? lineNo >= 1 : lineNo <= this.state.doc.lines) {
|
|
@@ -205,7 +205,7 @@ export class IndentationMap {
|
|
|
205
205
|
* Finds the state's active block (via the current selection) and sets all
|
|
206
206
|
* the active indent level for the lines in the block.
|
|
207
207
|
*/
|
|
208
|
-
|
|
208
|
+
protected findAndSetActiveLines() {
|
|
209
209
|
const currentLine = getCurrentLine(this.state);
|
|
210
210
|
|
|
211
211
|
if (!this.has(currentLine)) {
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,11 @@ import './style/variables.css';
|
|
|
4
4
|
|
|
5
5
|
export * from './config.js';
|
|
6
6
|
export * from './editor.js';
|
|
7
|
+
export * from './lsp/index.js';
|
|
7
8
|
export * from './mode.js';
|
|
8
|
-
export * from './
|
|
9
|
+
export * from './module.js';
|
|
9
10
|
export * from './factory.js';
|
|
10
11
|
export * from './monitor.js';
|
|
12
|
+
export * from './theme.js';
|
|
13
|
+
|
|
11
14
|
export * from './auto-complete/index.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const FoldIcon =
|
|
2
|
+
'<svg width="8px" height="6px" viewBox="0 0 8 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>1.通用/2.Icon图标/Line/Down</title><g id="0424" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Notebook-cell-色阶" transform="translate(-2015.000000, -434.000000)" fill="#A4AECB"><g id="编组-15" transform="translate(2004.000000, 407.000000)"><g id="1.通用/2.Icon图标/Line/Down" transform="translate(11.250000, 27.500000)"><path d="M7.34387369,0 L6.61145181,0 C6.56164712,0 6.51477212,0.0244140625 6.48547525,0.064453125 L3.71106119,3.88867188 L0.936647123,0.064453125 C0.907350248,0.0244140625 0.860475248,0 0.81067056,0 L0.0782486852,0 C0.0147721227,0 -0.0223372523,0.072265625 0.0147721227,0.124023438 L3.4581315,4.87109375 C3.5831315,5.04296875 3.83899087,5.04296875 3.96301431,4.87109375 L7.40637369,0.124023437 C7.44445962,0.072265625 7.40735025,0 7.34387369,0 Z" id="Down"></path></g></g></g></g></svg>';
|
|
3
|
+
export const UnFoldIcon =
|
|
4
|
+
'<svg width="6px" height="8px" viewBox="0 0 6 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>1.通用/2.Icon图标/Line/Down收起</title><g id="0424" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Notebook-cell-色阶" transform="translate(-2094.000000, -433.000000)" fill="#A4AECB"><g id="编组-15备份" transform="translate(2082.000000, 407.000000)"><g id="1.通用/2.Icon图标/Line/Down" transform="translate(15.000000, 30.039124) rotate(270.000000) translate(-15.000000, -30.039124) translate(11.289124, 27.539124)"><path d="M7.34387369,1.77635684e-15 L6.61145181,1.77635684e-15 C6.56164712,1.77635684e-15 6.51477212,0.0244140625 6.48547525,0.064453125 L3.71106119,3.88867188 L0.936647123,0.064453125 C0.907350248,0.0244140625 0.860475248,1.77635684e-15 0.81067056,1.77635684e-15 L0.0782486852,1.77635684e-15 C0.0147721227,1.77635684e-15 -0.0223372523,0.072265625 0.0147721227,0.124023438 L3.4581315,4.87109375 C3.5831315,5.04296875 3.83899087,5.04296875 3.96301431,4.87109375 L7.40637369,0.124023438 C7.44445962,0.072265625 7.40735025,1.77635684e-15 7.34387369,1.77635684e-15 Z" id="Down"></path></g></g></g></g></svg>';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { pythonLanguage } from '@codemirror/lang-python';
|
|
2
|
+
import { CompletionItemKind, CompletionTriggerKind } from '@difizen/libro-lsp';
|
|
3
|
+
|
|
4
|
+
import type { Completion, CompletionSource } from '../auto-complete/index.js';
|
|
5
|
+
|
|
6
|
+
import type { CMLSPExtension } from './protocol.js';
|
|
7
|
+
import { offsetToPos, renderMarkupContent } from './util.js';
|
|
8
|
+
|
|
9
|
+
export type CompletionItemDetailReolve = (
|
|
10
|
+
completion: Completion,
|
|
11
|
+
) => Node | null | Promise<Node | null>;
|
|
12
|
+
|
|
13
|
+
const CompletionItemKindMap = Object.fromEntries(
|
|
14
|
+
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]),
|
|
15
|
+
) as Record<CompletionItemKind, string>;
|
|
16
|
+
|
|
17
|
+
function toSet(chars: Set<string>) {
|
|
18
|
+
let preamble = '';
|
|
19
|
+
let flat = Array.from(chars).join('');
|
|
20
|
+
const words = /\w/.test(flat);
|
|
21
|
+
if (words) {
|
|
22
|
+
preamble += '\\w';
|
|
23
|
+
flat = flat.replace(/\w/g, '');
|
|
24
|
+
}
|
|
25
|
+
return `[${preamble}${flat.replace(/[^\w\s]/g, '\\$&')}]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function prefixMatch(options: Completion[]) {
|
|
29
|
+
const first = new Set<string>();
|
|
30
|
+
const rest = new Set<string>();
|
|
31
|
+
|
|
32
|
+
for (const { apply } of options) {
|
|
33
|
+
const [initial, ...restStr] = apply as string;
|
|
34
|
+
first.add(initial);
|
|
35
|
+
for (const char of restStr) {
|
|
36
|
+
rest.add(char);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const source = toSet(first) + toSet(rest) + '*$';
|
|
41
|
+
return [new RegExp('^' + source), new RegExp(source)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const lspPythonCompletion: CMLSPExtension = ({ lspProvider }) => {
|
|
45
|
+
const completionSource: CompletionSource = async (context) => {
|
|
46
|
+
/**
|
|
47
|
+
* 只在显式的使用tab触发时调用kernel completion
|
|
48
|
+
* 只在只在隐式的输入时触发时调用lsp completion
|
|
49
|
+
*/
|
|
50
|
+
if (!lspProvider || context.explicit === true) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { virtualDocument: doc, lspConnection, editor } = await lspProvider();
|
|
55
|
+
|
|
56
|
+
const { state } = context;
|
|
57
|
+
let { pos } = context;
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!lspConnection ||
|
|
61
|
+
!lspConnection.isReady ||
|
|
62
|
+
!lspConnection.provides('completionProvider')
|
|
63
|
+
) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { line, character } = offsetToPos(state.doc, pos);
|
|
68
|
+
|
|
69
|
+
const rootPos = doc.transformFromEditorToRoot(editor, {
|
|
70
|
+
line,
|
|
71
|
+
ch: character,
|
|
72
|
+
isEditor: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!rootPos) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const virtualPos = doc.virtualPositionAtDocument(rootPos);
|
|
80
|
+
|
|
81
|
+
const result = await lspConnection.clientRequests[
|
|
82
|
+
'textDocument/completion'
|
|
83
|
+
].request({
|
|
84
|
+
position: { line: virtualPos.line, character: virtualPos.ch },
|
|
85
|
+
textDocument: {
|
|
86
|
+
uri: doc.documentInfo.uri,
|
|
87
|
+
},
|
|
88
|
+
context: {
|
|
89
|
+
triggerKind: CompletionTriggerKind.Invoked,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!result) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const items = 'items' in result ? result.items : result;
|
|
98
|
+
|
|
99
|
+
let options = items.map((item) => {
|
|
100
|
+
const { detail, label, kind, textEdit, documentation, sortText, filterText } =
|
|
101
|
+
item;
|
|
102
|
+
const completion: Completion & {
|
|
103
|
+
filterText: string;
|
|
104
|
+
sortText?: string;
|
|
105
|
+
apply: string;
|
|
106
|
+
} = {
|
|
107
|
+
label,
|
|
108
|
+
detail,
|
|
109
|
+
apply: textEdit?.newText ?? label,
|
|
110
|
+
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
|
111
|
+
sortText: sortText ?? label,
|
|
112
|
+
filterText: filterText ?? label,
|
|
113
|
+
};
|
|
114
|
+
if (documentation) {
|
|
115
|
+
const resolver: CompletionItemDetailReolve = async () => {
|
|
116
|
+
return renderMarkupContent(documentation);
|
|
117
|
+
};
|
|
118
|
+
completion.info = resolver;
|
|
119
|
+
} else {
|
|
120
|
+
const resolver: CompletionItemDetailReolve = async () => {
|
|
121
|
+
const itemResult =
|
|
122
|
+
await lspConnection.clientRequests['completionItem/resolve'].request(item);
|
|
123
|
+
return itemResult.documentation
|
|
124
|
+
? renderMarkupContent(itemResult.documentation)
|
|
125
|
+
: null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
completion.info = resolver;
|
|
129
|
+
}
|
|
130
|
+
return completion;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const [, match] = prefixMatch(options);
|
|
134
|
+
const token = context.matchBefore(match);
|
|
135
|
+
|
|
136
|
+
// TODO: sort 方法需要进一步改进
|
|
137
|
+
if (token) {
|
|
138
|
+
pos = token.from;
|
|
139
|
+
const word = token.text.toLowerCase();
|
|
140
|
+
if (/^\w+$/.test(word)) {
|
|
141
|
+
options = options
|
|
142
|
+
.filter(({ filterText }) => filterText.toLowerCase().startsWith(word))
|
|
143
|
+
.sort(
|
|
144
|
+
({ apply: a, sortText: sortTexta }, { apply: b, sortText: sortTextb }) => {
|
|
145
|
+
switch (true) {
|
|
146
|
+
case sortTexta !== undefined && sortTextb !== undefined:
|
|
147
|
+
return sortTexta!.localeCompare(sortTextb!);
|
|
148
|
+
case a.startsWith(token.text) && !b.startsWith(token.text):
|
|
149
|
+
return -1;
|
|
150
|
+
case !a.startsWith(token.text) && b.startsWith(token.text):
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
options = options.sort(({ sortText: sortTexta }, { sortText: sortTextb }) => {
|
|
159
|
+
switch (true) {
|
|
160
|
+
case sortTexta !== undefined && sortTextb !== undefined:
|
|
161
|
+
return sortTexta!.localeCompare(sortTextb!);
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
from: pos,
|
|
169
|
+
options,
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
return pythonLanguage.data.of({
|
|
173
|
+
autocomplete: completionSource,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-parameter-properties */
|
|
2
|
+
/* eslint-disable @typescript-eslint/parameter-properties */
|
|
3
|
+
import type { TransactionSpec } from '@codemirror/state';
|
|
4
|
+
import { StateEffect } from '@codemirror/state';
|
|
5
|
+
import type {
|
|
6
|
+
Command,
|
|
7
|
+
EditorView,
|
|
8
|
+
KeyBinding,
|
|
9
|
+
PluginValue,
|
|
10
|
+
ViewUpdate,
|
|
11
|
+
} from '@codemirror/view';
|
|
12
|
+
import { ViewPlugin } from '@codemirror/view';
|
|
13
|
+
|
|
14
|
+
import { insertCompletionText } from '../auto-complete/index.js';
|
|
15
|
+
|
|
16
|
+
import type { CMLSPExtension, LSPExtensionOptions } from './protocol.js';
|
|
17
|
+
import { offsetToPos, posToOffset } from './util.js';
|
|
18
|
+
|
|
19
|
+
export const startFormatEffect = StateEffect.define<boolean>();
|
|
20
|
+
|
|
21
|
+
export const formatCell: Command = (view: EditorView) => {
|
|
22
|
+
view.dispatch({ effects: startFormatEffect.of(true) });
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const formatKeymap: readonly KeyBinding[] = [{ key: 'Alt-f', run: formatCell }];
|
|
27
|
+
|
|
28
|
+
class FormatPlugin implements PluginValue {
|
|
29
|
+
constructor(
|
|
30
|
+
readonly view: EditorView,
|
|
31
|
+
readonly options: LSPExtensionOptions,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
update(update: ViewUpdate) {
|
|
35
|
+
for (const tr of update.transactions) {
|
|
36
|
+
for (const effect of tr.effects) {
|
|
37
|
+
if (effect.is(startFormatEffect)) {
|
|
38
|
+
this.doFormat();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async doFormat() {
|
|
45
|
+
const lspProvider = await this.options.lspProvider?.();
|
|
46
|
+
if (!lspProvider) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// const { state } = this.view;
|
|
50
|
+
// const currentLine = state.doc.lineAt(state.selection.main.head).number;
|
|
51
|
+
|
|
52
|
+
const { editor, virtualDocument, lspConnection } = lspProvider;
|
|
53
|
+
const virtualStartPos = virtualDocument.transformEditorToVirtual(editor, {
|
|
54
|
+
line: 0,
|
|
55
|
+
ch: 0,
|
|
56
|
+
isEditor: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const end = offsetToPos(this.view.state.doc, this.view.state.doc.length);
|
|
60
|
+
|
|
61
|
+
const virtualEndPos = virtualDocument.transformEditorToVirtual(editor, {
|
|
62
|
+
line: end.line,
|
|
63
|
+
ch: end.character,
|
|
64
|
+
isEditor: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!virtualStartPos || !virtualEndPos) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lspConnection.clientRequests['textDocument/rangeFormatting']
|
|
72
|
+
.request({
|
|
73
|
+
textDocument: { uri: virtualDocument.uri },
|
|
74
|
+
range: {
|
|
75
|
+
start: { line: virtualStartPos.line, character: virtualStartPos.ch },
|
|
76
|
+
end: { line: virtualEndPos.line, character: virtualEndPos.ch },
|
|
77
|
+
},
|
|
78
|
+
options: {
|
|
79
|
+
tabSize: this.view.state.tabSize,
|
|
80
|
+
insertSpaces: true,
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
.then((result) => {
|
|
84
|
+
if (result && result?.length) {
|
|
85
|
+
const items = result;
|
|
86
|
+
const transaction: TransactionSpec[] = [];
|
|
87
|
+
items.forEach((item) => {
|
|
88
|
+
const defaultNewLine = {
|
|
89
|
+
line: end.line + 1,
|
|
90
|
+
ch: 0,
|
|
91
|
+
};
|
|
92
|
+
const editorStart =
|
|
93
|
+
virtualDocument.transformVirtualToEditor({
|
|
94
|
+
line: item.range.start.line,
|
|
95
|
+
ch: item.range.start.character,
|
|
96
|
+
isVirtual: true,
|
|
97
|
+
}) ?? defaultNewLine;
|
|
98
|
+
const editorEnd =
|
|
99
|
+
virtualDocument.transformVirtualToEditor({
|
|
100
|
+
line: item.range.end.line,
|
|
101
|
+
ch: item.range.end.character,
|
|
102
|
+
isVirtual: true,
|
|
103
|
+
}) ?? defaultNewLine;
|
|
104
|
+
|
|
105
|
+
if (!editorStart || !editorEnd) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const from = posToOffset(this.view.state.doc, {
|
|
109
|
+
line: editorStart.line,
|
|
110
|
+
character: editorStart.ch,
|
|
111
|
+
});
|
|
112
|
+
const to = posToOffset(this.view.state.doc, {
|
|
113
|
+
line: editorEnd.line,
|
|
114
|
+
character: editorEnd.ch,
|
|
115
|
+
});
|
|
116
|
+
// FIXME: 需要处理新增行的情况,目前在virtualdocument无法处理
|
|
117
|
+
// console.log('format', item.range, editorStart, editorEnd, from, to);
|
|
118
|
+
if (from !== undefined && to !== undefined) {
|
|
119
|
+
const trans = insertCompletionText(
|
|
120
|
+
this.view.state,
|
|
121
|
+
item.newText,
|
|
122
|
+
from,
|
|
123
|
+
to,
|
|
124
|
+
);
|
|
125
|
+
transaction.push(trans);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// console.log(transaction, 'format trans');
|
|
129
|
+
|
|
130
|
+
this.view.dispatch(...transaction);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
})
|
|
134
|
+
.catch(console.error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
destroy() {
|
|
138
|
+
//
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const lspFormat: CMLSPExtension = (options) => {
|
|
143
|
+
return [ViewPlugin.define((view) => new FormatPlugin(view, options))];
|
|
144
|
+
};
|
package/src/lsp/index.ts
ADDED
package/src/lsp/lint.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Diagnostic } from '@codemirror/lint';
|
|
2
|
+
import { setDiagnostics } from '@codemirror/lint';
|
|
3
|
+
import type { PluginValue, EditorView } from '@codemirror/view';
|
|
4
|
+
import { ViewPlugin } from '@codemirror/view';
|
|
5
|
+
import { DiagnosticSeverity } from '@difizen/libro-lsp';
|
|
6
|
+
|
|
7
|
+
import type { CMLSPExtension, LSPExtensionOptions } from './protocol.js';
|
|
8
|
+
import { posToOffset } from './util.js';
|
|
9
|
+
|
|
10
|
+
class LintPlugin implements PluginValue {
|
|
11
|
+
constructor(
|
|
12
|
+
readonly view: EditorView,
|
|
13
|
+
readonly options: LSPExtensionOptions,
|
|
14
|
+
) {
|
|
15
|
+
this.processDiagnostic();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
processDiagnostic() {
|
|
19
|
+
if (!this.options.lspProvider) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.options
|
|
23
|
+
.lspProvider()
|
|
24
|
+
.then(({ lspConnection, virtualDocument, editor }) => {
|
|
25
|
+
if (!lspConnection) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
lspConnection.serverNotifications['textDocument/publishDiagnostics'].event(
|
|
29
|
+
(e) => {
|
|
30
|
+
const diagnostics = e.diagnostics
|
|
31
|
+
.map(({ range, message, severity = DiagnosticSeverity.Information }) => {
|
|
32
|
+
const currentEditor = virtualDocument.getEditorAtVirtualLine({
|
|
33
|
+
line: range.start.line,
|
|
34
|
+
ch: range.start.character,
|
|
35
|
+
isVirtual: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// the diagnostic range must be in current editor
|
|
39
|
+
if (editor !== currentEditor) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const editorStart = virtualDocument.transformVirtualToEditor({
|
|
44
|
+
line: range.start.line,
|
|
45
|
+
ch: range.start.character,
|
|
46
|
+
isVirtual: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let offset: number | undefined;
|
|
50
|
+
if (editorStart) {
|
|
51
|
+
offset = posToOffset(this.view.state.doc, {
|
|
52
|
+
line: editorStart.line,
|
|
53
|
+
character: editorStart.ch,
|
|
54
|
+
})!;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const editorEnd = virtualDocument.transformVirtualToEditor({
|
|
58
|
+
line: range.end.line,
|
|
59
|
+
ch: range.end.character,
|
|
60
|
+
isVirtual: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let end: number | undefined;
|
|
64
|
+
if (editorEnd) {
|
|
65
|
+
end = posToOffset(this.view.state.doc, {
|
|
66
|
+
line: editorEnd.line,
|
|
67
|
+
character: editorEnd.ch,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
from: offset,
|
|
72
|
+
to: end,
|
|
73
|
+
severity: (
|
|
74
|
+
{
|
|
75
|
+
[DiagnosticSeverity.Error]: 'error',
|
|
76
|
+
[DiagnosticSeverity.Warning]: 'warning',
|
|
77
|
+
[DiagnosticSeverity.Information]: 'info',
|
|
78
|
+
[DiagnosticSeverity.Hint]: 'info',
|
|
79
|
+
} as const
|
|
80
|
+
)[severity],
|
|
81
|
+
message,
|
|
82
|
+
} as Diagnostic;
|
|
83
|
+
})
|
|
84
|
+
.filter<Diagnostic>(isDiagnostic)
|
|
85
|
+
.sort((a, b) => {
|
|
86
|
+
switch (true) {
|
|
87
|
+
case a.from < b.from:
|
|
88
|
+
return -1;
|
|
89
|
+
case a.from > b.from:
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.view.dispatch(setDiagnostics(this.view.state, diagnostics));
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
})
|
|
100
|
+
.catch(console.error);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
update() {
|
|
104
|
+
//
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destroy() {
|
|
108
|
+
//
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const lspLint: CMLSPExtension = (options) => {
|
|
113
|
+
return [ViewPlugin.define((view) => new LintPlugin(view, options))];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function isDiagnostic(item: any): item is Diagnostic {
|
|
117
|
+
return (
|
|
118
|
+
item !== undefined &&
|
|
119
|
+
item !== null &&
|
|
120
|
+
item.from !== null &&
|
|
121
|
+
item.to !== null &&
|
|
122
|
+
item.from !== undefined &&
|
|
123
|
+
item.to !== undefined
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { hoverTooltip } from '@codemirror/view';
|
|
2
|
+
|
|
3
|
+
import type { CMLSPExtension } from './protocol.js';
|
|
4
|
+
import { offsetToPos, posToOffset, renderMarkupContent } from './util.js';
|
|
5
|
+
|
|
6
|
+
export const lspTooltip: CMLSPExtension = (options) => {
|
|
7
|
+
return hoverTooltip(async (view, pos) => {
|
|
8
|
+
if (!options.lspProvider) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
lspConnection: connection,
|
|
14
|
+
virtualDocument: doc,
|
|
15
|
+
editor,
|
|
16
|
+
} = await options.lspProvider();
|
|
17
|
+
|
|
18
|
+
if (!connection || !connection.isReady || !connection.provides('hoverProvider')) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { line, character } = offsetToPos(view.state.doc, pos);
|
|
23
|
+
|
|
24
|
+
const virtualPos = doc.transformEditorToVirtual(editor, {
|
|
25
|
+
line,
|
|
26
|
+
ch: character,
|
|
27
|
+
isEditor: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!virtualPos) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await connection.clientRequests['textDocument/hover'].request({
|
|
35
|
+
position: { line: virtualPos.line, character: virtualPos.ch },
|
|
36
|
+
textDocument: {
|
|
37
|
+
uri: doc.documentInfo.uri,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
if (!result) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const { contents, range } = result;
|
|
44
|
+
let offset = posToOffset(view.state.doc, { line, character })!;
|
|
45
|
+
let end;
|
|
46
|
+
if (range) {
|
|
47
|
+
const editorStart = doc.transformVirtualToEditor({
|
|
48
|
+
line: range.start.line,
|
|
49
|
+
ch: range.start.character,
|
|
50
|
+
isVirtual: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (editorStart) {
|
|
54
|
+
offset = posToOffset(view.state.doc, {
|
|
55
|
+
line: editorStart.line,
|
|
56
|
+
character: editorStart.ch,
|
|
57
|
+
})!;
|
|
58
|
+
}
|
|
59
|
+
const editorEnd = doc.transformVirtualToEditor({
|
|
60
|
+
line: range.end.line,
|
|
61
|
+
ch: range.end.character,
|
|
62
|
+
isVirtual: true,
|
|
63
|
+
});
|
|
64
|
+
if (editorEnd) {
|
|
65
|
+
end = posToOffset(view.state.doc, {
|
|
66
|
+
line: editorEnd.line,
|
|
67
|
+
character: editorEnd.ch,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dom = renderMarkupContent(contents);
|
|
73
|
+
|
|
74
|
+
return { pos: offset, end, create: () => ({ dom }), above: false };
|
|
75
|
+
});
|
|
76
|
+
};
|
package/src/lsp/util.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Text } from '@codemirror/state';
|
|
2
|
+
import hljs from 'highlight.js';
|
|
3
|
+
import MarkdownIt from 'markdown-it';
|
|
4
|
+
import type * as lsp from 'vscode-languageserver-protocol';
|
|
5
|
+
import 'highlight.js/styles/github.css';
|
|
6
|
+
|
|
7
|
+
export function posToOffset(doc: Text, pos: { line: number; character: number }) {
|
|
8
|
+
if (pos.line >= doc.lines) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const offset = doc.line(pos.line + 1).from + pos.character;
|
|
12
|
+
if (offset > doc.length) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return offset;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function offsetToPos(doc: Text, offset: number) {
|
|
19
|
+
const line = doc.lineAt(offset);
|
|
20
|
+
return {
|
|
21
|
+
line: line.number - 1,
|
|
22
|
+
character: offset - line.from,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatContents(
|
|
27
|
+
contents: lsp.MarkupContent | lsp.MarkedString | lsp.MarkedString[],
|
|
28
|
+
): string {
|
|
29
|
+
if (Array.isArray(contents)) {
|
|
30
|
+
return contents.map((c) => formatContents(c) + '\n\n').join('');
|
|
31
|
+
} else if (typeof contents === 'string') {
|
|
32
|
+
return contents;
|
|
33
|
+
} else {
|
|
34
|
+
return contents.value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const renderMarkdownContent = (val: string) => {
|
|
39
|
+
const render = new MarkdownIt({
|
|
40
|
+
html: true,
|
|
41
|
+
linkify: true,
|
|
42
|
+
breaks: true,
|
|
43
|
+
highlight: function (str, lang) {
|
|
44
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
45
|
+
try {
|
|
46
|
+
const hl = hljs.highlight(lang, str).value;
|
|
47
|
+
return hl;
|
|
48
|
+
} catch (__) {
|
|
49
|
+
//
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return ''; // use external default escaping
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return render.render(val);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const renderMarkupContent = (
|
|
61
|
+
contents: lsp.MarkupContent | lsp.MarkedString | lsp.MarkedString[],
|
|
62
|
+
) => {
|
|
63
|
+
const dom = document.createElement('div');
|
|
64
|
+
dom.classList.add('documentation');
|
|
65
|
+
|
|
66
|
+
const res = renderMarkdownContent(formatContents(contents));
|
|
67
|
+
dom.innerHTML = res;
|
|
68
|
+
return dom;
|
|
69
|
+
};
|