Support linking to and moving to line number in pages (#988)
Support linking to and moving to line number in pagespull/995/head
parent
86bee00e5e
commit
a0f3f7ef41
|
@ -2,7 +2,7 @@ import { FunctionMap, Query } from "$sb/types.ts";
|
||||||
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
||||||
import { System } from "$lib/plugos/system.ts";
|
import { System } from "$lib/plugos/system.ts";
|
||||||
import { LimitedMap } from "$lib/limited_map.ts";
|
import { LimitedMap } from "$lib/limited_map.ts";
|
||||||
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
import { parsePageRef, positionOfLine } from "$sb/lib/page_ref.ts";
|
||||||
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
import { parse } from "$common/markdown_parser/parse_tree.ts";
|
||||||
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
|
||||||
import { traverseTree } from "$sb/lib/tree.ts";
|
import { traverseTree } from "$sb/lib/tree.ts";
|
||||||
|
@ -86,7 +86,7 @@ export function buildQueryFunctions(
|
||||||
return renderToText(tree);
|
return renderToText(tree);
|
||||||
},
|
},
|
||||||
|
|
||||||
// INTERNAL: Used to implement resolving [[links]] in expressions, also supports [[link#header]] and [[link$pos]] as well as [[link$anchor]]
|
// INTERNAL: Used to implement resolving [[links]] in expressions, also supports [[link#header]] and [[link@pos]] as well as [[link$anchor]]
|
||||||
async readPage(name: string): Promise<string> {
|
async readPage(name: string): Promise<string> {
|
||||||
const cachedPage = pageCache.get(name);
|
const cachedPage = pageCache.get(name);
|
||||||
if (cachedPage) {
|
if (cachedPage) {
|
||||||
|
@ -100,6 +100,13 @@ export function buildQueryFunctions(
|
||||||
|
|
||||||
// Extract page section if pos, anchor, or header are included
|
// Extract page section if pos, anchor, or header are included
|
||||||
if (pageRef.pos) {
|
if (pageRef.pos) {
|
||||||
|
if (pageRef.pos instanceof Object) {
|
||||||
|
pageRef.pos = positionOfLine(
|
||||||
|
page,
|
||||||
|
pageRef.pos.line,
|
||||||
|
pageRef.pos.column,
|
||||||
|
);
|
||||||
|
}
|
||||||
// If the page link includes a position, slice the page from that position
|
// If the page link includes a position, slice the page from that position
|
||||||
page = page.slice(pageRef.pos);
|
page = page.slice(pageRef.pos);
|
||||||
} else if (pageRef.anchor) {
|
} else if (pageRef.anchor) {
|
||||||
|
|
|
@ -10,6 +10,18 @@ Deno.test("Page utility functions", () => {
|
||||||
assertEquals(parsePageRef("foo"), { page: "foo" });
|
assertEquals(parsePageRef("foo"), { page: "foo" });
|
||||||
assertEquals(parsePageRef("[[foo]]"), { page: "foo" });
|
assertEquals(parsePageRef("[[foo]]"), { page: "foo" });
|
||||||
assertEquals(parsePageRef("foo@1"), { page: "foo", pos: 1 });
|
assertEquals(parsePageRef("foo@1"), { page: "foo", pos: 1 });
|
||||||
|
assertEquals(parsePageRef("foo@L1"), {
|
||||||
|
page: "foo",
|
||||||
|
pos: { line: 1, column: 1 },
|
||||||
|
});
|
||||||
|
assertEquals(parsePageRef("foo@L2C3"), {
|
||||||
|
page: "foo",
|
||||||
|
pos: { line: 2, column: 3 },
|
||||||
|
});
|
||||||
|
assertEquals(parsePageRef("foo@l2c3"), {
|
||||||
|
page: "foo",
|
||||||
|
pos: { line: 2, column: 3 },
|
||||||
|
});
|
||||||
assertEquals(parsePageRef("foo$bar"), { page: "foo", anchor: "bar" });
|
assertEquals(parsePageRef("foo$bar"), { page: "foo", anchor: "bar" });
|
||||||
assertEquals(parsePageRef("foo#My header"), {
|
assertEquals(parsePageRef("foo#My header"), {
|
||||||
page: "foo",
|
page: "foo",
|
||||||
|
@ -31,6 +43,14 @@ Deno.test("Page utility functions", () => {
|
||||||
// Encoding
|
// Encoding
|
||||||
assertEquals(encodePageRef({ page: "foo" }), "foo");
|
assertEquals(encodePageRef({ page: "foo" }), "foo");
|
||||||
assertEquals(encodePageRef({ page: "foo", pos: 10 }), "foo@10");
|
assertEquals(encodePageRef({ page: "foo", pos: 10 }), "foo@10");
|
||||||
|
assertEquals(
|
||||||
|
encodePageRef({ page: "foo", pos: { line: 10, column: 1 } }),
|
||||||
|
"foo@L10",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
encodePageRef({ page: "foo", pos: { line: 10, column: 5 } }),
|
||||||
|
"foo@L10C5",
|
||||||
|
);
|
||||||
assertEquals(encodePageRef({ page: "foo", anchor: "bar" }), "foo$bar");
|
assertEquals(encodePageRef({ page: "foo", anchor: "bar" }), "foo$bar");
|
||||||
assertEquals(encodePageRef({ page: "foo", header: "bar" }), "foo#bar");
|
assertEquals(encodePageRef({ page: "foo", header: "bar" }), "foo#bar");
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,14 @@ export function validatePageName(name: string) {
|
||||||
|
|
||||||
export type PageRef = {
|
export type PageRef = {
|
||||||
page: string;
|
page: string;
|
||||||
pos?: number;
|
pos?: number | { line: number; column: number };
|
||||||
anchor?: string;
|
anchor?: string;
|
||||||
header?: string;
|
header?: string;
|
||||||
meta?: boolean;
|
meta?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const posRegex = /@(\d+)$/;
|
const posRegex = /@(\d+)$/;
|
||||||
// Should be kept in sync with the regex in index.plug.yaml
|
const linePosRegex = /@[Ll](\d+)(?:[Cc](\d+))?$/; // column is optional, implicit 1
|
||||||
const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/;
|
const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/;
|
||||||
const headerRegex = /#([^#]*)$/;
|
const headerRegex = /#([^#]*)$/;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export function parsePageRef(name: string): PageRef {
|
||||||
}
|
}
|
||||||
const pageRef: PageRef = { page: name };
|
const pageRef: PageRef = { page: name };
|
||||||
if (pageRef.page.startsWith("^")) {
|
if (pageRef.page.startsWith("^")) {
|
||||||
// A carrot prefix means we're looking for a meta page, but that doesn't matter for most use cases
|
// A caret prefix means we're looking for a meta page, but that doesn't matter for most use cases
|
||||||
pageRef.page = pageRef.page.slice(1);
|
pageRef.page = pageRef.page.slice(1);
|
||||||
pageRef.meta = true;
|
pageRef.meta = true;
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,15 @@ export function parsePageRef(name: string): PageRef {
|
||||||
pageRef.pos = parseInt(posMatch[1]);
|
pageRef.pos = parseInt(posMatch[1]);
|
||||||
pageRef.page = pageRef.page.replace(posRegex, "");
|
pageRef.page = pageRef.page.replace(posRegex, "");
|
||||||
}
|
}
|
||||||
|
const linePosMatch = pageRef.page.match(linePosRegex);
|
||||||
|
if (linePosMatch) {
|
||||||
|
pageRef.pos = { line: parseInt(linePosMatch[1]), column: 1 };
|
||||||
|
if (linePosMatch[2]) {
|
||||||
|
pageRef.pos.column = parseInt(linePosMatch[2]);
|
||||||
|
}
|
||||||
|
pageRef.page = pageRef.page.replace(linePosRegex, "");
|
||||||
|
}
|
||||||
|
|
||||||
const anchorMatch = pageRef.page.match(anchorRegex);
|
const anchorMatch = pageRef.page.match(anchorRegex);
|
||||||
if (anchorMatch) {
|
if (anchorMatch) {
|
||||||
pageRef.anchor = anchorMatch[1];
|
pageRef.anchor = anchorMatch[1];
|
||||||
|
@ -58,13 +67,21 @@ export function parsePageRef(name: string): PageRef {
|
||||||
pageRef.header = headerMatch[1];
|
pageRef.header = headerMatch[1];
|
||||||
pageRef.page = pageRef.page.replace(headerRegex, "");
|
pageRef.page = pageRef.page.replace(headerRegex, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageRef;
|
return pageRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodePageRef(pageRef: PageRef): string {
|
export function encodePageRef(pageRef: PageRef): string {
|
||||||
let name = pageRef.page;
|
let name = pageRef.page;
|
||||||
if (pageRef.pos) {
|
if (pageRef.pos) {
|
||||||
name += `@${pageRef.pos}`;
|
if (pageRef.pos instanceof Object) {
|
||||||
|
name += `@L${pageRef.pos.line}`;
|
||||||
|
if (pageRef.pos.column !== 1) {
|
||||||
|
name += `C${pageRef.pos.column}`;
|
||||||
|
}
|
||||||
|
} else { // just a number
|
||||||
|
name += `@${pageRef.pos}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pageRef.anchor) {
|
if (pageRef.anchor) {
|
||||||
name += `$${pageRef.anchor}`;
|
name += `$${pageRef.anchor}`;
|
||||||
|
@ -74,3 +91,26 @@ export function encodePageRef(pageRef: PageRef): string {
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate line and column number (counting from 1) to position in text (counting from 0)
|
||||||
|
*/
|
||||||
|
export function positionOfLine(
|
||||||
|
text: string,
|
||||||
|
line: number,
|
||||||
|
column: number,
|
||||||
|
): number {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let targetLine = "";
|
||||||
|
let targetPos = 0;
|
||||||
|
for (let i = 0; i < line && lines.length; i++) {
|
||||||
|
targetLine = lines[i];
|
||||||
|
targetPos += targetLine.length;
|
||||||
|
}
|
||||||
|
// How much to move inside the line, column number starts from 1
|
||||||
|
const columnOffset = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(targetLine.length, column - 1),
|
||||||
|
);
|
||||||
|
return targetPos - targetLine.length + columnOffset;
|
||||||
|
}
|
||||||
|
|
|
@ -141,6 +141,14 @@ export function moveCursor(pos: number, center = false): Promise<void> {
|
||||||
return syscall("editor.moveCursor", pos, center);
|
return syscall("editor.moveCursor", pos, center);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function moveCursorToLine(
|
||||||
|
line: number,
|
||||||
|
column = 1,
|
||||||
|
center = false,
|
||||||
|
): Promise<void> {
|
||||||
|
return syscall("editor.moveCursorToLine", line, column, center);
|
||||||
|
}
|
||||||
|
|
||||||
export function insertAtCursor(text: string): Promise<void> {
|
export function insertAtCursor(text: string): Promise<void> {
|
||||||
return syscall("editor.insertAtCursor", text);
|
return syscall("editor.insertAtCursor", text);
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,11 @@ functions:
|
||||||
moveToPos:
|
moveToPos:
|
||||||
path: "./editor.ts:moveToPosCommand"
|
path: "./editor.ts:moveToPosCommand"
|
||||||
command:
|
command:
|
||||||
name: "Navigate: Move Cursor to Position"
|
name: "Navigate: To Position"
|
||||||
|
moveToLine:
|
||||||
|
path: "./editor.ts:moveToLineCommand"
|
||||||
|
command:
|
||||||
|
name: "Navigate: To Line"
|
||||||
navigateToPage:
|
navigateToPage:
|
||||||
path: "./navigate.ts:navigateToPage"
|
path: "./navigate.ts:navigateToPage"
|
||||||
command:
|
command:
|
||||||
|
|
|
@ -44,7 +44,32 @@ export async function moveToPosCommand() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = +posString;
|
const pos = +posString;
|
||||||
await editor.moveCursor(pos);
|
await editor.moveCursor(pos, true); // showing the movement for better UX
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveToLineCommand() {
|
||||||
|
const lineString = await editor.prompt(
|
||||||
|
"Move to line (and optionally column):",
|
||||||
|
);
|
||||||
|
if (!lineString) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Match sequence of digits at the start, optionally another sequence
|
||||||
|
const numberRegex = /^(\d+)(?:[^\d]+(\d+))?/;
|
||||||
|
const match = lineString.match(numberRegex);
|
||||||
|
if (!match) {
|
||||||
|
await editor.flashNotification(
|
||||||
|
"Could not parse line number in prompt",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let column = 1;
|
||||||
|
const line = parseInt(match[1]);
|
||||||
|
if (match[2]) {
|
||||||
|
column = parseInt(match[2]);
|
||||||
|
}
|
||||||
|
await editor.moveCursorToLine(line, column, true); // showing the movement for better UX
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function customFlashMessage(_def: any, message: string) {
|
export async function customFlashMessage(_def: any, message: string) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { ObjectValue } from "../../plug-api/types.ts";
|
||||||
import { indexObjects, queryObjects } from "../index/plug_api.ts";
|
import { indexObjects, queryObjects } from "../index/plug_api.ts";
|
||||||
import { updateITags } from "$sb/lib/tags.ts";
|
import { updateITags } from "$sb/lib/tags.ts";
|
||||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
import { parsePageRef, positionOfLine } from "$sb/lib/page_ref.ts";
|
||||||
|
|
||||||
export type TaskObject = ObjectValue<
|
export type TaskObject = ObjectValue<
|
||||||
{
|
{
|
||||||
|
@ -219,10 +219,13 @@ export async function updateTaskState(
|
||||||
if (page === currentPage) {
|
if (page === currentPage) {
|
||||||
// In current page, just update the task marker with dispatch
|
// In current page, just update the task marker with dispatch
|
||||||
const editorText = await editor.getText();
|
const editorText = await editor.getText();
|
||||||
|
const targetPos = pos instanceof Object
|
||||||
|
? positionOfLine(editorText, pos.line, pos.column)
|
||||||
|
: pos;
|
||||||
// Check if the task state marker is still there
|
// Check if the task state marker is still there
|
||||||
const targetText = editorText.substring(
|
const targetText = editorText.substring(
|
||||||
pos + 1,
|
targetPos + 1,
|
||||||
pos + 1 + oldState.length,
|
targetPos + 1 + oldState.length,
|
||||||
);
|
);
|
||||||
if (targetText !== oldState) {
|
if (targetText !== oldState) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -233,8 +236,8 @@ export async function updateTaskState(
|
||||||
}
|
}
|
||||||
await editor.dispatch({
|
await editor.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: pos + 1,
|
from: targetPos + 1,
|
||||||
to: pos + 1 + oldState.length,
|
to: targetPos + 1 + oldState.length,
|
||||||
insert: newState,
|
insert: newState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -242,8 +245,11 @@ export async function updateTaskState(
|
||||||
let text = await space.readPage(page);
|
let text = await space.readPage(page);
|
||||||
|
|
||||||
const referenceMdTree = await markdown.parseMarkdown(text);
|
const referenceMdTree = await markdown.parseMarkdown(text);
|
||||||
|
const targetPos = pos instanceof Object
|
||||||
|
? positionOfLine(text, pos.line, pos.column)
|
||||||
|
: pos;
|
||||||
// Adding +1 to immediately hit the task state node
|
// Adding +1 to immediately hit the task state node
|
||||||
const taskStateNode = nodeAtPos(referenceMdTree, pos + 1);
|
const taskStateNode = nodeAtPos(referenceMdTree, targetPos + 1);
|
||||||
if (!taskStateNode || taskStateNode.type !== "TaskState") {
|
if (!taskStateNode || taskStateNode.type !== "TaskState") {
|
||||||
console.error(
|
console.error(
|
||||||
"Reference not a task marker, out of date?",
|
"Reference not a task marker, out of date?",
|
||||||
|
|
|
@ -358,7 +358,8 @@ export class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Was there a pos or anchor set?
|
// Was there a pos or anchor set?
|
||||||
let pos: number | undefined = pageState.pos;
|
let pos: number | { line: number; column: number } | undefined =
|
||||||
|
pageState.pos;
|
||||||
if (pageState.anchor) {
|
if (pageState.anchor) {
|
||||||
console.log("Navigating to anchor", pageState.anchor);
|
console.log("Navigating to anchor", pageState.anchor);
|
||||||
const pageText = this.editorView.state.sliceDoc();
|
const pageText = this.editorView.state.sliceDoc();
|
||||||
|
@ -404,6 +405,15 @@ export class Client {
|
||||||
adjustedPosition = true;
|
adjustedPosition = true;
|
||||||
}
|
}
|
||||||
if (pos !== undefined) {
|
if (pos !== undefined) {
|
||||||
|
// Translate line and column number to position in text
|
||||||
|
if (pos instanceof Object) {
|
||||||
|
// CodeMirror already keeps information about lines
|
||||||
|
const cmLine = this.editorView.state.doc.line(pos.line);
|
||||||
|
// How much to move inside the line, column number starts from 1
|
||||||
|
const offset = Math.max(0, Math.min(cmLine.length, pos.column - 1));
|
||||||
|
pos = cmLine.from + offset;
|
||||||
|
}
|
||||||
|
|
||||||
this.editorView.dispatch({
|
this.editorView.dispatch({
|
||||||
selection: { anchor: pos! },
|
selection: { anchor: pos! },
|
||||||
effects: EditorView.scrollIntoView(pos!, {
|
effects: EditorView.scrollIntoView(pos!, {
|
||||||
|
|
|
@ -214,6 +214,19 @@ export function editorSyscalls(client: Client): SysCallMapping {
|
||||||
}
|
}
|
||||||
client.editorView.focus();
|
client.editorView.focus();
|
||||||
},
|
},
|
||||||
|
"editor.moveCursorToLine": (
|
||||||
|
_ctx,
|
||||||
|
line: number,
|
||||||
|
column = 1,
|
||||||
|
center = false,
|
||||||
|
) => {
|
||||||
|
// CodeMirror already keeps information about lines
|
||||||
|
const cmLine = client.editorView.state.doc.line(line);
|
||||||
|
// How much to move inside the line, column number starts from 1
|
||||||
|
const offset = Math.max(0, Math.min(cmLine.length, column - 1));
|
||||||
|
// Just reuse the implementation above
|
||||||
|
syscalls["editor.moveCursor"](_ctx, cmLine.from + offset, center);
|
||||||
|
},
|
||||||
"editor.setSelection": (_ctx, from: number, to: number) => {
|
"editor.setSelection": (_ctx, from: number, to: number) => {
|
||||||
client.editorView.dispatch({
|
client.editorView.dispatch({
|
||||||
selection: {
|
selection: {
|
||||||
|
@ -263,7 +276,10 @@ export function editorSyscalls(client: Client): SysCallMapping {
|
||||||
const cm = vimGetCm(client.editorView)!;
|
const cm = vimGetCm(client.editorView)!;
|
||||||
return Vim.handleEx(cm, exCommand);
|
return Vim.handleEx(cm, exCommand);
|
||||||
},
|
},
|
||||||
"editor.openPageNavigator": (_ctx, mode: "page" | "meta" | "all" = "page") => {
|
"editor.openPageNavigator": (
|
||||||
|
_ctx,
|
||||||
|
mode: "page" | "meta" | "all" = "page",
|
||||||
|
) => {
|
||||||
client.startPageNavigate(mode);
|
client.startPageNavigate(mode);
|
||||||
},
|
},
|
||||||
"editor.openCommandPalette": () => {
|
"editor.openCommandPalette": () => {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
An attempt at documenting the changes/new features introduced in each
|
An attempt at documenting the changes/new features introduced in each release.
|
||||||
release.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ Internal links can have various formats:
|
||||||
* `[[CHANGELOG|The Change Log]]`: a link with an alias that appears like this: [[CHANGELOG|The Change Log]].
|
* `[[CHANGELOG|The Change Log]]`: a link with an alias that appears like this: [[CHANGELOG|The Change Log]].
|
||||||
* `[[CHANGELOG$edge]]`: a link referencing a particular [[Markdown/Anchors|anchor]]: [[CHANGELOG$edge]]. When the page name is omitted, the anchor is expected to be local to the current page.
|
* `[[CHANGELOG$edge]]`: a link referencing a particular [[Markdown/Anchors|anchor]]: [[CHANGELOG$edge]]. When the page name is omitted, the anchor is expected to be local to the current page.
|
||||||
* `[[CHANGELOG#Edge]]`: a link referencing a particular header: [[CHANGELOG#Edge]]. When the page name is omitted, the header is expected to be local to the current page.
|
* `[[CHANGELOG#Edge]]`: a link referencing a particular header: [[CHANGELOG#Edge]]. When the page name is omitted, the header is expected to be local to the current page.
|
||||||
* `[[CHANGELOG@1234]]`: a link referencing a particular position in a page (characters from the start of the document). This notation is generally automatically generated through templates.
|
* `[[CHANGELOG@...]]`: a link referencing a particular position in a page. This notation is generally automatically generated through templates.
|
||||||
|
* `[[CHANGELOG@1234]]`: character in text (starting from 0): [[CHANGELOG@1234]]
|
||||||
|
* `[[CHANGELOG@L3]]`: line of text (starting from 1): [[CHANGELOG@L3]]. When column number is omitted it is assumed to be start of line. This starts from one to match the convention widely used in other text editors.
|
||||||
|
* `[[CHANGELOG@L1C3]]`: line and column: [[CHANGELOG@L1C3]]. This also starts from 1 for the start of line. The cursor will be placed at the end of line if the passed number is larger than the line
|
||||||
|
|
||||||
# Caret page links
|
# Caret page links
|
||||||
[[Meta Pages]] are excluded from link auto complete in many contexts. However, you may still want to reference a meta page outside of a “meta context.” To make it easier to reference, you can use the caret syntax: `[[^SETTINGS]]`. Semantically this has the same meaning as `[[SETTINGS]]`. The only difference is that auto complete will _only_ complete meta pages.
|
[[Meta Pages]] are excluded from link auto complete in many contexts. However, you may still want to reference a meta page outside of a “meta context.” To make it easier to reference, you can use the caret syntax: `[[^SETTINGS]]`. Semantically this has the same meaning as `[[SETTINGS]]`. The only difference is that auto complete will _only_ complete meta pages.
|
||||||
|
|
|
@ -20,6 +20,7 @@ The `editor` plug implements foundational editor functionality for SilverBullet.
|
||||||
* {[Navigate: To This Page]}: navigate to the page under the cursor
|
* {[Navigate: To This Page]}: navigate to the page under the cursor
|
||||||
* {[Navigate: Center Cursor]}: center the cursor at the center of the screen
|
* {[Navigate: Center Cursor]}: center the cursor at the center of the screen
|
||||||
* {[Navigate: Move Cursor to Position]}: move cursor to a specific (numeric) cursor position (# of characters from the start of the document)
|
* {[Navigate: Move Cursor to Position]}: move cursor to a specific (numeric) cursor position (# of characters from the start of the document)
|
||||||
|
* {[Navigate: Move Cursor to Line]}: move cursor to a specific line, counting from 1; write two numbers (separated by any non-digit) to also move to a column, counting from 1.
|
||||||
|
|
||||||
## Text editing
|
## Text editing
|
||||||
* {[Text: Quote Selection]}: turns the selection into a blockquote (`>` prefix)
|
* {[Text: Quote Selection]}: turns the selection into a blockquote (`>` prefix)
|
||||||
|
|
Loading…
Reference in New Issue