add new live preview renderer and supporting settings values

This commit is contained in:
Eric Sowell 2024-02-22 23:02:34 -06:00
parent ddb9ca65d2
commit a6ef3a830c
5 changed files with 218 additions and 40 deletions

View file

@ -5,6 +5,8 @@ import { DEFAULT_SETTINGS, LinkRangeSettings, LinkRangeSettingTab } from 'src/se
import { linkRangePostProcessor } from 'src/markdownPostProcessor';
import { checkLink } from 'src/utils';
import { LinkRangeView } from 'src/linkRangeView';
import { buildCMViewPlugin } from 'src/livePreviewDisplayView';
import { Prec } from "@codemirror/state";
export default class LinkRange extends Plugin {
settings: LinkRangeSettings;
@ -28,6 +30,9 @@ export default class LinkRange extends Plugin {
return new LinkRangeView(this.settings, this.app)
}));
const ext = Prec.lowest(buildCMViewPlugin(this.app, this.settings));
this.registerEditorExtension(ext);
const pagePreviewPlugin = this.app.internalPlugins.plugins["page-preview"];
console.log("LinkRange: Hooking into page-preview onHover calls")

78
package-lock.json generated
View file

@ -7,6 +7,7 @@
"": {
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "^5.60.7",
@ -14,6 +15,7 @@
"monkey-around": "^2.3.0"
},
"devDependencies": {
"@codemirror/language": "6.10.1",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
@ -54,13 +56,13 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
"integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
@ -87,17 +89,17 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
"integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz",
"integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A=="
},
"node_modules/@codemirror/view": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz",
"integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==",
"version": "6.24.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.0.tgz",
"integrity": "sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w==",
"dependencies": {
"@codemirror/state": "^6.1.4",
"style-mod": "^4.0.0",
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
@ -524,9 +526,9 @@
"peer": true
},
"node_modules/@lezer/common": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
"integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
},
"node_modules/@lezer/highlight": {
"version": "1.1.3",
@ -2374,9 +2376,9 @@
}
},
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz",
"integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA=="
},
"node_modules/supports-color": {
"version": "7.2.0",
@ -2596,13 +2598,13 @@
}
},
"@codemirror/language": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
"integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
@ -2629,17 +2631,17 @@
}
},
"@codemirror/state": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
"integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz",
"integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A=="
},
"@codemirror/view": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz",
"integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==",
"version": "6.24.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.0.tgz",
"integrity": "sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w==",
"requires": {
"@codemirror/state": "^6.1.4",
"style-mod": "^4.0.0",
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
@ -2849,9 +2851,9 @@
"peer": true
},
"@lezer/common": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
"integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
},
"@lezer/highlight": {
"version": "1.1.3",
@ -4200,9 +4202,9 @@
"peer": true
},
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz",
"integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA=="
},
"supports-color": {
"version": "7.2.0",

View file

@ -21,7 +21,8 @@
"obsidian": "latest",
"patch-package": "^6.5.1",
"tslib": "2.4.0",
"typescript": "4.7.4"
"typescript": "4.7.4",
"@codemirror/language": "6.10.1"
},
"dependencies": {
"@types/codemirror": "^5.60.7",

View file

@ -0,0 +1,142 @@
import { App } from 'obsidian';
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { LinkRangeSettings } from "./settings";
class CharacterOverwriteWidget extends WidgetType {
private char: string;
constructor(char: string) {
super();
this.char = char;
}
toDOM() {
let el = document.createElement("span");
el.innerText = this.char;
el.style.textDecoration = 'underline';
return el;
}
}
export function buildCMViewPlugin(app: App, settings: LinkRangeSettings) {
const viewPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
decoratedRanges: Array<{ from: number, to: number }>;
lastLocation: { from: number; to: number; };
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view, null);
this.lastLocation = { from: 0, to: 0 };;
}
update(update: ViewUpdate) {
let currentLocation = {
from: update.state.selection.ranges[0].from,
to: update.state.selection.ranges[0].to
};
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view, currentLocation);
}
else {
// isRepeatUpdate - avoid repeated updates and renders
let isRepeatUpdate = this.lastLocation.from == currentLocation.from && this.lastLocation.to === currentLocation.to;
if (update.state.selection.ranges.length > 0 && !isRepeatUpdate) {
this.decorations = this.buildDecorations(update.view, currentLocation);
}
}
this.lastLocation = currentLocation;
}
buildDecorations(view: EditorView, location: { from: number, to: number } | null): DecorationSet {
let builder = new RangeSetBuilder<Decoration>();
const lastPassDecoratedRanges: Array<{from: number, to: number}> = this.decoratedRanges;
this.decoratedRanges = [];
const inLastPass = function(nodeStart: number, index: number | undefined): boolean {
if (index === undefined) {
return false;
}
for (let i in lastPassDecoratedRanges) {
const rng = lastPassDecoratedRanges[i];
if (rng.from == nodeStart && index >= rng.from && index <= rng.to) {
return true;
}
}
return false;
}
for (let {from, to} of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
// Big thanks to Supercharged Links. Used your code as an example! https://github.com/mdelobelle/obsidian_supercharged_links
// TODO: there used to be a different way than splitting the name but the api for this has
// changed in CodeMirror. Should look into it more.
const tokenProps = node.type.name.split('_');
if (tokenProps) {
const props = new Set(tokenProps);
const isLink = props.has("hmd-internal-link");
const isAlias = props.has("link-alias");
const isPipe = props.has("link-alias-pipe");
const isMDUrl = props.has('url');
if (isLink && !isAlias && !isPipe || isMDUrl) {
let linkText = view.state.doc.sliceString(node.from, node.to);
const indexOfHeaderMarker = linkText.indexOf('#') + node.from;
if (indexOfHeaderMarker >= node.from && indexOfHeaderMarker <= node.to) {
if (!inLastPass(node.from - 2, location?.from)) {
let attributeDeco = Decoration.mark({
class: 'heading-range-live-link'
});
builder.add(node.from - 2, node.to + 2, attributeDeco);
let overrideP2HWidget = Decoration.widget({
widget: new CharacterOverwriteWidget(settings.headingVisual),
});
builder.add(indexOfHeaderMarker, indexOfHeaderMarker + 1, overrideP2HWidget);
const indexOfRangeMarker = linkText.indexOf(settings.headingSeparator) + node.from;
if (indexOfRangeMarker >= node.from && indexOfRangeMarker <= node.to) {
let overrideH2HWidget = Decoration.widget({
widget: new CharacterOverwriteWidget(settings.headingSeparatorVisual),
});
builder.add(indexOfRangeMarker, indexOfRangeMarker + settings.headingSeparator.length, overrideH2HWidget);
}
}
this.decoratedRanges.push({
from: node.from - 2,
to: node.to + 2
});
}
}
}
}
})
}
const bufferedDecs = builder.finish();
return bufferedDecs;
}
},
{
decorations: v => v.decorations
}
);
return viewPlugin;
}

