Skip to content

Commit c85b4cc

Browse files
committed
fix: fixes live preview crash, improves LP rendering
1 parent 59449ce commit c85b4cc

File tree

4 files changed

+237
-250
lines changed

4 files changed

+237
-250
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@types/node": "^14.14.2",
2727
"dotenv": "^10.0.0",
2828
"esbuild": "^0.14.2",
29-
"obsidian": "^0.16.0",
29+
"obsidian": "^0.16.3",
3030
"rollup": "^2.32.1",
3131
"rollup-plugin-css-only": "^3.1.0",
3232
"standard-version": "^9.3.2",

src/live-preview.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* inspired and adapted from https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/main.ts
3+
*
4+
* The original work is MIT-licensed.
5+
*
6+
* MIT License
7+
*
8+
* Copyright (c) 2022 artisticat1
9+
*
10+
* Permission is hereby granted, free of charge, to any person obtaining a copy
11+
* of this software and associated documentation files (the "Software"), to deal
12+
* in the Software without restriction, including without limitation the rights
13+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
* copies of the Software, and to permit persons to whom the Software is
15+
* furnished to do so, subject to the following conditions:
16+
*
17+
* The above copyright notice and this permission notice shall be included in all
18+
* copies or substantial portions of the Software.
19+
*
20+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
* SOFTWARE.
27+
*
28+
* */
29+
30+
import {
31+
Decoration,
32+
DecorationSet,
33+
EditorView,
34+
ViewPlugin,
35+
ViewUpdate,
36+
WidgetType
37+
} from "@codemirror/view";
38+
import { EditorSelection, Range } from "@codemirror/state";
39+
import { syntaxTree } from "@codemirror/language";
40+
import {
41+
Component,
42+
editorEditorField,
43+
editorLivePreviewField,
44+
editorViewField
45+
} from "obsidian";
46+
import Processor from "./processor";
47+
48+
function selectionAndRangeOverlap(
49+
selection: EditorSelection,
50+
rangeFrom: number,
51+
rangeTo: number
52+
) {
53+
for (const range of selection.ranges) {
54+
if (range.from <= rangeTo && range.to >= rangeFrom) {
55+
return true;
56+
}
57+
}
58+
59+
return false;
60+
}
61+
62+
/* class InlineWidget extends WidgetType {
63+
constructor(
64+
readonly cssClasses: string[],
65+
readonly rawQuery: string,
66+
private el: HTMLElement,
67+
private view: EditorView
68+
) {
69+
super();
70+
}
71+
72+
// Widgets only get updated when the raw query changes/the element gets focus and loses it
73+
// to prevent redraws when the editor updates.
74+
eq(other: InlineWidget): boolean {
75+
if (other.rawQuery === this.rawQuery) {
76+
// change CSS classes without redrawing the element
77+
for (let value of other.cssClasses) {
78+
if (!this.cssClasses.includes(value)) {
79+
this.el.removeClass(value);
80+
} else {
81+
this.el.addClass(value);
82+
}
83+
}
84+
return true;
85+
}
86+
return false;
87+
}
88+
89+
// Add CSS classes and return HTML element.
90+
// In "complex" cases it will get filled with the correct text/child elements later.
91+
toDOM(view: EditorView): HTMLElement {
92+
this.el.addClasses(this.cssClasses);
93+
return this.el;
94+
}
95+
96+
/* Make queries only editable when shift is pressed (or navigated inside with the keyboard
97+
* or the mouse is placed at the end, but that is always possible regardless of this method).
98+
* Mostly useful for links, and makes results selectable.
99+
* If the widgets should always be expandable, make this always return false.
100+
*/
101+
/* ignoreEvent(event: MouseEvent | Event): boolean {
102+
// instanceof check does not work in pop-out windows, so check it like this
103+
if (event.type === "mousedown") {
104+
const currentPos = this.view.posAtCoords({
105+
x: (event as MouseEvent).x,
106+
y: (event as MouseEvent).y
107+
});
108+
if ((event as MouseEvent).shiftKey) {
109+
// Set the cursor after the element so that it doesn't select starting from the last cursor position.
110+
if (currentPos) {
111+
//@ts-ignore
112+
const { editor } = this.view.state
113+
.field(editorEditorField)
114+
.state.field(editorViewField);
115+
editor.setCursor(editor.offsetToPos(currentPos));
116+
}
117+
return false;
118+
}
119+
}
120+
return true;
121+
}
122+
} */
123+
124+
function inlineRender(view: EditorView) {
125+
// still doesn't work as expected for tables and callouts
126+
127+
const currentFile = app.workspace.getActiveFile();
128+
if (!currentFile) return;
129+
130+
const widgets: Range<Decoration>[] = [];
131+
const selection = view.state.selection;
132+
/* before:
133+
* em for italics
134+
* highlight for highlight
135+
* after:
136+
* strong for bold
137+
* strikethrough for strikethrough
138+
*/
139+
for (const { from, to } of view.visibleRanges) {
140+
syntaxTree(view.state).iterate({
141+
from,
142+
to,
143+
enter: ({ node }) => {
144+
const type = node.type;
145+
// markdown formatting symbols
146+
if (type.name.includes("formatting")) return;
147+
148+
// contains the position of node
149+
const start = node.from;
150+
const end = node.to;
151+
// don't continue if current cursor position and inline code node (including formatting
152+
// symbols) overlap
153+
if (selectionAndRangeOverlap(selection, start - 1, end + 1))
154+
return;
155+
156+
const original = view.state.doc.sliceString(start, end).trim();
157+
if (!Processor.END_RE.test(original)) return;
158+
159+
/* If the query result is predefined text (e.g. in the case of errors), set innerText to it.
160+
* Otherwise, pass on an empty element and fill it in later.
161+
* This is necessary because {@link InlineWidget.toDOM} is synchronous but some rendering
162+
* asynchronous.
163+
*/
164+
const parsed = Processor.parse(original) ?? [];
165+
166+
for (const item of parsed) {
167+
const { attributes, text } = item;
168+
const firstBracket = original
169+
.slice(0, original.indexOf(text))
170+
.lastIndexOf("{");
171+
172+
const lastBracket = original.indexOf(
173+
"}",
174+
original.indexOf(text)
175+
);
176+
177+
/* const classes = getCssClasses(type.name); */
178+
/* return; */
179+
widgets.push(
180+
Decoration.replace({
181+
/* widget: new InlineWidget(classes, code, el, view), */
182+
inclusive: false,
183+
block: false
184+
}).range(start + firstBracket, start + lastBracket + 1),
185+
Decoration.mark({
186+
inclusive: true,
187+
attributes: Object.fromEntries(attributes)
188+
}).range(start, end)
189+
);
190+
}
191+
}
192+
});
193+
}
194+
195+
return Decoration.set(widgets, true);
196+
}
197+
198+
export function inlinePlugin() {
199+
return ViewPlugin.fromClass(
200+
class {
201+
decorations: DecorationSet;
202+
203+
constructor(view: EditorView) {
204+
this.decorations = inlineRender(view) ?? Decoration.none;
205+
}
206+
207+
update(update: ViewUpdate) {
208+
// only activate in LP and not source mode
209+
//@ts-ignore
210+
if (!update.state.field(editorLivePreviewField)) {
211+
this.decorations = Decoration.none;
212+
return;
213+
}
214+
if (
215+
update.docChanged ||
216+
update.viewportChanged ||
217+
update.selectionSet
218+
) {
219+
this.decorations =
220+
inlineRender(update.view) ?? Decoration.none;
221+
}
222+
}
223+
},
224+
{ decorations: (v) => v.decorations }
225+
);
226+
}

0 commit comments

Comments
 (0)