Fixes #164: Rewrote all CM view plugins to statefields
parent
453c613ef4
commit
c8c4271aeb
|
@ -1,4 +1,4 @@
|
|||
import { nodeAtPos, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
||||
import { nodeAtPos, ParseTree } from "$sb/lib/tree.ts";
|
||||
import { replaceAsync } from "$sb/lib/util.ts";
|
||||
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import {
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
|
||||
function hideNodes(view: EditorView) {
|
||||
function hideNodes(state: EditorState) {
|
||||
const widgets: any[] = [];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "HorizontalRule" &&
|
||||
!isCursorInRange(view.state, [node.from, node.to])
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
widgets.push(
|
||||
|
@ -29,7 +23,7 @@ function hideNodes(view: EditorView) {
|
|||
|
||||
if (
|
||||
node.name === "Image" &&
|
||||
!isCursorInRange(view.state, [node.from, node.to])
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
}
|
||||
|
@ -38,7 +32,7 @@ function hideNodes(view: EditorView) {
|
|||
node.name === "FrontMatterMarker"
|
||||
) {
|
||||
const parent = node.node.parent!;
|
||||
if (!isCursorInRange(view.state, [parent.from, parent.to])) {
|
||||
if (!isCursorInRange(state, [parent.from, parent.to])) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
|
@ -54,7 +48,7 @@ function hideNodes(view: EditorView) {
|
|||
// Hide ONLY if CodeMark is not insine backticks (InlineCode) and the cursor is placed outside
|
||||
if (
|
||||
parent.node.name !== "InlineCode" &&
|
||||
!isCursorInRange(view.state, [parent.from, parent.to])
|
||||
!isCursorInRange(state, [parent.from, parent.to])
|
||||
) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
|
@ -68,19 +62,6 @@ function hideNodes(view: EditorView) {
|
|||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export const cleanBlockPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = hideNodes(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
this.decorations = hideNodes(update.view);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
export function cleanBlockPlugin() {
|
||||
return decoratorStateField(hideNodes);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,26 @@
|
|||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import {
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
|
||||
class BlockquotePlugin {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.decorateLists(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.decorateLists(update.view);
|
||||
}
|
||||
}
|
||||
private decorateLists(view: EditorView) {
|
||||
const widgets: any[] = [];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
enter: ({ type, from, to }) => {
|
||||
if (isCursorInRange(view.state, [from, to])) return;
|
||||
if (type.name === "QuoteMark") {
|
||||
widgets.push(invisibleDecoration.range(from, to));
|
||||
widgets.push(
|
||||
Decoration.line({ class: "sb-blockquote-outside" }).range(from),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
function decorateBlockQuote(state: EditorState) {
|
||||
const widgets: any[] = [];
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (isCursorInRange(state, [from, to])) return;
|
||||
if (type.name === "QuoteMark") {
|
||||
widgets.push(invisibleDecoration.range(from, to));
|
||||
widgets.push(
|
||||
Decoration.line({ class: "sb-blockquote-outside" }).range(from),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export function blockquotePlugin() {
|
||||
return decoratorStateField(decorateBlockQuote);
|
||||
}
|
||||
export const blockquotePlugin = ViewPlugin.fromClass(
|
||||
BlockquotePlugin,
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Extension } from "../deps.ts";
|
|||
import { Editor } from "../editor.tsx";
|
||||
import { blockquotePlugin } from "./block_quote.ts";
|
||||
import { directivePlugin } from "./directive.ts";
|
||||
import { hideHeaderMarkPlugin, hideMarks } from "./hide_mark.ts";
|
||||
import { hideHeaderMarkPlugin, hideMarksPlugin } from "./hide_mark.ts";
|
||||
import { cleanBlockPlugin } from "./block.ts";
|
||||
import { linkPlugin } from "./link.ts";
|
||||
import { listBulletPlugin } from "./list.ts";
|
||||
|
@ -15,11 +15,11 @@ import { cleanCommandLinkPlugin } from "./command_link.ts";
|
|||
export function cleanModePlugins(editor: Editor) {
|
||||
return [
|
||||
linkPlugin(editor),
|
||||
directivePlugin,
|
||||
blockquotePlugin,
|
||||
hideMarks(),
|
||||
hideHeaderMarkPlugin,
|
||||
cleanBlockPlugin,
|
||||
directivePlugin(),
|
||||
blockquotePlugin(),
|
||||
hideMarksPlugin(),
|
||||
hideHeaderMarkPlugin(),
|
||||
cleanBlockPlugin(),
|
||||
taskListPlugin({
|
||||
// TODO: Move this logic elsewhere?
|
||||
onCheckboxClick: (pos) => {
|
||||
|
@ -34,8 +34,8 @@ export function cleanModePlugins(editor: Editor) {
|
|||
editor.dispatchAppEvent("page:click", clickEvent);
|
||||
},
|
||||
}),
|
||||
listBulletPlugin,
|
||||
tablePlugin,
|
||||
listBulletPlugin(),
|
||||
tablePlugin(editor),
|
||||
cleanWikiLinkPlugin(editor),
|
||||
cleanCommandLinkPlugin(editor),
|
||||
] as Extension[];
|
||||
|
|
|
@ -1,99 +1,75 @@
|
|||
import { commandLinkRegex, pageLinkRegex } from "../../common/parser.ts";
|
||||
import { ClickEvent } from "../../plug-api/app_event.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { commandLinkRegex } from "../../common/parser.ts";
|
||||
import { ClickEvent } from "$sb/app_event.ts";
|
||||
import { Decoration, syntaxTree } from "../deps.ts";
|
||||
import { Editor } from "../editor.tsx";
|
||||
import {
|
||||
ButtonWidget,
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
|
||||
/**
|
||||
* Plugin to hide path prefix when the cursor is not inside.
|
||||
*/
|
||||
export function cleanCommandLinkPlugin(editor: Editor) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.compute(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged || update.viewportChanged || update.selectionSet
|
||||
) {
|
||||
this.decorations = this.compute(update.view);
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
// let parentRange: [number, number];
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "CommandLink") {
|
||||
return;
|
||||
}
|
||||
if (isCursorInRange(state, [from, to])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
compute(view: EditorView): DecorationSet {
|
||||
const widgets: any[] = [];
|
||||
// let parentRange: [number, number];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "CommandLink") {
|
||||
return;
|
||||
}
|
||||
if (isCursorInRange(view.state, [from, to])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = view.state.sliceDoc(from, to);
|
||||
const match = commandLinkRegex.exec(text);
|
||||
if (!match) return;
|
||||
const [_fullMatch, command, _pipePart, alias] = match;
|
||||
const text = state.sliceDoc(from, to);
|
||||
const match = commandLinkRegex.exec(text);
|
||||
if (!match) return;
|
||||
const [_fullMatch, command, _pipePart, alias] = match;
|
||||
|
||||
// Hide the whole thing
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
);
|
||||
// Hide the whole thing
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
);
|
||||
|
||||
const linkText = alias || command;
|
||||
// And replace it with a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ButtonWidget(
|
||||
linkText,
|
||||
`Run command: ${command}`,
|
||||
"sb-command-button",
|
||||
(e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return view.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
const linkText = alias || command;
|
||||
// And replace it with a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ButtonWidget(
|
||||
linkText,
|
||||
`Run command: ${command}`,
|
||||
"sb-command-button",
|
||||
(e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return editor.editorView!.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,70 +1,44 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
syntaxTree,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { isCursorInRange } from "./util.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import { decoratorStateField, isCursorInRange } from "./util.ts";
|
||||
|
||||
function getDirectives(view: EditorView) {
|
||||
function getDirectives(state: EditorState) {
|
||||
const widgets: any[] = [];
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "CommentBlock") {
|
||||
return;
|
||||
}
|
||||
const text = view.state.sliceDoc(from, to);
|
||||
if (/<!--\s*#/.exec(text)) {
|
||||
// Open directive
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-start",
|
||||
}).range(from),
|
||||
);
|
||||
} else if (/<!--\s*\//.exec(text)) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-end",
|
||||
}).range(from),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!isCursorInRange(view.state, [from, to])) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-outside",
|
||||
}).range(from),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "CommentBlock") {
|
||||
return;
|
||||
}
|
||||
const text = state.sliceDoc(from, to);
|
||||
if (/<!--\s*#/.exec(text)) {
|
||||
// Open directive
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-start",
|
||||
}).range(from),
|
||||
);
|
||||
} else if (/<!--\s*\//.exec(text)) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-end",
|
||||
}).range(from),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!isCursorInRange(state, [from, to])) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-directive-outside",
|
||||
}).range(from),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export const directivePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = getDirectives(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged ||
|
||||
update.viewportChanged ||
|
||||
update.selectionSet
|
||||
) {
|
||||
this.decorations = getDirectives(update.view);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
export function directivePlugin() {
|
||||
return decoratorStateField(getDirectives);
|
||||
}
|
||||
|
|
|
@ -2,18 +2,12 @@
|
|||
// Original author: Pranav Karawale
|
||||
// License: Apache License 2.0.
|
||||
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import {
|
||||
checkRangeOverlap,
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
|
||||
/**
|
||||
|
@ -37,22 +31,16 @@ const markTypes = [
|
|||
];
|
||||
|
||||
/**
|
||||
* Plugin to hide marks when the they are not in the editor selection.
|
||||
* Ixora hide marks plugin.
|
||||
*
|
||||
* This plugin allows to:
|
||||
* - Hide marks when they are not in the editor selection.
|
||||
*/
|
||||
class HideMarkPlugin {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.compute(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.compute(update.view);
|
||||
}
|
||||
}
|
||||
compute(view: EditorView): DecorationSet {
|
||||
export function hideMarksPlugin() {
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
let parentRange: [number, number];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to, node }) => {
|
||||
if (typesWithMarks.includes(type.name)) {
|
||||
// There can be a possibility that the current node is a
|
||||
|
@ -64,7 +52,7 @@ class HideMarkPlugin {
|
|||
) {
|
||||
return;
|
||||
} else parentRange = [from, to];
|
||||
if (isCursorInRange(view.state, [from, to])) return;
|
||||
if (isCursorInRange(state, [from, to])) return;
|
||||
const innerTree = node.toTree();
|
||||
innerTree.iterate({
|
||||
enter({ type, from: markFrom, to: markTo }) {
|
||||
|
@ -83,78 +71,36 @@ class HideMarkPlugin {
|
|||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ixora hide marks plugin.
|
||||
*
|
||||
* This plugin allows to:
|
||||
* - Hide marks when they are not in the editor selection.
|
||||
*/
|
||||
export const hideMarks = () => [
|
||||
ViewPlugin.fromClass(HideMarkPlugin, {
|
||||
decorations: (v) => v.decorations,
|
||||
}),
|
||||
];
|
||||
|
||||
// HEADINGS
|
||||
|
||||
class HideHeaderMarkPlugin {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.hideHeaderMark(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.hideHeaderMark(update.view);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Function to decide if to insert a decoration to hide the header mark
|
||||
* @param view - Editor view
|
||||
* @returns The `Decoration`s that hide the header marks
|
||||
*/
|
||||
private hideHeaderMark(view: EditorView) {
|
||||
export function hideHeaderMarkPlugin() {
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
const ranges = view.state.selection.ranges;
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (!type.name.startsWith("ATXHeading")) {
|
||||
return;
|
||||
}
|
||||
// Get the active line
|
||||
const line = view.lineBlockAt(from);
|
||||
// If any cursor overlaps with the heading line, skip
|
||||
const cursorOverlaps = ranges.some(({ from, to }) =>
|
||||
checkRangeOverlap([from, to], [line.from, line.to])
|
||||
);
|
||||
if (cursorOverlaps && type.name === "HeaderMark") {
|
||||
const line = state.sliceDoc(from, to);
|
||||
if (isCursorInRange(state, [from, to])) {
|
||||
widgets.push(
|
||||
Decoration.line({ class: "sb-header-inside" }).range(from),
|
||||
);
|
||||
return;
|
||||
} else if (cursorOverlaps) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
type.name === "HeaderMark" &&
|
||||
// Setext heading's horizontal lines are not hidden.
|
||||
/[#]/.test(view.state.sliceDoc(from, to))
|
||||
) {
|
||||
const dec = Decoration.replace({});
|
||||
widgets.push(dec.range(from, to + 1));
|
||||
}
|
||||
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
from + line.indexOf(" ") + 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin to hide the header mark.
|
||||
*
|
||||
* The header mark will not be hidden when:
|
||||
* - The cursor is on the active line
|
||||
* - The mark is on a line which is in the current selection
|
||||
*/
|
||||
export const hideHeaderMarkPlugin = ViewPlugin.fromClass(HideHeaderMarkPlugin, {
|
||||
decorations: (v) => v.decorations,
|
||||
});
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
EditorState,
|
||||
Range,
|
||||
syntaxTree,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
import { invisibleDecoration, isCursorInRange } from "./util.ts";
|
||||
import { decoratorStateField } from "./util.ts";
|
||||
|
||||
class InlineImageWidget extends WidgetType {
|
||||
constructor(readonly url: string, readonly title: string) {
|
||||
|
@ -35,21 +32,19 @@ class InlineImageWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
const inlineImages = (view: EditorView) => {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/;
|
||||
export function inlineImagesPlugin() {
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/;
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
syntaxTree(state).iterate({
|
||||
enter: (node) => {
|
||||
if (node.name !== "Image") {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageRexexResult = imageRegex.exec(
|
||||
view.state.sliceDoc(node.from, node.to),
|
||||
state.sliceDoc(node.from, node.to),
|
||||
);
|
||||
if (imageRexexResult === null || !imageRexexResult.groups) {
|
||||
return;
|
||||
|
@ -64,27 +59,7 @@ const inlineImages = (view: EditorView) => {
|
|||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
};
|
||||
|
||||
export const inlineImagesPlugin = () =>
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = inlineImages(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = inlineImages(update.view);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
Range,
|
||||
syntaxTree,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { Decoration, EditorState, Range, syntaxTree } from "../deps.ts";
|
||||
import { decoratorStateField } from "./util.ts";
|
||||
|
||||
interface WrapElement {
|
||||
selector: string;
|
||||
|
@ -14,16 +7,12 @@ interface WrapElement {
|
|||
nesting?: boolean;
|
||||
}
|
||||
|
||||
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
||||
let widgets: Range<Decoration>[] = [];
|
||||
const elementStack: string[] = [];
|
||||
const doc = view.state.doc;
|
||||
// Disabling the visible ranges for now, because it may be a bit buggy.
|
||||
// RISK: this may actually become slow for large documents.
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
export function lineWrapper(wrapElements: WrapElement[]) {
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
const elementStack: string[] = [];
|
||||
const doc = state.doc;
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
for (const wrapElement of wrapElements) {
|
||||
if (type.name == wrapElement.selector) {
|
||||
|
@ -55,30 +44,7 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
|||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// Widgets have to be sorted by `from` in ascending order
|
||||
widgets = widgets.sort((a, b) => {
|
||||
return a.from < b.from ? -1 : 1;
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
return Decoration.set(widgets);
|
||||
}
|
||||
|
||||
export const lineWrapper = (wrapElements: WrapElement[]) =>
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = wrapLines(view, wrapElements);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = wrapLines(update.view, wrapElements);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,104 +1,79 @@
|
|||
import { ClickEvent } from "../../plug-api/app_event.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { Decoration, syntaxTree } from "../deps.ts";
|
||||
import { Editor } from "../editor.tsx";
|
||||
import {
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
import { LinkWidget } from "./util.ts";
|
||||
|
||||
export function linkPlugin(editor: Editor) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(readonly view: EditorView) {
|
||||
this.decorations = this.calculateDecorations();
|
||||
}
|
||||
calculateDecorations() {
|
||||
const widgets: any[] = [];
|
||||
const view = this.view;
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
|
||||
iterateTreeInVisibleRanges(this.view, {
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "Link") {
|
||||
return;
|
||||
}
|
||||
// Adding 2 on each side due to [[ and ]] that are outside the WikiLinkPage node
|
||||
if (isCursorInRange(view.state, [from, to])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = view.state.sliceDoc(from, to);
|
||||
// Links are of the form [hell](https://example.com)
|
||||
const [anchorPart, linkPart] = text.split("]("); // Not pretty
|
||||
if (!linkPart) {
|
||||
// Invalid link
|
||||
return;
|
||||
}
|
||||
const cleanAnchor = anchorPart.substring(1); // cut off the initial [
|
||||
const cleanLink = linkPart.substring(0, linkPart.length - 1); // cut off the final )
|
||||
|
||||
// Hide the whole thing
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
);
|
||||
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new LinkWidget(
|
||||
{
|
||||
text: cleanAnchor,
|
||||
title: `Click to visit ${cleanLink}`,
|
||||
cssClass: "sb-link",
|
||||
href: cleanLink,
|
||||
callback: (e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link, approximate location
|
||||
return view.dispatch({
|
||||
selection: { anchor: from + 1 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged ||
|
||||
update.viewportChanged ||
|
||||
update.selectionSet
|
||||
) {
|
||||
this.decorations = this.calculateDecorations();
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "Link") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
// Adding 2 on each side due to [[ and ]] that are outside the WikiLinkPage node
|
||||
if (isCursorInRange(state, [from, to])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = state.sliceDoc(from, to);
|
||||
// Links are of the form [hell](https://example.com)
|
||||
const [anchorPart, linkPart] = text.split("]("); // Not pretty
|
||||
if (!linkPart) {
|
||||
// Invalid link
|
||||
return;
|
||||
}
|
||||
const cleanAnchor = anchorPart.substring(1); // cut off the initial [
|
||||
const cleanLink = linkPart.substring(0, linkPart.length - 1); // cut off the final )
|
||||
|
||||
// Hide the whole thing
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
);
|
||||
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new LinkWidget(
|
||||
{
|
||||
text: cleanAnchor,
|
||||
title: `Click to visit ${cleanLink}`,
|
||||
cssClass: "sb-link",
|
||||
href: cleanLink,
|
||||
callback: (e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link, approximate location
|
||||
return editor.editorView!.dispatch({
|
||||
selection: { anchor: from + 1 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,38 +2,19 @@
|
|||
// Original author: Pranav Karawale
|
||||
// License: Apache License 2.0.
|
||||
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts";
|
||||
import { Decoration, syntaxTree, WidgetType } from "../deps.ts";
|
||||
import { decoratorStateField, isCursorInRange } from "./util.ts";
|
||||
|
||||
const bulletListMarkerRE = /^[-+*]/;
|
||||
|
||||
/**
|
||||
* Plugin to add custom list bullet mark.
|
||||
*/
|
||||
class ListBulletPlugin {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.decorateLists(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.decorateLists(update.view);
|
||||
}
|
||||
}
|
||||
private decorateLists(view: EditorView) {
|
||||
export function listBulletPlugin() {
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (isCursorInRange(view.state, [from, to])) return;
|
||||
if (isCursorInRange(state, [from, to])) return;
|
||||
if (type.name === "ListMark") {
|
||||
const listMark = view.state.sliceDoc(from, to);
|
||||
const listMark = state.sliceDoc(from, to);
|
||||
if (bulletListMarkerRE.test(listMark)) {
|
||||
const dec = Decoration.replace({
|
||||
widget: new ListBulletWidget(listMark),
|
||||
|
@ -44,11 +25,8 @@ class ListBulletPlugin {
|
|||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
export const listBulletPlugin = ViewPlugin.fromClass(ListBulletPlugin, {
|
||||
decorations: (v) => v.decorations,
|
||||
});
|
||||
|
||||
/**
|
||||
* Widget to render list bullet mark.
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorState,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
syntaxTree,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
import {
|
||||
editorLines,
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
} from "./util.ts";
|
||||
|
||||
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
|
||||
import { ParseTree } from "$sb/lib/tree.ts";
|
||||
import { lezerToParseTree } from "../../common/parse_tree.ts";
|
||||
import type { Editor } from "../editor.tsx";
|
||||
|
||||
class TableViewWidget extends WidgetType {
|
||||
constructor(
|
||||
|
@ -49,25 +48,27 @@ class TableViewWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
class TablePlugin {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.decorateLists(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.decorateLists(update.view);
|
||||
}
|
||||
}
|
||||
private decorateLists(view: EditorView) {
|
||||
export function tablePlugin(editor: Editor) {
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
syntaxTree(state).iterate({
|
||||
enter: (node) => {
|
||||
const { from, to, name } = node;
|
||||
if (name !== "Table") return;
|
||||
if (isCursorInRange(view.state, [from, to])) return;
|
||||
if (isCursorInRange(state, [from, to])) return;
|
||||
|
||||
const lines = editorLines(view, from, to);
|
||||
const tableText = state.sliceDoc(from, to);
|
||||
const lineStrings = tableText.split("\n");
|
||||
|
||||
const lines: { from: number; to: number }[] = [];
|
||||
let fromIt = from;
|
||||
for (const line of lineStrings) {
|
||||
lines.push({
|
||||
from: fromIt,
|
||||
to: fromIt + line.length,
|
||||
});
|
||||
fromIt += line.length + 1;
|
||||
}
|
||||
|
||||
const firstLine = lines[0], lastLine = lines[lines.length - 1];
|
||||
|
||||
|
@ -84,12 +85,12 @@ class TablePlugin {
|
|||
),
|
||||
);
|
||||
});
|
||||
const text = view.state.sliceDoc(0, to);
|
||||
const text = state.sliceDoc(0, to);
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new TableViewWidget(
|
||||
from,
|
||||
view,
|
||||
editor.editorView!,
|
||||
lezerToParseTree(text, node.node),
|
||||
),
|
||||
}).range(from),
|
||||
|
@ -97,11 +98,5 @@ class TablePlugin {
|
|||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
export const tablePlugin = ViewPlugin.fromClass(
|
||||
TablePlugin,
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,73 +1,5 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
NodeType,
|
||||
SyntaxNodeRef,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts";
|
||||
|
||||
// TODO: Find a nicer way to inject this on task handler into the class
|
||||
function TaskListsPluginFactory(onCheckboxClick: (pos: number) => void) {
|
||||
return class TaskListsPlugin {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(
|
||||
view: EditorView,
|
||||
) {
|
||||
this.decorations = this.addCheckboxes(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.addCheckboxes(update.view);
|
||||
}
|
||||
}
|
||||
addCheckboxes(view: EditorView) {
|
||||
const widgets: any[] = [];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
enter: this.iterateTree(view, widgets),
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
private iterateTree(view: EditorView, widgets: any[]) {
|
||||
return ({ type, from, to, node }: SyntaxNodeRef) => {
|
||||
if (type.name !== "Task") return;
|
||||
let checked = false;
|
||||
// Iterate inside the task node to find the checkbox
|
||||
node.toTree().iterate({
|
||||
enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
|
||||
});
|
||||
if (checked) {
|
||||
widgets.push(
|
||||
Decoration.mark({
|
||||
tagName: "span",
|
||||
class: "cm-task-checked",
|
||||
}).range(from, to),
|
||||
);
|
||||
}
|
||||
|
||||
function iterateInner(type: NodeType, nfrom: number, nto: number) {
|
||||
if (type.name !== "TaskMarker") return;
|
||||
if (isCursorInRange(view.state, [from + nfrom, from + nto])) return;
|
||||
const checkbox = view.state.sliceDoc(from + nfrom, from + nto);
|
||||
// Checkbox is checked if it has a 'x' in between the []
|
||||
if ("xX".includes(checkbox[1])) checked = true;
|
||||
const dec = Decoration.replace({
|
||||
widget: new CheckboxWidget(
|
||||
checked,
|
||||
from + nfrom + 1,
|
||||
onCheckboxClick,
|
||||
),
|
||||
});
|
||||
widgets.push(dec.range(from + nfrom, from + nto));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
import { Decoration, NodeType, syntaxTree, WidgetType } from "../deps.ts";
|
||||
import { decoratorStateField, isCursorInRange } from "./util.ts";
|
||||
|
||||
/**
|
||||
* Widget to render checkbox for a task list item.
|
||||
|
@ -80,7 +12,7 @@ class CheckboxWidget extends WidgetType {
|
|||
) {
|
||||
super();
|
||||
}
|
||||
toDOM(_view: EditorView): HTMLElement {
|
||||
toDOM(): HTMLElement {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.classList.add("sb-checkbox");
|
||||
const checkbox = document.createElement("input");
|
||||
|
@ -100,7 +32,42 @@ class CheckboxWidget extends WidgetType {
|
|||
export function taskListPlugin(
|
||||
{ onCheckboxClick }: { onCheckboxClick: (pos: number) => void },
|
||||
) {
|
||||
return ViewPlugin.fromClass(TaskListsPluginFactory(onCheckboxClick), {
|
||||
decorations: (v) => v.decorations,
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
syntaxTree(state).iterate({
|
||||
enter({ type, from, to, node }) {
|
||||
if (type.name !== "Task") return;
|
||||
let checked = false;
|
||||
// Iterate inside the task node to find the checkbox
|
||||
node.toTree().iterate({
|
||||
enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
|
||||
});
|
||||
if (checked) {
|
||||
widgets.push(
|
||||
Decoration.mark({
|
||||
tagName: "span",
|
||||
class: "cm-task-checked",
|
||||
}).range(from, to),
|
||||
);
|
||||
}
|
||||
|
||||
function iterateInner(type: NodeType, nfrom: number, nto: number) {
|
||||
if (type.name !== "TaskMarker") return;
|
||||
if (isCursorInRange(state, [from + nfrom, from + nto])) return;
|
||||
const checkbox = state.sliceDoc(from + nfrom, from + nto);
|
||||
// Checkbox is checked if it has a 'x' in between the []
|
||||
if ("xX".includes(checkbox[1])) checked = true;
|
||||
const dec = Decoration.replace({
|
||||
widget: new CheckboxWidget(
|
||||
checked,
|
||||
from + nfrom + 1,
|
||||
onCheckboxClick,
|
||||
),
|
||||
});
|
||||
widgets.push(dec.range(from + nfrom, from + nto));
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
// License: Apache License 2.0.
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorState,
|
||||
EditorView,
|
||||
foldedRanges,
|
||||
SyntaxNodeRef,
|
||||
syntaxTree,
|
||||
StateField,
|
||||
Transaction,
|
||||
WidgetType,
|
||||
} from "../deps.ts";
|
||||
|
||||
|
@ -39,6 +40,25 @@ export class LinkWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
export function decoratorStateField(
|
||||
stateToDecoratorMapper: (state: EditorState) => DecorationSet,
|
||||
) {
|
||||
return StateField.define<DecorationSet>({
|
||||
create(state: EditorState) {
|
||||
return stateToDecoratorMapper(state);
|
||||
},
|
||||
|
||||
update(value: DecorationSet, tr: Transaction) {
|
||||
// if (tr.docChanged || tr.selection) {
|
||||
return stateToDecoratorMapper(tr.state);
|
||||
// }
|
||||
// return value;
|
||||
},
|
||||
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
}
|
||||
|
||||
export class ButtonWidget extends WidgetType {
|
||||
constructor(
|
||||
readonly text: string,
|
||||
|
@ -106,19 +126,6 @@ export function isCursorInRange(state: EditorState, range: [number, number]) {
|
|||
*/
|
||||
export const invisibleDecoration = Decoration.replace({});
|
||||
|
||||
export function iterateTreeInVisibleRanges(
|
||||
view: EditorView,
|
||||
iterateFns: {
|
||||
enter(node: SyntaxNodeRef): boolean | void;
|
||||
leave?(node: SyntaxNodeRef): void;
|
||||
},
|
||||
) {
|
||||
// for (const { from, to } of view.visibleRanges) {
|
||||
// syntaxTree(view.state).iterate({ ...iterateFns, from, to });
|
||||
// }
|
||||
syntaxTree(view.state).iterate(iterateFns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lines of the editor that are in the given range and not folded.
|
||||
* This function is of use when you need to get the lines of a particular
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import { pageLinkRegex } from "../../common/parser.ts";
|
||||
import { ClickEvent } from "../../plug-api/app_event.ts";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import { Decoration, syntaxTree } from "../deps.ts";
|
||||
import { Editor } from "../editor.tsx";
|
||||
import {
|
||||
decoratorStateField,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
LinkWidget,
|
||||
} from "./util.ts";
|
||||
|
||||
|
@ -19,119 +13,99 @@ import {
|
|||
* Plugin to hide path prefix when the cursor is not inside.
|
||||
*/
|
||||
export function cleanWikiLinkPlugin(editor: Editor) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.compute(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged || update.viewportChanged || update.selectionSet
|
||||
) {
|
||||
this.decorations = this.compute(update.view);
|
||||
return decoratorStateField((state) => {
|
||||
const widgets: any[] = [];
|
||||
// let parentRange: [number, number];
|
||||
syntaxTree(state).iterate({
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "WikiLink") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
compute(view: EditorView): DecorationSet {
|
||||
const widgets: any[] = [];
|
||||
// let parentRange: [number, number];
|
||||
iterateTreeInVisibleRanges(view, {
|
||||
enter: ({ type, from, to }) => {
|
||||
if (type.name !== "WikiLink") {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = view.state.sliceDoc(from, to);
|
||||
const match = pageLinkRegex.exec(text);
|
||||
if (!match) return;
|
||||
const [_fullMatch, page, pipePart, alias] = match;
|
||||
const text = state.sliceDoc(from, to);
|
||||
const match = pageLinkRegex.exec(text);
|
||||
if (!match) return;
|
||||
const [_fullMatch, page, pipePart, alias] = match;
|
||||
|
||||
const allPages = editor.space.listPages();
|
||||
let pageExists = false;
|
||||
let cleanPage = page;
|
||||
if (page.includes("@")) {
|
||||
cleanPage = page.split("@")[0];
|
||||
}
|
||||
for (const pageMeta of allPages) {
|
||||
if (pageMeta.name === cleanPage) {
|
||||
pageExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cleanPage === "") {
|
||||
// Empty page name, or local @anchor use
|
||||
pageExists = true;
|
||||
}
|
||||
const allPages = editor.space.listPages();
|
||||
let pageExists = false;
|
||||
let cleanPage = page;
|
||||
if (page.includes("@")) {
|
||||
cleanPage = page.split("@")[0];
|
||||
}
|
||||
for (const pageMeta of allPages) {
|
||||
if (pageMeta.name === cleanPage) {
|
||||
pageExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cleanPage === "") {
|
||||
// Empty page name, or local @anchor use
|
||||
pageExists = true;
|
||||
}
|
||||
|
||||
if (isCursorInRange(view.state, [from, to])) {
|
||||
// Only attach a CSS class, then get out
|
||||
if (!pageExists) {
|
||||
widgets.push(
|
||||
Decoration.mark({
|
||||
class: "sb-wiki-link-page-missing",
|
||||
}).range(from + 2, from + page.length + 2),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the whole thing
|
||||
if (isCursorInRange(state, [from, to])) {
|
||||
// Only attach a CSS class, then get out
|
||||
if (!pageExists) {
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
Decoration.mark({
|
||||
class: "sb-wiki-link-page-missing",
|
||||
}).range(from + 2, from + page.length + 2),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let linkText = alias || page;
|
||||
if (!pipePart && text.indexOf("/") !== -1) {
|
||||
// Let's use the last part of the path as the link text
|
||||
linkText = page.split("/").pop()!;
|
||||
}
|
||||
// Hide the whole thing
|
||||
widgets.push(
|
||||
invisibleDecoration.range(
|
||||
from,
|
||||
to,
|
||||
),
|
||||
);
|
||||
|
||||
// And replace it with a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new LinkWidget(
|
||||
{
|
||||
text: linkText,
|
||||
title: pageExists
|
||||
? `Navigate to ${page}`
|
||||
: `Create ${page}`,
|
||||
href: `/${page.replaceAll(" ", "_")}`,
|
||||
cssClass: pageExists
|
||||
? "sb-wiki-link-page"
|
||||
: "sb-wiki-link-page-missing",
|
||||
callback: (e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return view.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
let linkText = alias || page;
|
||||
if (!pipePart && text.indexOf("/") !== -1) {
|
||||
// Let's use the last part of the path as the link text
|
||||
linkText = page.split("/").pop()!;
|
||||
}
|
||||
|
||||
// And replace it with a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new LinkWidget(
|
||||
{
|
||||
text: linkText,
|
||||
title: pageExists ? `Navigate to ${page}` : `Create ${page}`,
|
||||
href: `/${page.replaceAll(" ", "_")}`,
|
||||
cssClass: pageExists
|
||||
? "sb-wiki-link-page"
|
||||
: "sb-wiki-link-page-missing",
|
||||
callback: (e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return editor.editorView!.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
// Dispatch click event to navigate there without moving the cursor
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
pos: from,
|
||||
};
|
||||
editor.dispatchAppEvent("page:click", clickEvent).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
}).range(from),
|
||||
);
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -734,7 +734,6 @@ export class Editor {
|
|||
const editorView = this.editorView!;
|
||||
if (pageState) {
|
||||
// Restore state
|
||||
// console.log("Restoring selection state", pageState);
|
||||
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
|
||||
editorView.dispatch({
|
||||
selection: pageState.selection,
|
||||
|
|
Loading…
Reference in New Issue