View file

@ -6,12 +6,16 @@ export interface LinkRangeSettings {
headingSeparator: string;
altFormat: string;
endInclusive: boolean;
headingVisual: string,
headingSeparatorVisual: string,
}
export const DEFAULT_SETTINGS: LinkRangeSettings = {
headingSeparator: '..',
altFormat: '$note:$h1-$h2',
endInclusive: true
endInclusive: true,
headingVisual: ':',
headingSeparatorVisual: '-',
}
export class LinkRangeSettingTab extends PluginSettingTab {
@ -53,6 +57,30 @@ export class LinkRangeSettingTab extends PluginSettingTab {
postProcessorUpdate(this.app)
}));
new Setting(containerEl)
.setName('Heading visual override for live preview')
.setDesc('Defines the override for the heading (#) character for live preview mode.')
.addText(text => text
.setPlaceholder('Enter an override')
.setValue(this.plugin.settings.headingVisual)
.onChange(async (value) => {
this.plugin.settings.headingVisual = value;
await this.plugin.saveSettings();
postProcessorUpdate(this.app)
}));
new Setting(containerEl)
.setName('Heading separator visual override for live preview')
.setDesc('Defines the override for the heading (>, or whatever is defined) character for live preview mode.')
.addText(text => text
.setPlaceholder('Enter an override')
.setValue(this.plugin.settings.headingSeparatorVisual)
.onChange(async (value) => {
this.plugin.settings.headingSeparatorVisual = value;
await this.plugin.saveSettings();
postProcessorUpdate(this.app)
}));
new Setting(containerEl)
.setName('End Inclusive')
.setDesc('Whether or not the end heading should be inclusive or exclusive')