@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.
Files changed (84) hide show
  1. package/es/auto-complete/filter.d.ts.map +1 -1
  2. package/es/auto-complete/filter.js +1 -7
  3. package/es/auto-complete/index.d.ts.map +1 -1
  4. package/es/auto-complete/index.js +5 -9
  5. package/es/auto-complete/snippet.d.ts.map +1 -1
  6. package/es/auto-complete/snippet.js +2 -2
  7. package/es/auto-complete/state.d.ts.map +1 -1
  8. package/es/auto-complete/state.js +5 -16
  9. package/es/auto-complete/view.d.ts +5 -0
  10. package/es/auto-complete/view.d.ts.map +1 -1
  11. package/es/auto-complete/view.js +2 -13
  12. package/es/completion.js +1 -1
  13. package/es/config.d.ts +15 -10
  14. package/es/config.d.ts.map +1 -1
  15. package/es/config.js +15 -6
  16. package/es/editor-contribution.d.ts +8 -0
  17. package/es/editor-contribution.d.ts.map +1 -0
  18. package/es/editor-contribution.js +30 -0
  19. package/es/editor.d.ts +40 -10
  20. package/es/editor.d.ts.map +1 -1
  21. package/es/editor.js +241 -45
  22. package/es/indentation-markers/map.d.ts +9 -9
  23. package/es/indentation-markers/map.d.ts.map +1 -1
  24. package/es/index.d.ts +3 -1
  25. package/es/index.d.ts.map +1 -1
  26. package/es/index.js +3 -1
  27. package/es/libro-icon.d.ts +2 -2
  28. package/es/libro-icon.d.ts.map +1 -1
  29. package/es/libro-icon.js +2 -2
  30. package/es/lsp/completion.d.ts +5 -0
  31. package/es/lsp/completion.d.ts.map +1 -0
  32. package/es/lsp/completion.js +245 -0
  33. package/es/lsp/format.d.ts +7 -0
  34. package/es/lsp/format.d.ts.map +1 -0
  35. package/es/lsp/format.js +193 -0
  36. package/es/lsp/index.d.ts +7 -0
  37. package/es/lsp/index.d.ts.map +1 -0
  38. package/es/lsp/index.js +6 -0
  39. package/es/lsp/lint.d.ts +3 -0
  40. package/es/lsp/lint.d.ts.map +1 -0
  41. package/es/lsp/lint.js +114 -0
  42. package/es/lsp/protocol.d.ts +7 -0
  43. package/es/lsp/protocol.d.ts.map +1 -0
  44. package/es/lsp/protocol.js +1 -0
  45. package/es/lsp/tooltip.d.ts +3 -0
  46. package/es/lsp/tooltip.d.ts.map +1 -0
  47. package/es/lsp/tooltip.js +113 -0
  48. package/es/lsp/util.d.ts +15 -0
  49. package/es/lsp/util.d.ts.map +1 -0
  50. package/es/lsp/util.js +58 -0
  51. package/es/mode.d.ts.map +1 -1
  52. package/es/module.d.ts +3 -0
  53. package/es/module.d.ts.map +1 -0
  54. package/es/module.js +4 -0
  55. package/es/theme.js +4 -5
  56. package/es/tooltip.d.ts.map +1 -1
  57. package/es/tooltip.js +2 -4
  58. package/package.json +7 -5
  59. package/src/auto-complete/filter.ts +5 -7
  60. package/src/auto-complete/index.ts +6 -7
  61. package/src/auto-complete/snippet.ts +8 -2
  62. package/src/auto-complete/state.ts +13 -18
  63. package/src/auto-complete/view.ts +7 -13
  64. package/src/completion.ts +2 -2
  65. package/src/config.ts +40 -28
  66. package/src/editor-contribution.ts +17 -0
  67. package/src/editor.ts +226 -50
  68. package/src/hyperlink.ts +1 -1
  69. package/src/indentation-markers/index.ts +3 -3
  70. package/src/indentation-markers/map.ts +9 -9
  71. package/src/index.ts +4 -1
  72. package/src/libro-icon.tsx +4 -0
  73. package/src/lsp/completion.ts +175 -0
  74. package/src/lsp/format.ts +144 -0
  75. package/src/lsp/index.ts +6 -0
  76. package/src/lsp/lint.ts +125 -0
  77. package/src/lsp/protocol.ts +8 -0
  78. package/src/lsp/tooltip.ts +76 -0
  79. package/src/lsp/util.ts +69 -0
  80. package/src/mode.ts +1 -1
  81. package/src/module.ts +8 -0
  82. package/src/theme.ts +4 -4
  83. package/src/tooltip.ts +2 -4
  84. 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
- private state: EditorState;
25
+ protected state: EditorState;
26
26
 
27
27
  /** The set of lines that are used as an entrypoint. */
28
- private lines: Set<Line>;
28
+ protected lines: Set<Line>;
29
29
 
30
30
  /** The internal mapping of line numbers to {@link IndentEntry} objects. */
31
- private map: Map<number, IndentEntry>;
31
+ protected map: Map<number, IndentEntry>;
32
32
 
33
33
  /** The width of the editor's indent unit. */
34
- private unitWidth: number;
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
- private markerType: 'fullScope' | 'codeOnly';
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
- private set(line: Line, col: number, level: number) {
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
- private add(line: Line) {
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
- private closestNonEmpty(from: Line, dir: -1 | 1) {
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
- private findAndSetActiveLines() {
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 './theme.js';
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
+ };
@@ -0,0 +1,6 @@
1
+ export * from './lint.js';
2
+ export * from './protocol.js';
3
+ export * from './tooltip.js';
4
+ export * from './util.js';
5
+ export * from './completion.js';
6
+ export * from './format.js';
@@ -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,8 @@
1
+ import type { Extension } from '@codemirror/state';
2
+ import type { LSPProvider } from '@difizen/libro-lsp';
3
+
4
+ export interface LSPExtensionOptions {
5
+ lspProvider?: LSPProvider;
6
+ }
7
+
8
+ export type CMLSPExtension = (option: LSPExtensionOptions) => Extension;
@@ -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
+ };
@@ -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
+ };