@difizen/libro-codemirror 0.0.2-alpha.0
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/LICENSE +21 -0
- package/README.md +0 -0
- package/es/auto-complete/closebrackets.d.ts +12 -0
- package/es/auto-complete/closebrackets.d.ts.map +1 -0
- package/es/auto-complete/closebrackets.js +408 -0
- package/es/auto-complete/completion.d.ts +57 -0
- package/es/auto-complete/completion.d.ts.map +1 -0
- package/es/auto-complete/completion.js +265 -0
- package/es/auto-complete/config.d.ts +22 -0
- package/es/auto-complete/config.d.ts.map +1 -0
- package/es/auto-complete/config.js +44 -0
- package/es/auto-complete/filter.d.ts +13 -0
- package/es/auto-complete/filter.d.ts.map +1 -0
- package/es/auto-complete/filter.js +191 -0
- package/es/auto-complete/index.d.ts +17 -0
- package/es/auto-complete/index.d.ts.map +1 -0
- package/es/auto-complete/index.js +107 -0
- package/es/auto-complete/snippet.d.ts +14 -0
- package/es/auto-complete/snippet.d.ts.map +1 -0
- package/es/auto-complete/snippet.js +447 -0
- package/es/auto-complete/state.d.ts +63 -0
- package/es/auto-complete/state.d.ts.map +1 -0
- package/es/auto-complete/state.js +452 -0
- package/es/auto-complete/theme.d.ts +6 -0
- package/es/auto-complete/theme.d.ts.map +1 -0
- package/es/auto-complete/theme.js +151 -0
- package/es/auto-complete/tooltip.d.ts +5 -0
- package/es/auto-complete/tooltip.d.ts.map +1 -0
- package/es/auto-complete/tooltip.js +365 -0
- package/es/auto-complete/view.d.ts +43 -0
- package/es/auto-complete/view.d.ts.map +1 -0
- package/es/auto-complete/view.js +372 -0
- package/es/auto-complete/word.d.ts +3 -0
- package/es/auto-complete/word.d.ts.map +1 -0
- package/es/auto-complete/word.js +119 -0
- package/es/completion.d.ts +6 -0
- package/es/completion.d.ts.map +1 -0
- package/es/completion.js +84 -0
- package/es/config.d.ts +184 -0
- package/es/config.d.ts.map +1 -0
- package/es/config.js +473 -0
- package/es/editor.d.ts +361 -0
- package/es/editor.d.ts.map +1 -0
- package/es/editor.js +1126 -0
- package/es/factory.d.ts +3 -0
- package/es/factory.d.ts.map +1 -0
- package/es/factory.js +12 -0
- package/es/hyperlink.d.ts +15 -0
- package/es/hyperlink.d.ts.map +1 -0
- package/es/hyperlink.js +120 -0
- package/es/indent.d.ts +8 -0
- package/es/indent.d.ts.map +1 -0
- package/es/indent.js +58 -0
- package/es/indentation-markers/config.d.ts +17 -0
- package/es/indentation-markers/config.d.ts.map +1 -0
- package/es/indentation-markers/config.js +10 -0
- package/es/indentation-markers/index.d.ts +3 -0
- package/es/indentation-markers/index.d.ts.map +1 -0
- package/es/indentation-markers/index.js +160 -0
- package/es/indentation-markers/map.d.ts +77 -0
- package/es/indentation-markers/map.d.ts.map +1 -0
- package/es/indentation-markers/map.js +265 -0
- package/es/indentation-markers/utils.d.ts +27 -0
- package/es/indentation-markers/utils.d.ts.map +1 -0
- package/es/indentation-markers/utils.js +91 -0
- package/es/index.d.ts +11 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +10 -0
- package/es/libro-icon.d.ts +3 -0
- package/es/libro-icon.d.ts.map +1 -0
- package/es/libro-icon.js +2 -0
- package/es/mimetype.d.ts +22 -0
- package/es/mimetype.d.ts.map +1 -0
- package/es/mimetype.js +59 -0
- package/es/mode.d.ts +86 -0
- package/es/mode.d.ts.map +1 -0
- package/es/mode.js +284 -0
- package/es/monitor.d.ts +32 -0
- package/es/monitor.d.ts.map +1 -0
- package/es/monitor.js +129 -0
- package/es/python-lang.d.ts +3 -0
- package/es/python-lang.d.ts.map +1 -0
- package/es/python-lang.js +7 -0
- package/es/style/base.css +131 -0
- package/es/style/theme.css +12 -0
- package/es/style/variables.css +403 -0
- package/es/theme.d.ts +35 -0
- package/es/theme.d.ts.map +1 -0
- package/es/theme.js +225 -0
- package/es/tooltip.d.ts +10 -0
- package/es/tooltip.d.ts.map +1 -0
- package/es/tooltip.js +170 -0
- package/package.json +74 -0
- package/src/auto-complete/README.md +71 -0
- package/src/auto-complete/closebrackets.ts +423 -0
- package/src/auto-complete/completion.ts +345 -0
- package/src/auto-complete/config.ts +101 -0
- package/src/auto-complete/filter.ts +215 -0
- package/src/auto-complete/index.ts +112 -0
- package/src/auto-complete/snippet.ts +394 -0
- package/src/auto-complete/state.ts +472 -0
- package/src/auto-complete/theme.ts +126 -0
- package/src/auto-complete/tooltip.ts +386 -0
- package/src/auto-complete/view.ts +343 -0
- package/src/auto-complete/word.ts +118 -0
- package/src/completion.ts +61 -0
- package/src/config.ts +689 -0
- package/src/editor.ts +1078 -0
- package/src/factory.ts +10 -0
- package/src/hyperlink.ts +95 -0
- package/src/indent.ts +69 -0
- package/src/indentation-markers/config.ts +31 -0
- package/src/indentation-markers/index.ts +192 -0
- package/src/indentation-markers/map.ts +273 -0
- package/src/indentation-markers/utils.ts +84 -0
- package/src/index.ts +11 -0
- package/src/libro-icon.ts +4 -0
- package/src/mimetype.ts +49 -0
- package/src/mode.ts +269 -0
- package/src/monitor.ts +105 -0
- package/src/python-lang.ts +7 -0
- package/src/style/base.css +129 -0
- package/src/style/theme.css +12 -0
- package/src/style/variables.css +405 -0
- package/src/theme.ts +231 -0
- package/src/tooltip.ts +145 -0
package/src/factory.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CodeEditorFactory, IEditorOptions } from '@difizen/libro-code-editor';
|
|
2
|
+
|
|
3
|
+
import { codeMirrorDefaultConfig, CodeMirrorEditor } from './editor.js';
|
|
4
|
+
|
|
5
|
+
export const codeMirrorEditorFactory: CodeEditorFactory = (options: IEditorOptions) => {
|
|
6
|
+
return new CodeMirrorEditor({
|
|
7
|
+
...options,
|
|
8
|
+
config: { ...codeMirrorDefaultConfig, ...options.config },
|
|
9
|
+
});
|
|
10
|
+
};
|
package/src/hyperlink.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { syntaxTree } from '@codemirror/language';
|
|
2
|
+
import type { Extension, Range } from '@codemirror/state';
|
|
3
|
+
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
|
4
|
+
import { ViewPlugin, EditorView, Decoration, WidgetType } from '@codemirror/view';
|
|
5
|
+
|
|
6
|
+
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
|
|
7
|
+
|
|
8
|
+
export interface HyperLinkState {
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class HyperLink extends WidgetType {
|
|
15
|
+
private readonly state: HyperLinkState;
|
|
16
|
+
constructor(state: HyperLinkState) {
|
|
17
|
+
super();
|
|
18
|
+
this.state = state;
|
|
19
|
+
}
|
|
20
|
+
override eq(other: HyperLink) {
|
|
21
|
+
return (
|
|
22
|
+
this.state.url === other.state.url &&
|
|
23
|
+
this.state.to === other.state.to &&
|
|
24
|
+
this.state.from === other.state.from
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
toDOM() {
|
|
28
|
+
const wrapper = document.createElement('a');
|
|
29
|
+
wrapper.href = this.state.url;
|
|
30
|
+
wrapper.target = '__blank';
|
|
31
|
+
wrapper.innerHTML = pathStr;
|
|
32
|
+
wrapper.className = 'cm-hyper-link-icon';
|
|
33
|
+
return wrapper;
|
|
34
|
+
}
|
|
35
|
+
override ignoreEvent() {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hyperLinkDecorations(view: EditorView) {
|
|
41
|
+
const widgets: Range<Decoration>[] = [];
|
|
42
|
+
for (const range of view.visibleRanges) {
|
|
43
|
+
syntaxTree(view.state).iterate({
|
|
44
|
+
from: range.from,
|
|
45
|
+
to: range.to,
|
|
46
|
+
enter: ({ type, from, to }) => {
|
|
47
|
+
const callExp: string = view.state.doc.sliceString(from, to);
|
|
48
|
+
if (type.name === 'URL') {
|
|
49
|
+
const widget = Decoration.widget({
|
|
50
|
+
widget: new HyperLink({
|
|
51
|
+
from,
|
|
52
|
+
to,
|
|
53
|
+
url: callExp,
|
|
54
|
+
}),
|
|
55
|
+
side: 1,
|
|
56
|
+
});
|
|
57
|
+
widgets.push(widget.range(to));
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return Decoration.set(widgets);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hyperLinkExtension() {
|
|
66
|
+
return ViewPlugin.fromClass(
|
|
67
|
+
class HyperLinkView {
|
|
68
|
+
decorations: DecorationSet;
|
|
69
|
+
constructor(view: EditorView) {
|
|
70
|
+
this.decorations = hyperLinkDecorations(view);
|
|
71
|
+
}
|
|
72
|
+
update(update: ViewUpdate) {
|
|
73
|
+
if (update.docChanged || update.viewportChanged) {
|
|
74
|
+
this.decorations = hyperLinkDecorations(update.view);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
decorations: (v) => v.decorations,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const hyperLinkStyle = EditorView.baseTheme({
|
|
85
|
+
'.cm-hyper-link-icon': {
|
|
86
|
+
display: 'inline-block',
|
|
87
|
+
verticalAlign: 'middle',
|
|
88
|
+
marginLeft: '0.2ch',
|
|
89
|
+
},
|
|
90
|
+
'.cm-hyper-link-icon svg': {
|
|
91
|
+
display: 'block',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const hyperLink: Extension = [hyperLinkExtension(), hyperLinkStyle];
|
package/src/indent.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { indentLess, indentMore, insertTab } from '@codemirror/commands';
|
|
2
|
+
import type { Command, EditorView } from '@codemirror/view';
|
|
3
|
+
|
|
4
|
+
import { completionState, startCompletionEffect } from './auto-complete/state.js';
|
|
5
|
+
import { startTooltip } from './tooltip.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Indent or insert a tab as appropriate.
|
|
9
|
+
*/
|
|
10
|
+
export const indentMoreOrInsertTab: Command = (view: EditorView): boolean => {
|
|
11
|
+
const from = view.state.selection.main.from;
|
|
12
|
+
const to = view.state.selection.main.to;
|
|
13
|
+
if (from !== to) {
|
|
14
|
+
return indentMore(view);
|
|
15
|
+
}
|
|
16
|
+
const line = view.state.doc.lineAt(from);
|
|
17
|
+
const before = view.state.doc.slice(line.from, from).toString();
|
|
18
|
+
if (/^\s*$/.test(before)) {
|
|
19
|
+
return indentMore(view);
|
|
20
|
+
} else {
|
|
21
|
+
return insertTab(view);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* RegExp to test for leading whitespace
|
|
27
|
+
*/
|
|
28
|
+
const leadingWhitespaceRe = /^\s+$/;
|
|
29
|
+
export const indentOrCompletion: Command = (view: EditorView) => {
|
|
30
|
+
const from = view.state.selection.main.from;
|
|
31
|
+
|
|
32
|
+
const line = view.state.doc.lineAt(from);
|
|
33
|
+
|
|
34
|
+
let shouldIndent = false;
|
|
35
|
+
if (line.from === from) {
|
|
36
|
+
shouldIndent = true;
|
|
37
|
+
} else {
|
|
38
|
+
shouldIndent =
|
|
39
|
+
view.state.doc
|
|
40
|
+
.slice(from - 1, from)
|
|
41
|
+
.toString()
|
|
42
|
+
.match(leadingWhitespaceRe) !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (shouldIndent) {
|
|
46
|
+
return indentMoreOrInsertTab(view);
|
|
47
|
+
} else {
|
|
48
|
+
const cState = view.state.field(completionState, false);
|
|
49
|
+
if (!cState) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
view.dispatch({ effects: startCompletionEffect.of(true) });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const indentOrTooltip: Command = (view: EditorView) => {
|
|
58
|
+
const from = view.state.selection.main.from;
|
|
59
|
+
const line = view.state.doc.lineAt(from);
|
|
60
|
+
const shouldIndentLess =
|
|
61
|
+
view.state.doc.slice(line.from, from).toString().match(leadingWhitespaceRe) !==
|
|
62
|
+
null;
|
|
63
|
+
|
|
64
|
+
if (shouldIndentLess) {
|
|
65
|
+
return indentLess(view);
|
|
66
|
+
} else {
|
|
67
|
+
return startTooltip(view);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { combineConfig, Facet } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
export interface IndentationMarkerConfiguration {
|
|
4
|
+
/**
|
|
5
|
+
* Determines whether active block marker is styled differently.
|
|
6
|
+
*/
|
|
7
|
+
highlightActiveBlock?: boolean;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether markers in the first column are omitted.
|
|
11
|
+
*/
|
|
12
|
+
hideFirstIndent?: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Determines the type of indentation marker.
|
|
16
|
+
*/
|
|
17
|
+
markerType?: 'fullScope' | 'codeOnly';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const indentationMarkerConfig = Facet.define<
|
|
21
|
+
IndentationMarkerConfiguration,
|
|
22
|
+
Required<IndentationMarkerConfiguration>
|
|
23
|
+
>({
|
|
24
|
+
combine(configs) {
|
|
25
|
+
return combineConfig(configs, {
|
|
26
|
+
highlightActiveBlock: true,
|
|
27
|
+
hideFirstIndent: false,
|
|
28
|
+
markerType: 'codeOnly',
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getIndentUnit } from '@codemirror/language';
|
|
2
|
+
import type { EditorState } from '@codemirror/state';
|
|
3
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
4
|
+
import type { DecorationSet, ViewUpdate, PluginValue } from '@codemirror/view';
|
|
5
|
+
import { Decoration, ViewPlugin, EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import { indentationMarkerConfig } from './config.js';
|
|
8
|
+
import type { IndentationMarkerConfiguration } from './config.js';
|
|
9
|
+
import type { IndentEntry } from './map.js';
|
|
10
|
+
import { IndentationMap } from './map.js';
|
|
11
|
+
import { getCurrentLine, getVisibleLines } from './utils.js';
|
|
12
|
+
|
|
13
|
+
// CSS classes:
|
|
14
|
+
// - .cm-indent-markers
|
|
15
|
+
|
|
16
|
+
// CSS variables:
|
|
17
|
+
// - --indent-marker-bg-part
|
|
18
|
+
// - --indent-marker-active-bg-part
|
|
19
|
+
|
|
20
|
+
/** Color of inactive indent markers. Based on RUI's var(--background-higher) */
|
|
21
|
+
const MARKER_COLOR_LIGHT = '#5f6064';
|
|
22
|
+
const MARKER_COLOR_DARK = '#5f6064';
|
|
23
|
+
|
|
24
|
+
/** Color of active indent markers. Based on RUI's var(--background-highest) */
|
|
25
|
+
const MARKER_COLOR_ACTIVE_LIGHT = '#A4AECB';
|
|
26
|
+
const MARKER_COLOR_ACTIVE_DARK = '#565C6D';
|
|
27
|
+
|
|
28
|
+
/** Thickness of indent markers. Probably should be integer pixel values. */
|
|
29
|
+
const MARKER_THICKNESS = '1px';
|
|
30
|
+
|
|
31
|
+
const indentTheme = EditorView.baseTheme({
|
|
32
|
+
'&light': {
|
|
33
|
+
'--indent-marker-bg-color': MARKER_COLOR_LIGHT,
|
|
34
|
+
'--indent-marker-active-bg-color': MARKER_COLOR_ACTIVE_LIGHT,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
'&dark': {
|
|
38
|
+
'--indent-marker-bg-color': MARKER_COLOR_DARK,
|
|
39
|
+
'--indent-marker-active-bg-color': MARKER_COLOR_ACTIVE_DARK,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
'.cm-line': {
|
|
43
|
+
position: 'relative',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// this pseudo-element is used to draw the indent markers,
|
|
47
|
+
// while still allowing the line to have its own background.
|
|
48
|
+
'.cm-indent-markers::before': {
|
|
49
|
+
content: '""',
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
top: 0,
|
|
52
|
+
left: 0,
|
|
53
|
+
right: 0,
|
|
54
|
+
bottom: 0,
|
|
55
|
+
background: 'var(--indent-markers)',
|
|
56
|
+
pointerEvents: 'none',
|
|
57
|
+
zIndex: '-1',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function createGradient(
|
|
62
|
+
markerCssProperty: string,
|
|
63
|
+
indentWidth: number,
|
|
64
|
+
startOffset: number,
|
|
65
|
+
columns: number,
|
|
66
|
+
) {
|
|
67
|
+
const gradient = `repeating-linear-gradient(to right, var(${markerCssProperty}) 0 ${MARKER_THICKNESS}, transparent ${MARKER_THICKNESS} ${indentWidth}ch)`;
|
|
68
|
+
// Subtract one pixel from the background width to get rid of artifacts of pixel rounding
|
|
69
|
+
return `${gradient} ${startOffset * indentWidth}ch/calc(${
|
|
70
|
+
indentWidth * columns
|
|
71
|
+
}ch - 1px) no-repeat`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// libro 没有使用codemirror的dark theme机制
|
|
75
|
+
// const indentMarkerBgColor = '--indent-marker-bg-color';
|
|
76
|
+
// const indentMarkerActiveBgColor = '--indent-marker-active-bg-color';
|
|
77
|
+
const indentMarkerBgColor = '--mana-libro-editor-indent-marker-bg-color';
|
|
78
|
+
const indentMarkerActiveBgColor = '--mana-libro-editor-indent-marker-active-bg-color';
|
|
79
|
+
|
|
80
|
+
function makeBackgroundCSS(
|
|
81
|
+
entry: IndentEntry,
|
|
82
|
+
indentWidth: number,
|
|
83
|
+
hideFirstIndent: boolean,
|
|
84
|
+
) {
|
|
85
|
+
const { level, active } = entry;
|
|
86
|
+
if (hideFirstIndent && level === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const startAt = hideFirstIndent ? 1 : 0;
|
|
90
|
+
const backgrounds = [];
|
|
91
|
+
|
|
92
|
+
if (active !== undefined) {
|
|
93
|
+
const markersBeforeActive = active - startAt - 1;
|
|
94
|
+
if (markersBeforeActive > 0) {
|
|
95
|
+
backgrounds.push(
|
|
96
|
+
createGradient(indentMarkerBgColor, indentWidth, startAt, markersBeforeActive),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
backgrounds.push(
|
|
100
|
+
createGradient(indentMarkerActiveBgColor, indentWidth, active - 1, 1),
|
|
101
|
+
);
|
|
102
|
+
if (active !== level) {
|
|
103
|
+
backgrounds.push(
|
|
104
|
+
createGradient(indentMarkerBgColor, indentWidth, active, level - active),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
backgrounds.push(
|
|
109
|
+
createGradient(indentMarkerBgColor, indentWidth, startAt, level - startAt),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return backgrounds.join(',');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class IndentMarkersClass implements PluginValue {
|
|
117
|
+
view: EditorView;
|
|
118
|
+
decorations!: DecorationSet;
|
|
119
|
+
|
|
120
|
+
private unitWidth: number;
|
|
121
|
+
private currentLineNumber: number;
|
|
122
|
+
|
|
123
|
+
constructor(view: EditorView) {
|
|
124
|
+
this.view = view;
|
|
125
|
+
this.unitWidth = getIndentUnit(view.state);
|
|
126
|
+
this.currentLineNumber = getCurrentLine(view.state).number;
|
|
127
|
+
this.generate(view.state);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
update(update: ViewUpdate) {
|
|
131
|
+
const unitWidth = getIndentUnit(update.state);
|
|
132
|
+
const unitWidthChanged = unitWidth !== this.unitWidth;
|
|
133
|
+
if (unitWidthChanged) {
|
|
134
|
+
this.unitWidth = unitWidth;
|
|
135
|
+
}
|
|
136
|
+
const lineNumber = getCurrentLine(update.state).number;
|
|
137
|
+
const lineNumberChanged = lineNumber !== this.currentLineNumber;
|
|
138
|
+
this.currentLineNumber = lineNumber;
|
|
139
|
+
const activeBlockUpdateRequired =
|
|
140
|
+
update.state.facet(indentationMarkerConfig).highlightActiveBlock &&
|
|
141
|
+
lineNumberChanged;
|
|
142
|
+
if (
|
|
143
|
+
update.docChanged ||
|
|
144
|
+
update.viewportChanged ||
|
|
145
|
+
unitWidthChanged ||
|
|
146
|
+
activeBlockUpdateRequired
|
|
147
|
+
) {
|
|
148
|
+
this.generate(update.state);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private generate(state: EditorState) {
|
|
153
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
154
|
+
|
|
155
|
+
const lines = getVisibleLines(this.view, state);
|
|
156
|
+
const { hideFirstIndent, markerType } = state.facet(indentationMarkerConfig);
|
|
157
|
+
const map = new IndentationMap(lines, state, this.unitWidth, markerType);
|
|
158
|
+
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const entry = map.get(line.number);
|
|
161
|
+
|
|
162
|
+
if (!entry?.level) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const backgrounds = makeBackgroundCSS(entry, this.unitWidth, hideFirstIndent);
|
|
167
|
+
|
|
168
|
+
builder.add(
|
|
169
|
+
line.from,
|
|
170
|
+
line.from,
|
|
171
|
+
Decoration.line({
|
|
172
|
+
class: 'cm-indent-markers',
|
|
173
|
+
attributes: {
|
|
174
|
+
style: `--indent-markers: ${backgrounds}`,
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.decorations = builder.finish();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function indentationMarkers(config: IndentationMarkerConfiguration = {}) {
|
|
185
|
+
return [
|
|
186
|
+
indentationMarkerConfig.of(config),
|
|
187
|
+
indentTheme,
|
|
188
|
+
ViewPlugin.fromClass(IndentMarkersClass, {
|
|
189
|
+
decorations: (v) => v.decorations,
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { EditorState, Line } from '@codemirror/state';
|
|
2
|
+
|
|
3
|
+
import { indentationMarkerConfig } from './config.js';
|
|
4
|
+
import { getCurrentLine, numColumns } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export interface IndentEntry {
|
|
7
|
+
line: Line;
|
|
8
|
+
col: number;
|
|
9
|
+
level: number;
|
|
10
|
+
empty: boolean;
|
|
11
|
+
active?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Indentation map for a set of lines.
|
|
16
|
+
*
|
|
17
|
+
* This map will contain the indentation for lines that are not a part of the given set,
|
|
18
|
+
* but this is because calculating the indentation for those lines was necessary to
|
|
19
|
+
* calculate the indentation for the lines provided to the constructor.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link IndentEntry}
|
|
22
|
+
*/
|
|
23
|
+
export class IndentationMap {
|
|
24
|
+
/** The {@link EditorState} indentation is derived from. */
|
|
25
|
+
private state: EditorState;
|
|
26
|
+
|
|
27
|
+
/** The set of lines that are used as an entrypoint. */
|
|
28
|
+
private lines: Set<Line>;
|
|
29
|
+
|
|
30
|
+
/** The internal mapping of line numbers to {@link IndentEntry} objects. */
|
|
31
|
+
private map: Map<number, IndentEntry>;
|
|
32
|
+
|
|
33
|
+
/** The width of the editor's indent unit. */
|
|
34
|
+
private unitWidth: number;
|
|
35
|
+
|
|
36
|
+
/** The type of indentation to use (terminate at end of scope vs last non-empty line in scope) */
|
|
37
|
+
private markerType: 'fullScope' | 'codeOnly';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param lines - The set of lines to get the indentation map for.
|
|
41
|
+
* @param state - The {@link EditorState} to derive the indentation map from.
|
|
42
|
+
* @param unitWidth - The width of the editor's indent unit.
|
|
43
|
+
* @param markerType - The type of indentation to use (terminate at end of scope vs last line of code in scope)
|
|
44
|
+
*/
|
|
45
|
+
constructor(
|
|
46
|
+
lines: Set<Line>,
|
|
47
|
+
state: EditorState,
|
|
48
|
+
unitWidth: number,
|
|
49
|
+
markerType: 'fullScope' | 'codeOnly',
|
|
50
|
+
) {
|
|
51
|
+
this.lines = lines;
|
|
52
|
+
this.state = state;
|
|
53
|
+
this.map = new Map();
|
|
54
|
+
this.unitWidth = unitWidth;
|
|
55
|
+
this.markerType = markerType;
|
|
56
|
+
|
|
57
|
+
for (const line of this.lines) {
|
|
58
|
+
this.add(line);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.state.facet(indentationMarkerConfig).highlightActiveBlock) {
|
|
62
|
+
this.findAndSetActiveLines();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if the indentation map has an entry for the given line.
|
|
68
|
+
*
|
|
69
|
+
* @param line - The {@link Line} or line number to check for.
|
|
70
|
+
*/
|
|
71
|
+
has(line: Line | number) {
|
|
72
|
+
return this.map.has(typeof line === 'number' ? line : line.number);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the {@link IndentEntry} for the given line.
|
|
77
|
+
*
|
|
78
|
+
* Note that this function will throw an error if the line does not exist in the map.
|
|
79
|
+
*
|
|
80
|
+
* @param line - The {@link Line} or line number to get the entry for.
|
|
81
|
+
*/
|
|
82
|
+
get(line: Line | number) {
|
|
83
|
+
const entry = this.map.get(typeof line === 'number' ? line : line.number);
|
|
84
|
+
|
|
85
|
+
if (!entry) {
|
|
86
|
+
throw new Error('Line not found in indentation map');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sets the {@link IndentEntry} for the given line.
|
|
94
|
+
*
|
|
95
|
+
* @param line - The {@link Line} to set the entry for.
|
|
96
|
+
* @param col - The visual beginning whitespace width of the line.
|
|
97
|
+
* @param level - The indentation level of the line.
|
|
98
|
+
*/
|
|
99
|
+
private set(line: Line, col: number, level: number) {
|
|
100
|
+
const empty = !line.text.trim().length;
|
|
101
|
+
const entry: IndentEntry = { line, col, level, empty };
|
|
102
|
+
this.map.set(entry.line.number, entry);
|
|
103
|
+
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Adds a line to the indentation map.
|
|
109
|
+
*
|
|
110
|
+
* @param line - The {@link Line} to add to the map.
|
|
111
|
+
*/
|
|
112
|
+
private add(line: Line) {
|
|
113
|
+
if (this.has(line)) {
|
|
114
|
+
return this.get(line);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// empty lines continue their indentation from surrounding lines
|
|
118
|
+
if (!line.length || !line.text.trim().length) {
|
|
119
|
+
// the very first line, if empty, is just ignored and set as a 0 indent level
|
|
120
|
+
if (line.number === 1) {
|
|
121
|
+
return this.set(line, 0, 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// if we're at the end, we'll just use the previous line's indentation
|
|
125
|
+
if (line.number === this.state.doc.lines) {
|
|
126
|
+
const prev = this.closestNonEmpty(line, -1);
|
|
127
|
+
|
|
128
|
+
return this.set(line, 0, prev.level);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const prev = this.closestNonEmpty(line, -1);
|
|
132
|
+
const next = this.closestNonEmpty(line, 1);
|
|
133
|
+
|
|
134
|
+
// if the next line ends the block and the marker type is not set to codeOnly,
|
|
135
|
+
// we'll just use the previous line's indentation
|
|
136
|
+
if (prev.level >= next.level && this.markerType !== 'codeOnly') {
|
|
137
|
+
return this.set(line, 0, prev.level);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// having an indent marker that starts from an empty line looks weird
|
|
141
|
+
if (prev.empty && prev.level === 0 && next.level !== 0) {
|
|
142
|
+
return this.set(line, 0, 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// if the next indentation level is greater than the previous,
|
|
146
|
+
// we'll only increment up to the next indentation level. this prevents
|
|
147
|
+
// a weirdly "backwards propagating" indentation.
|
|
148
|
+
if (next.level > prev.level) {
|
|
149
|
+
return this.set(line, 0, prev.level + 1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// else, we default to the next line's indentation
|
|
153
|
+
return this.set(line, 0, next.level);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const col = numColumns(line.text, this.state.tabSize);
|
|
157
|
+
const level = Math.floor(col / this.unitWidth);
|
|
158
|
+
|
|
159
|
+
return this.set(line, col, level);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Finds the closest non-empty line, starting from the given line.
|
|
164
|
+
*
|
|
165
|
+
* @param from - The {@link Line} to start from.
|
|
166
|
+
* @param dir - The direction to search in. Either `1` or `-1`.
|
|
167
|
+
*/
|
|
168
|
+
private closestNonEmpty(from: Line, dir: -1 | 1) {
|
|
169
|
+
let lineNo = from.number + dir;
|
|
170
|
+
|
|
171
|
+
while (dir === -1 ? lineNo >= 1 : lineNo <= this.state.doc.lines) {
|
|
172
|
+
if (this.has(lineNo)) {
|
|
173
|
+
const entry = this.get(lineNo);
|
|
174
|
+
if (!entry.empty) {
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// we can check if the line is empty, if it's not, we can
|
|
180
|
+
// just create a new entry for it and return it.
|
|
181
|
+
// this prevents us from hitting the beginning/end of the document unnecessarily.
|
|
182
|
+
|
|
183
|
+
const line = this.state.doc.line(lineNo);
|
|
184
|
+
|
|
185
|
+
if (line.text.trim().length) {
|
|
186
|
+
const col = numColumns(line.text, this.state.tabSize);
|
|
187
|
+
const level = Math.floor(col / this.unitWidth);
|
|
188
|
+
|
|
189
|
+
return this.set(line, col, level);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lineNo += dir;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// if we're here, we didn't find anything.
|
|
196
|
+
// that means we're at the beginning/end of the document,
|
|
197
|
+
// and the first/last line is empty.
|
|
198
|
+
|
|
199
|
+
const line = this.state.doc.line(dir === -1 ? 1 : this.state.doc.lines);
|
|
200
|
+
|
|
201
|
+
return this.set(line, 0, 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Finds the state's active block (via the current selection) and sets all
|
|
206
|
+
* the active indent level for the lines in the block.
|
|
207
|
+
*/
|
|
208
|
+
private findAndSetActiveLines() {
|
|
209
|
+
const currentLine = getCurrentLine(this.state);
|
|
210
|
+
|
|
211
|
+
if (!this.has(currentLine)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let current = this.get(currentLine);
|
|
216
|
+
|
|
217
|
+
// check if the current line is starting a new block, if yes, we want to
|
|
218
|
+
// start from inside the block.
|
|
219
|
+
if (this.has(current.line.number + 1)) {
|
|
220
|
+
const next = this.get(current.line.number + 1);
|
|
221
|
+
if (next.level > current.level) {
|
|
222
|
+
current = next;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// same, but if the current line is ending a block
|
|
227
|
+
if (this.has(current.line.number - 1)) {
|
|
228
|
+
const prev = this.get(current.line.number - 1);
|
|
229
|
+
if (prev.level > current.level) {
|
|
230
|
+
current = prev;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (current.level === 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
current.active = current.level;
|
|
239
|
+
|
|
240
|
+
let start: number;
|
|
241
|
+
let end: number;
|
|
242
|
+
|
|
243
|
+
// iterate to the start of the block
|
|
244
|
+
for (start = current.line.number; start > 1; start--) {
|
|
245
|
+
if (!this.has(start - 1)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const prev = this.get(start - 1);
|
|
250
|
+
|
|
251
|
+
if (prev.level < current.level) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
prev.active = current.level;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// iterate to the end of the block
|
|
259
|
+
for (end = current.line.number; end < this.state.doc.lines; end++) {
|
|
260
|
+
if (!this.has(end + 1)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const next = this.get(end + 1);
|
|
265
|
+
|
|
266
|
+
if (next.level < current.level) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
next.active = current.level;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|