2024-07-31 17:28:31 +08:00
|
|
|
import type {
|
|
|
|
CompletionContext,
|
|
|
|
CompletionResult,
|
|
|
|
} from "@codemirror/autocomplete";
|
2024-03-16 22:29:24 +08:00
|
|
|
import type { Compartment } from "@codemirror/state";
|
|
|
|
import { EditorView } from "@codemirror/view";
|
|
|
|
import { syntaxTree } from "@codemirror/language";
|
|
|
|
import { compile as gitIgnoreCompiler } from "gitignore-parser";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { SyntaxNode } from "@lezer/common";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { Space } from "../common/space.ts";
|
2024-08-07 02:11:38 +08:00
|
|
|
import type { FilterOption } from "@silverbulletmd/silverbullet/type/client";
|
2024-02-28 03:05:12 +08:00
|
|
|
import { EventHook } from "../common/hooks/event.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { AppCommand } from "$lib/command.ts";
|
2024-01-24 21:44:39 +08:00
|
|
|
import {
|
2024-07-30 23:33:33 +08:00
|
|
|
type PageState,
|
2024-01-24 21:44:39 +08:00
|
|
|
parsePageRefFromURI,
|
|
|
|
PathPageNavigator,
|
|
|
|
} from "./navigator.ts";
|
2023-07-14 19:58:16 +08:00
|
|
|
|
2024-08-07 02:11:38 +08:00
|
|
|
import type { AppViewState } from "./type.ts";
|
2022-11-18 23:04:37 +08:00
|
|
|
|
2024-02-29 22:23:05 +08:00
|
|
|
import type {
|
|
|
|
AppEvent,
|
|
|
|
CompleteEvent,
|
|
|
|
SlashCompletions,
|
|
|
|
} from "../plug-api/types.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { StyleObject } from "../plugs/index/style.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { throttle } from "$lib/async.ts";
|
|
|
|
import { PlugSpacePrimitives } from "$common/spaces/plug_space_primitives.ts";
|
|
|
|
import { EventedSpacePrimitives } from "$common/spaces/evented_space_primitives.ts";
|
2023-08-30 03:17:29 +08:00
|
|
|
import {
|
2024-07-30 23:33:33 +08:00
|
|
|
type ISyncService,
|
2023-08-30 03:17:29 +08:00
|
|
|
NoSyncSyncService,
|
|
|
|
pageSyncInterval,
|
|
|
|
SyncService,
|
|
|
|
} from "./sync_service.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { simpleHash } from "$lib/crypto.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { SyncStatus } from "$common/spaces/sync.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { HttpSpacePrimitives } from "$common/spaces/http_space_primitives.ts";
|
|
|
|
import { FallbackSpacePrimitives } from "$common/spaces/fallback_space_primitives.ts";
|
|
|
|
import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitives.ts";
|
2024-08-07 02:11:38 +08:00
|
|
|
import {
|
|
|
|
encodePageRef,
|
2024-08-20 15:38:56 +08:00
|
|
|
encodePageURI,
|
2024-08-07 02:11:38 +08:00
|
|
|
validatePageName,
|
|
|
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
2023-07-14 19:44:30 +08:00
|
|
|
import { ClientSystem } from "./client_system.ts";
|
2023-07-14 19:58:16 +08:00
|
|
|
import { createEditorState } from "./editor_state.ts";
|
2023-07-14 20:22:26 +08:00
|
|
|
import { MainUI } from "./editor_ui.tsx";
|
2024-08-07 02:11:38 +08:00
|
|
|
import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
2024-07-31 17:28:31 +08:00
|
|
|
import type {
|
|
|
|
CodeWidgetButton,
|
|
|
|
FileMeta,
|
|
|
|
PageMeta,
|
|
|
|
} from "../plug-api/types.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { DataStore } from "$lib/data/datastore.ts";
|
|
|
|
import { IndexedDBKvPrimitives } from "$lib/data/indexeddb_kv_primitives.ts";
|
|
|
|
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
|
|
|
import { DataStoreSpacePrimitives } from "$common/spaces/datastore_space_primitives.ts";
|
2024-01-15 23:43:12 +08:00
|
|
|
|
2024-02-28 23:22:28 +08:00
|
|
|
import { ensureSpaceIndex } from "$common/space_index.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { renderTheTemplate } from "$common/syscalls/template.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { PageRef } from "../plug-api/lib/page_ref.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
2024-07-30 03:21:16 +08:00
|
|
|
import {
|
|
|
|
ensureAndLoadSettingsAndIndex,
|
|
|
|
updateObjectDecorators,
|
2024-08-02 22:47:36 +08:00
|
|
|
} from "../common/config.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { LimitedMap } from "$lib/limited_map.ts";
|
2024-05-28 02:33:41 +08:00
|
|
|
import { plugPrefix } from "$common/spaces/constants.ts";
|
2024-07-07 02:29:38 +08:00
|
|
|
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
|
2024-08-07 02:11:38 +08:00
|
|
|
import { findNodeMatching } from "@silverbulletmd/silverbullet/lib/tree";
|
2024-07-25 21:18:58 +08:00
|
|
|
import type { LinkObject } from "../plugs/index/page_links.ts";
|
2024-08-15 22:39:06 +08:00
|
|
|
import type { Config, ConfigContainer } from "../type/config.ts";
|
2024-08-22 03:36:50 +08:00
|
|
|
import { editor } from "@silverbulletmd/silverbullet/syscalls";
|
2024-05-28 02:33:41 +08:00
|
|
|
|
2023-07-14 19:44:30 +08:00
|
|
|
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
2022-03-20 16:56:28 +08:00
|
|
|
|
2023-07-14 19:44:30 +08:00
|
|
|
const autoSaveInterval = 1000;
|
2022-03-31 20:28:07 +08:00
|
|
|
|
2023-05-24 02:53:53 +08:00
|
|
|
declare global {
|
2024-07-30 21:17:34 +08:00
|
|
|
// deno-lint-ignore no-var
|
|
|
|
var silverBulletConfig: {
|
|
|
|
spaceFolderPath: string;
|
|
|
|
syncOnly: boolean;
|
|
|
|
readOnly: boolean;
|
|
|
|
enableSpaceScript: boolean;
|
|
|
|
};
|
|
|
|
// deno-lint-ignore no-var
|
|
|
|
var client: Client;
|
2023-05-24 02:53:53 +08:00
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
type WidgetCacheItem = {
|
|
|
|
height: number;
|
|
|
|
html: string;
|
|
|
|
buttons?: CodeWidgetButton[];
|
|
|
|
banner?: string;
|
|
|
|
};
|
2024-01-24 18:58:33 +08:00
|
|
|
|
2024-08-02 22:47:36 +08:00
|
|
|
export class Client implements ConfigContainer {
|
2024-07-26 02:29:13 +08:00
|
|
|
// Event bus used to communicate between components
|
|
|
|
eventHook = new EventHook();
|
|
|
|
|
|
|
|
space!: Space;
|
2024-08-02 22:47:36 +08:00
|
|
|
config!: Config;
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2024-02-07 21:50:01 +08:00
|
|
|
clientSystem!: ClientSystem;
|
2024-07-26 02:29:13 +08:00
|
|
|
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
|
|
|
|
httpSpacePrimitives!: HttpSpacePrimitives;
|
|
|
|
|
|
|
|
ui!: MainUI;
|
|
|
|
stateDataStore!: DataStore;
|
|
|
|
spaceKV?: KvPrimitives;
|
|
|
|
mq!: DataStoreMQ;
|
|
|
|
|
|
|
|
// CodeMirror editor
|
2023-10-03 20:16:33 +08:00
|
|
|
editorView!: EditorView;
|
2024-01-21 02:16:07 +08:00
|
|
|
keyHandlerCompartment?: Compartment;
|
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
private pageNavigator!: PathPageNavigator;
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
private dbPrefix: string;
|
2022-12-22 23:20:05 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
saveTimeout?: number;
|
2022-04-04 21:25:07 +08:00
|
|
|
debouncedUpdateEvent = throttle(() => {
|
2022-04-19 22:54:47 +08:00
|
|
|
this.eventHook
|
|
|
|
.dispatchEvent("editor:updated")
|
|
|
|
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
2022-04-04 21:25:07 +08:00
|
|
|
}, 1000);
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Sync related stuff
|
2023-05-24 02:53:53 +08:00
|
|
|
// Track if plugs have been updated since sync cycle
|
|
|
|
fullSyncCompleted = false;
|
2023-10-03 20:16:33 +08:00
|
|
|
syncService!: ISyncService;
|
2022-03-20 16:56:28 +08:00
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
private onLoadPageRef: PageRef;
|
2023-12-22 22:55:50 +08:00
|
|
|
|
2022-08-02 18:43:39 +08:00
|
|
|
constructor(
|
2023-10-03 20:16:33 +08:00
|
|
|
private parent: Element,
|
2024-01-27 00:05:10 +08:00
|
|
|
public syncMode: boolean,
|
|
|
|
private readOnlyMode: boolean,
|
2022-08-02 18:43:39 +08:00
|
|
|
) {
|
2023-09-01 22:57:29 +08:00
|
|
|
if (!syncMode) {
|
|
|
|
this.fullSyncCompleted = true;
|
|
|
|
}
|
2023-05-24 02:53:53 +08:00
|
|
|
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
|
2023-07-27 17:41:44 +08:00
|
|
|
this.dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
2024-01-24 21:44:39 +08:00
|
|
|
this.onLoadPageRef = parsePageRefFromURI();
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
/**
|
|
|
|
* Initialize the client
|
|
|
|
* This is a separated from the constructor to allow for async initialization
|
|
|
|
*/
|
|
|
|
async init() {
|
2024-07-26 02:29:13 +08:00
|
|
|
// Setup the state data store
|
2023-10-03 20:16:33 +08:00
|
|
|
const stateKvPrimitives = new IndexedDBKvPrimitives(
|
|
|
|
`${this.dbPrefix}_state`,
|
2023-05-24 02:53:53 +08:00
|
|
|
);
|
2023-10-03 20:16:33 +08:00
|
|
|
await stateKvPrimitives.init();
|
2024-02-03 02:19:07 +08:00
|
|
|
this.stateDataStore = new DataStore(stateKvPrimitives);
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
// Setup message queue
|
|
|
|
this.mq = new DataStoreMQ(this.stateDataStore);
|
2023-08-11 00:32:41 +08:00
|
|
|
|
2023-07-14 19:44:30 +08:00
|
|
|
// Instantiate a PlugOS system
|
2024-02-07 21:50:01 +08:00
|
|
|
this.clientSystem = new ClientSystem(
|
2023-07-14 19:44:30 +08:00
|
|
|
this,
|
2023-08-11 00:32:41 +08:00
|
|
|
this.mq,
|
2023-10-03 20:16:33 +08:00
|
|
|
this.stateDataStore,
|
2023-07-14 19:44:30 +08:00
|
|
|
this.eventHook,
|
2024-07-05 03:22:37 +08:00
|
|
|
this.readOnlyMode,
|
2023-07-14 19:44:30 +08:00
|
|
|
);
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
const localSpacePrimitives = await this.initSpace();
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-08-30 03:17:29 +08:00
|
|
|
this.syncService = this.syncMode
|
|
|
|
? new SyncService(
|
|
|
|
localSpacePrimitives,
|
|
|
|
this.plugSpaceRemotePrimitives,
|
2023-10-03 20:16:33 +08:00
|
|
|
this.stateDataStore,
|
2023-08-30 03:17:29 +08:00
|
|
|
this.eventHook,
|
2024-07-26 02:29:13 +08:00
|
|
|
(path) => { // isSyncCandidate
|
|
|
|
// Exclude all plug space primitives paths
|
|
|
|
return !this.plugSpaceRemotePrimitives.isLikelyHandled(path) ||
|
2023-08-30 03:17:29 +08:00
|
|
|
// Except federated ones
|
|
|
|
path.startsWith("!");
|
|
|
|
},
|
|
|
|
)
|
|
|
|
: new NoSyncSyncService(this.space);
|
2022-03-29 17:21:32 +08:00
|
|
|
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui = new MainUI(this);
|
2023-10-03 20:16:33 +08:00
|
|
|
this.ui.render(this.parent);
|
2022-10-26 00:50:07 +08:00
|
|
|
|
2022-03-20 16:56:28 +08:00
|
|
|
this.editorView = new EditorView({
|
2023-07-14 19:58:16 +08:00
|
|
|
state: createEditorState(this, "", "", false),
|
2022-07-22 19:44:28 +08:00
|
|
|
parent: document.getElementById("sb-editor")!,
|
2022-03-20 16:56:28 +08:00
|
|
|
});
|
2022-12-21 21:55:24 +08:00
|
|
|
|
2022-03-20 16:56:28 +08:00
|
|
|
this.focus();
|
|
|
|
|
2024-02-07 21:50:01 +08:00
|
|
|
this.clientSystem.init();
|
2022-11-24 19:04:00 +08:00
|
|
|
|
2023-12-28 23:14:30 +08:00
|
|
|
await this.loadCaches();
|
2024-07-26 02:29:13 +08:00
|
|
|
|
|
|
|
// Let's ping the remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
2023-07-25 16:51:44 +08:00
|
|
|
try {
|
2023-12-17 18:46:18 +08:00
|
|
|
await this.httpSpacePrimitives.ping();
|
2023-07-27 18:37:39 +08:00
|
|
|
} catch (e: any) {
|
|
|
|
if (e.message === "Not authenticated") {
|
|
|
|
console.warn("Not authenticated, redirecting to auth page");
|
|
|
|
return;
|
|
|
|
}
|
2023-11-25 21:30:31 +08:00
|
|
|
if (e.message.includes("Offline") && !this.syncMode) {
|
|
|
|
// Offline and not in sync mode, this is not going to fly.
|
|
|
|
this.flashNotification(
|
|
|
|
"Could not reach remote server, going to reload in a few seconds",
|
|
|
|
"error",
|
|
|
|
);
|
|
|
|
setTimeout(() => {
|
|
|
|
location.reload();
|
|
|
|
}, 5000);
|
|
|
|
throw e;
|
|
|
|
}
|
2023-07-27 17:41:44 +08:00
|
|
|
console.warn(
|
2023-07-27 18:37:39 +08:00
|
|
|
"Could not reach remote server, we're offline or the server is down",
|
|
|
|
e,
|
2023-07-25 16:51:44 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-02 22:47:36 +08:00
|
|
|
// Load plugs
|
2023-07-27 21:25:33 +08:00
|
|
|
await this.loadPlugs();
|
2024-08-02 22:47:36 +08:00
|
|
|
|
|
|
|
// Load config (after the plugs, specifically the 'index' plug is loaded)
|
|
|
|
await this.loadConfig();
|
|
|
|
|
2024-02-07 21:50:01 +08:00
|
|
|
await this.clientSystem.loadSpaceScripts();
|
2024-02-06 23:51:04 +08:00
|
|
|
|
2024-02-03 02:19:07 +08:00
|
|
|
await this.initNavigator();
|
2024-01-11 20:20:50 +08:00
|
|
|
await this.initSync();
|
2022-11-24 19:04:00 +08:00
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// We can load custom styles async
|
2023-07-27 17:41:44 +08:00
|
|
|
this.loadCustomStyles().catch(console.error);
|
|
|
|
|
2023-07-27 21:25:33 +08:00
|
|
|
await this.dispatchAppEvent("editor:init");
|
2023-08-08 02:42:52 +08:00
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Regularly sync the currently open file
|
2023-08-08 02:42:52 +08:00
|
|
|
setInterval(() => {
|
2023-08-10 00:07:01 +08:00
|
|
|
try {
|
2024-01-24 21:44:39 +08:00
|
|
|
this.syncService.syncFile(`${this.currentPage}.md`).catch((e: any) => {
|
2023-08-10 00:07:01 +08:00
|
|
|
console.error("Interval sync error", e);
|
|
|
|
});
|
|
|
|
} catch (e: any) {
|
2023-08-09 23:22:42 +08:00
|
|
|
console.error("Interval sync error", e);
|
2023-08-10 00:07:01 +08:00
|
|
|
}
|
2023-08-08 02:42:52 +08:00
|
|
|
}, pageSyncInterval);
|
2023-12-22 22:55:50 +08:00
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Let's update the local page list cache asynchronously
|
2023-12-22 22:55:50 +08:00
|
|
|
this.updatePageListCache().catch(console.error);
|
2023-07-27 21:25:33 +08:00
|
|
|
}
|
|
|
|
|
2024-08-02 22:47:36 +08:00
|
|
|
async loadConfig() {
|
|
|
|
this.config = await ensureAndLoadSettingsAndIndex(
|
2024-01-25 18:42:36 +08:00
|
|
|
this.space.spacePrimitives,
|
2024-08-02 22:47:36 +08:00
|
|
|
this.clientSystem.system,
|
2024-01-25 18:42:36 +08:00
|
|
|
);
|
2024-08-02 22:47:36 +08:00
|
|
|
updateObjectDecorators(this.config, this.stateDataStore);
|
2024-01-25 18:42:36 +08:00
|
|
|
this.ui.viewDispatch({
|
2024-08-02 22:47:36 +08:00
|
|
|
type: "config-loaded",
|
|
|
|
config: this.config,
|
2024-01-25 18:42:36 +08:00
|
|
|
});
|
2024-07-15 01:57:06 +08:00
|
|
|
this.clientSystem.slashCommandHook.buildAllCommands(
|
|
|
|
this.clientSystem.system,
|
|
|
|
);
|
2024-08-04 02:59:53 +08:00
|
|
|
this.eventHook.dispatchEvent("config:loaded", this.config);
|
2024-01-21 02:16:07 +08:00
|
|
|
}
|
|
|
|
|
2024-01-11 20:20:50 +08:00
|
|
|
private async initSync() {
|
2023-07-27 17:41:44 +08:00
|
|
|
this.syncService.start();
|
|
|
|
|
2024-01-11 20:20:50 +08:00
|
|
|
// We're still booting, if a initial sync has already been completed we know this is the initial sync
|
2024-03-12 03:56:29 +08:00
|
|
|
let initialSync = !await this.syncService.hasInitialSyncCompleted();
|
2024-01-11 20:20:50 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.eventHook.addLocalListener("sync:success", async (operations) => {
|
|
|
|
if (operations > 0) {
|
|
|
|
// Update the page list
|
2023-12-22 22:55:50 +08:00
|
|
|
await this.space.updatePageList();
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
|
|
|
if (operations !== undefined) {
|
|
|
|
// "sync:success" is called with a number of operations only from syncSpace(), not from syncing individual pages
|
|
|
|
this.fullSyncCompleted = true;
|
2024-01-11 20:20:50 +08:00
|
|
|
|
|
|
|
console.log("Full sync completed");
|
|
|
|
|
|
|
|
// A full sync just completed
|
|
|
|
if (!initialSync) {
|
|
|
|
// If this was NOT the initial sync let's check if we need to perform a space reindex
|
2024-02-07 21:50:01 +08:00
|
|
|
ensureSpaceIndex(this.stateDataStore, this.clientSystem.system).catch(
|
2024-01-11 20:20:50 +08:00
|
|
|
console.error,
|
|
|
|
);
|
2024-03-12 03:56:29 +08:00
|
|
|
} else { // initialSync
|
2024-07-26 02:29:13 +08:00
|
|
|
// Let's load space scripts again, which probably weren't loaded before
|
2024-02-07 21:50:01 +08:00
|
|
|
await this.clientSystem.loadSpaceScripts();
|
2024-02-28 17:55:25 +08:00
|
|
|
console.log(
|
|
|
|
"Initial sync completed, now need to do a full space index to ensure all pages are indexed using any custom space script indexers",
|
|
|
|
);
|
|
|
|
ensureSpaceIndex(this.stateDataStore, this.clientSystem.system).catch(
|
|
|
|
console.error,
|
|
|
|
);
|
2024-03-12 03:56:29 +08:00
|
|
|
initialSync = false;
|
2024-01-11 20:20:50 +08:00
|
|
|
}
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
2023-10-03 20:16:33 +08:00
|
|
|
if (operations) {
|
|
|
|
// Likely initial sync so let's show visually that we're synced now
|
|
|
|
this.showProgress(100);
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
|
|
|
|
2023-08-16 17:40:31 +08:00
|
|
|
this.ui.viewDispatch({ type: "sync-change", syncSuccess: true });
|
2023-07-27 17:41:44 +08:00
|
|
|
});
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.eventHook.addLocalListener("sync:error", (_name) => {
|
2023-08-16 17:40:31 +08:00
|
|
|
this.ui.viewDispatch({ type: "sync-change", syncSuccess: false });
|
2023-07-27 17:41:44 +08:00
|
|
|
});
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.eventHook.addLocalListener("sync:conflict", (name) => {
|
|
|
|
this.flashNotification(
|
|
|
|
`Sync: conflict detected for ${name} - conflict copy created`,
|
|
|
|
"error",
|
|
|
|
);
|
|
|
|
});
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.eventHook.addLocalListener("sync:progress", (status: SyncStatus) => {
|
|
|
|
this.showProgress(
|
|
|
|
Math.round(status.filesProcessed / status.totalFiles * 100),
|
|
|
|
);
|
|
|
|
});
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2023-08-30 03:17:29 +08:00
|
|
|
this.eventHook.addLocalListener(
|
|
|
|
"file:synced",
|
|
|
|
(meta: FileMeta, direction: string) => {
|
2023-08-08 02:42:52 +08:00
|
|
|
if (meta.name.endsWith(".md") && direction === "secondary->primary") {
|
|
|
|
// We likely polled the currently open page which trigggered a local update, let's update the editor accordingly
|
|
|
|
this.space.getPageMeta(meta.name.slice(0, -3));
|
|
|
|
}
|
|
|
|
},
|
2023-08-30 03:17:29 +08:00
|
|
|
);
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
|
|
|
|
2024-01-24 18:58:33 +08:00
|
|
|
private navigateWithinPage(pageState: PageState) {
|
|
|
|
// Did we end up doing anything in terms of internal navigation?
|
|
|
|
let adjustedPosition = false;
|
|
|
|
|
|
|
|
// Was a particular scroll position persisted?
|
2024-01-24 21:44:39 +08:00
|
|
|
if (
|
|
|
|
pageState.scrollTop !== undefined &&
|
|
|
|
!(pageState.scrollTop === 0 &&
|
2024-01-25 21:51:40 +08:00
|
|
|
(pageState.pos !== undefined || pageState.anchor !== undefined ||
|
|
|
|
pageState.header !== undefined))
|
2024-01-24 21:44:39 +08:00
|
|
|
) {
|
2024-01-24 18:58:33 +08:00
|
|
|
setTimeout(() => {
|
|
|
|
this.editorView.scrollDOM.scrollTop = pageState.scrollTop!;
|
|
|
|
});
|
|
|
|
adjustedPosition = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Was a particular cursor/selection set?
|
2024-01-25 21:51:40 +08:00
|
|
|
if (
|
|
|
|
pageState.selection?.anchor && !pageState.pos && !pageState.anchor &&
|
|
|
|
!pageState.header
|
|
|
|
) { // Only do this if we got a specific cursor position
|
2024-01-24 18:58:33 +08:00
|
|
|
console.log("Changing cursor position to", pageState.selection);
|
|
|
|
this.editorView.dispatch({
|
|
|
|
selection: pageState.selection,
|
|
|
|
});
|
|
|
|
adjustedPosition = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Was there a pos or anchor set?
|
2024-07-29 02:31:37 +08:00
|
|
|
let pos: number | { line: number; column: number } | undefined =
|
|
|
|
pageState.pos;
|
2024-01-24 18:58:33 +08:00
|
|
|
if (pageState.anchor) {
|
|
|
|
console.log("Navigating to anchor", pageState.anchor);
|
|
|
|
const pageText = this.editorView.state.sliceDoc();
|
|
|
|
|
2024-07-07 02:29:38 +08:00
|
|
|
const sTree = syntaxTree(this.editorView.state);
|
|
|
|
const tree = lezerToParseTree(pageText, sTree.topNode);
|
2024-01-24 18:58:33 +08:00
|
|
|
|
2024-07-07 02:29:38 +08:00
|
|
|
const foundNode = findNodeMatching(tree, (node) => {
|
|
|
|
if (
|
|
|
|
node.type === "NamedAnchor" &&
|
|
|
|
node.children![0].text === `$${pageState.anchor}`
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!foundNode) {
|
2024-01-24 18:58:33 +08:00
|
|
|
return this.flashNotification(
|
|
|
|
`Could not find anchor $${pageState.anchor}`,
|
|
|
|
"error",
|
|
|
|
);
|
2024-07-07 02:29:38 +08:00
|
|
|
} else {
|
|
|
|
pos = foundNode.from;
|
2024-01-24 18:58:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
adjustedPosition = true;
|
|
|
|
}
|
2024-01-25 21:51:40 +08:00
|
|
|
if (pageState.header) {
|
|
|
|
console.log("Navigating to header", pageState.header);
|
|
|
|
const pageText = this.editorView.state.sliceDoc();
|
|
|
|
|
|
|
|
// This is somewhat of a simplistic way to find the header, but it works for now
|
|
|
|
pos = pageText.indexOf(`# ${pageState.header}\n`) + 2;
|
|
|
|
|
|
|
|
if (pos === -1) {
|
|
|
|
return this.flashNotification(
|
|
|
|
`Could not find header "${pageState.header}"`,
|
|
|
|
"error",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
adjustedPosition = true;
|
|
|
|
}
|
2024-01-24 18:58:33 +08:00
|
|
|
if (pos !== undefined) {
|
2024-07-29 02:31:37 +08:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2024-01-24 18:58:33 +08:00
|
|
|
this.editorView.dispatch({
|
|
|
|
selection: { anchor: pos! },
|
|
|
|
effects: EditorView.scrollIntoView(pos!, {
|
|
|
|
y: "start",
|
|
|
|
yMargin: 5,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
adjustedPosition = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not: just put the cursor at the top of the page, right after the frontmatter
|
|
|
|
if (!adjustedPosition) {
|
|
|
|
// Somewhat ad-hoc way to determine if the document contains frontmatter and if so, putting the cursor _after it_.
|
|
|
|
const pageText = this.editorView.state.sliceDoc();
|
|
|
|
|
|
|
|
// Default the cursor to be at position 0
|
|
|
|
let initialCursorPos = 0;
|
|
|
|
const match = frontMatterRegex.exec(pageText);
|
|
|
|
if (match) {
|
|
|
|
// Frontmatter found, put cursor after it
|
|
|
|
initialCursorPos = match[0].length;
|
|
|
|
}
|
|
|
|
// By default scroll to the top
|
|
|
|
this.editorView.scrollDOM.scrollTop = 0;
|
|
|
|
this.editorView.dispatch({
|
|
|
|
selection: { anchor: initialCursorPos },
|
|
|
|
// And then scroll down if required
|
|
|
|
scrollIntoView: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-03 02:19:07 +08:00
|
|
|
private async initNavigator() {
|
2024-01-24 18:58:33 +08:00
|
|
|
this.pageNavigator = new PathPageNavigator(this);
|
2023-07-27 17:41:44 +08:00
|
|
|
|
2024-02-03 02:19:07 +08:00
|
|
|
await this.pageNavigator.init();
|
|
|
|
|
2024-01-24 18:58:33 +08:00
|
|
|
this.pageNavigator.subscribe(async (pageState) => {
|
|
|
|
console.log("Now navigating to", pageState);
|
|
|
|
|
|
|
|
await this.loadPage(pageState.page);
|
|
|
|
|
|
|
|
// Setup scroll position, cursor position, etc
|
|
|
|
this.navigateWithinPage(pageState);
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Persist this page as the last opened page, we'll use this for cold start PWA loads
|
2024-01-24 18:58:33 +08:00
|
|
|
await this.stateDataStore.set(
|
|
|
|
["client", "lastOpenedPage"],
|
|
|
|
pageState.page,
|
|
|
|
);
|
|
|
|
});
|
2023-08-16 17:40:31 +08:00
|
|
|
|
2024-08-02 22:47:36 +08:00
|
|
|
if (location.hash === "#boot" && this.config.pwaOpenLastPage !== false) {
|
2024-07-26 02:29:13 +08:00
|
|
|
// Cold start PWA load
|
|
|
|
const lastPage = await this.stateDataStore.get([
|
|
|
|
"client",
|
|
|
|
"lastOpenedPage",
|
|
|
|
]);
|
|
|
|
if (lastPage) {
|
|
|
|
console.log("Navigating to last opened page", lastPage);
|
|
|
|
await this.navigate({ page: lastPage });
|
|
|
|
}
|
2023-08-16 17:40:31 +08:00
|
|
|
}
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
2022-03-20 16:56:28 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
async initSpace(): Promise<SpacePrimitives> {
|
2023-12-17 18:46:18 +08:00
|
|
|
this.httpSpacePrimitives = new HttpSpacePrimitives(
|
2023-07-27 17:41:44 +08:00
|
|
|
location.origin,
|
|
|
|
window.silverBulletConfig.spaceFolderPath,
|
|
|
|
);
|
2023-01-22 22:48:12 +08:00
|
|
|
|
2023-12-17 18:46:18 +08:00
|
|
|
let remoteSpacePrimitives: SpacePrimitives = this.httpSpacePrimitives;
|
|
|
|
|
2024-01-27 00:05:10 +08:00
|
|
|
if (this.readOnlyMode) {
|
|
|
|
remoteSpacePrimitives = new ReadOnlySpacePrimitives(
|
|
|
|
remoteSpacePrimitives,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
2023-12-17 18:46:18 +08:00
|
|
|
remoteSpacePrimitives,
|
2024-02-07 21:50:01 +08:00
|
|
|
this.clientSystem.namespaceHook,
|
2023-11-15 23:13:44 +08:00
|
|
|
this.syncMode ? undefined : "client",
|
2023-07-27 17:41:44 +08:00
|
|
|
);
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
let fileFilterFn: (s: string) => boolean = () => true;
|
|
|
|
|
2023-08-26 14:31:51 +08:00
|
|
|
let localSpacePrimitives: SpacePrimitives | undefined;
|
|
|
|
|
2023-08-30 03:17:29 +08:00
|
|
|
if (this.syncMode) {
|
2023-10-03 20:16:33 +08:00
|
|
|
// We'll store the space files in a separate data store
|
|
|
|
const spaceKvPrimitives = new IndexedDBKvPrimitives(
|
|
|
|
`${this.dbPrefix}_synced_space`,
|
|
|
|
);
|
|
|
|
await spaceKvPrimitives.init();
|
|
|
|
|
2024-01-27 00:05:10 +08:00
|
|
|
this.spaceKV = spaceKvPrimitives;
|
|
|
|
|
2023-08-26 14:31:51 +08:00
|
|
|
localSpacePrimitives = new FilteredSpacePrimitives(
|
2023-10-03 20:16:33 +08:00
|
|
|
new EventedSpacePrimitives(
|
|
|
|
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
|
|
|
|
new FallbackSpacePrimitives(
|
2024-01-21 02:16:07 +08:00
|
|
|
new DataStoreSpacePrimitives(
|
|
|
|
new DataStore(
|
|
|
|
spaceKvPrimitives,
|
2024-02-06 23:51:04 +08:00
|
|
|
{},
|
2024-01-21 02:16:07 +08:00
|
|
|
),
|
|
|
|
),
|
2023-10-03 20:16:33 +08:00
|
|
|
this.plugSpaceRemotePrimitives,
|
2023-07-27 17:41:44 +08:00
|
|
|
),
|
2023-10-03 20:16:33 +08:00
|
|
|
this.eventHook,
|
2023-07-27 17:41:44 +08:00
|
|
|
),
|
2023-08-26 14:31:51 +08:00
|
|
|
(meta) => fileFilterFn(meta.name),
|
|
|
|
// Run when a list of files has been retrieved
|
|
|
|
async () => {
|
2024-08-02 22:47:36 +08:00
|
|
|
if (!this.config) {
|
|
|
|
await this.loadConfig();
|
2023-12-17 18:46:18 +08:00
|
|
|
}
|
|
|
|
|
2024-08-02 22:47:36 +08:00
|
|
|
if (typeof this.config?.spaceIgnore === "string") {
|
|
|
|
fileFilterFn = gitIgnoreCompiler(this.config.spaceIgnore).accepts;
|
2023-08-26 14:31:51 +08:00
|
|
|
} else {
|
|
|
|
fileFilterFn = () => true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
2023-08-27 17:02:24 +08:00
|
|
|
localSpacePrimitives = new EventedSpacePrimitives(
|
|
|
|
this.plugSpaceRemotePrimitives,
|
|
|
|
this.eventHook,
|
|
|
|
);
|
2023-08-26 14:31:51 +08:00
|
|
|
}
|
2023-05-24 02:53:53 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
this.space = new Space(
|
|
|
|
localSpacePrimitives,
|
|
|
|
this.eventHook,
|
|
|
|
);
|
2023-07-27 17:41:44 +08:00
|
|
|
|
2024-03-14 20:02:34 +08:00
|
|
|
let lastSaveTimestamp: number | undefined;
|
|
|
|
|
|
|
|
this.eventHook.addLocalListener("editor:pageSaving", () => {
|
|
|
|
lastSaveTimestamp = Date.now();
|
|
|
|
});
|
|
|
|
|
2023-11-12 17:33:27 +08:00
|
|
|
this.eventHook.addLocalListener(
|
|
|
|
"file:changed",
|
|
|
|
(
|
|
|
|
path: string,
|
2024-06-14 14:44:46 +08:00
|
|
|
_localChange: boolean,
|
2024-03-14 20:02:34 +08:00
|
|
|
oldHash: number,
|
|
|
|
newHash: number,
|
2023-11-12 17:33:27 +08:00
|
|
|
) => {
|
|
|
|
// Only reload when watching the current page (to avoid reloading when switching pages)
|
|
|
|
if (
|
2024-03-14 20:02:34 +08:00
|
|
|
this.space.watchInterval && `${this.currentPage}.md` === path &&
|
|
|
|
// Avoid reloading if the page was just saved (5s window)
|
|
|
|
(!lastSaveTimestamp || (lastSaveTimestamp < Date.now() - 5000))
|
2023-11-12 17:33:27 +08:00
|
|
|
) {
|
|
|
|
console.log(
|
|
|
|
"Page changed elsewhere, reloading. Old hash",
|
|
|
|
oldHash,
|
|
|
|
"new hash",
|
|
|
|
newHash,
|
|
|
|
);
|
2024-03-14 20:02:34 +08:00
|
|
|
console.log(
|
|
|
|
"Last save timestamp",
|
|
|
|
lastSaveTimestamp,
|
|
|
|
"now",
|
|
|
|
Date.now(),
|
|
|
|
);
|
2023-11-12 17:33:27 +08:00
|
|
|
this.flashNotification("Page changed elsewhere, reloading");
|
|
|
|
this.reloadPage();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2023-08-27 17:02:24 +08:00
|
|
|
|
2024-05-28 02:33:41 +08:00
|
|
|
// Caching a list of known files for the wiki_link highlighter (that checks if a file exists)
|
2024-07-26 02:29:13 +08:00
|
|
|
// And keeping it up to date as we go
|
2024-05-28 02:33:41 +08:00
|
|
|
this.eventHook.addLocalListener("file:changed", (fileName: string) => {
|
|
|
|
// Make sure this file is in the list of known pages
|
|
|
|
this.clientSystem.allKnownFiles.add(fileName);
|
2023-12-22 22:55:50 +08:00
|
|
|
});
|
2024-05-28 02:33:41 +08:00
|
|
|
this.eventHook.addLocalListener("file:deleted", (fileName: string) => {
|
|
|
|
this.clientSystem.allKnownFiles.delete(fileName);
|
2023-12-22 22:55:50 +08:00
|
|
|
});
|
|
|
|
this.eventHook.addLocalListener(
|
|
|
|
"file:listed",
|
|
|
|
(allFiles: FileMeta[]) => {
|
|
|
|
// Update list of known pages
|
2024-05-28 02:33:41 +08:00
|
|
|
this.clientSystem.allKnownFiles.clear();
|
2024-01-21 02:16:07 +08:00
|
|
|
allFiles.forEach((f) => {
|
2024-05-28 02:33:41 +08:00
|
|
|
if (!f.name.startsWith(plugPrefix)) {
|
|
|
|
this.clientSystem.allKnownFiles.add(f.name);
|
2024-01-21 02:16:07 +08:00
|
|
|
}
|
|
|
|
});
|
2023-12-22 22:55:50 +08:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2023-07-27 17:41:44 +08:00
|
|
|
this.space.watch();
|
2023-08-26 14:31:51 +08:00
|
|
|
|
|
|
|
return localSpacePrimitives;
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2024-01-24 21:44:39 +08:00
|
|
|
get currentPage(): string {
|
|
|
|
return this.ui.viewState.currentPage !== undefined
|
|
|
|
? this.ui.viewState.currentPage
|
|
|
|
: this.onLoadPageRef.page; // best effort
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
|
|
|
|
2023-07-27 21:25:33 +08:00
|
|
|
dispatchAppEvent(name: AppEvent, ...args: any[]): Promise<any[]> {
|
|
|
|
return this.eventHook.dispatchEvent(name, ...args);
|
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Save the current page
|
2022-10-10 20:50:21 +08:00
|
|
|
save(immediate = false): Promise<void> {
|
2022-03-31 20:28:07 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (this.saveTimeout) {
|
|
|
|
clearTimeout(this.saveTimeout);
|
|
|
|
}
|
|
|
|
this.saveTimeout = setTimeout(
|
|
|
|
() => {
|
|
|
|
if (this.currentPage) {
|
2022-12-21 21:55:24 +08:00
|
|
|
if (
|
2023-07-14 20:22:26 +08:00
|
|
|
!this.ui.viewState.unsavedChanges ||
|
|
|
|
this.ui.viewState.uiOptions.forcedROMode
|
2022-12-21 21:55:24 +08:00
|
|
|
) {
|
2022-10-23 02:23:54 +08:00
|
|
|
// No unsaved changes, or read-only mode, not gonna save
|
2022-10-22 01:02:00 +08:00
|
|
|
return resolve();
|
|
|
|
}
|
2022-03-31 20:28:07 +08:00
|
|
|
console.log("Saving page", this.currentPage);
|
2024-03-02 22:21:36 +08:00
|
|
|
this.dispatchAppEvent(
|
|
|
|
"editor:pageSaving",
|
|
|
|
this.currentPage,
|
|
|
|
);
|
2022-03-31 20:28:07 +08:00
|
|
|
this.space
|
|
|
|
.writePage(
|
|
|
|
this.currentPage,
|
2023-07-27 17:41:44 +08:00
|
|
|
this.editorView.state.sliceDoc(0),
|
2022-10-10 20:50:21 +08:00
|
|
|
true,
|
2022-03-31 20:28:07 +08:00
|
|
|
)
|
2023-06-14 02:47:05 +08:00
|
|
|
.then(async (meta) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({ type: "page-saved" });
|
2023-06-14 02:47:05 +08:00
|
|
|
await this.dispatchAppEvent(
|
|
|
|
"editor:pageSaved",
|
|
|
|
this.currentPage,
|
|
|
|
meta,
|
|
|
|
);
|
2024-07-14 17:29:43 +08:00
|
|
|
|
|
|
|
// At this all the essential stuff is done, let's proceed
|
|
|
|
resolve();
|
|
|
|
|
|
|
|
// In the background we'll fetch any enriched meta data, if any
|
2024-07-13 20:55:35 +08:00
|
|
|
const enrichedMeta = await this.clientSystem.getObjectByRef<
|
|
|
|
PageMeta
|
|
|
|
>(
|
|
|
|
this.currentPage,
|
|
|
|
"page",
|
|
|
|
this.currentPage,
|
|
|
|
);
|
2024-07-14 17:29:43 +08:00
|
|
|
if (enrichedMeta) {
|
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "update-current-page-meta",
|
|
|
|
meta: enrichedMeta,
|
|
|
|
});
|
|
|
|
}
|
2022-03-31 20:28:07 +08:00
|
|
|
})
|
2022-07-19 23:21:11 +08:00
|
|
|
.catch((e) => {
|
|
|
|
this.flashNotification(
|
|
|
|
"Could not save page, retrying again in 10 seconds",
|
2022-10-10 20:50:21 +08:00
|
|
|
"error",
|
2022-07-19 23:21:11 +08:00
|
|
|
);
|
|
|
|
this.saveTimeout = setTimeout(this.save.bind(this), 10000);
|
|
|
|
reject(e);
|
|
|
|
});
|
2022-03-31 20:28:07 +08:00
|
|
|
} else {
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
},
|
2023-07-14 19:44:30 +08:00
|
|
|
immediate ? 0 : autoSaveInterval,
|
2022-03-31 20:28:07 +08:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-14 19:32:28 +08:00
|
|
|
flashNotification(message: string, type: "info" | "error" = "info") {
|
2022-10-10 20:50:21 +08:00
|
|
|
const id = Math.floor(Math.random() * 1000000);
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-03-20 16:56:28 +08:00
|
|
|
type: "show-notification",
|
|
|
|
notification: {
|
2022-07-14 19:32:28 +08:00
|
|
|
id,
|
|
|
|
type,
|
|
|
|
message,
|
2022-03-20 16:56:28 +08:00
|
|
|
date: new Date(),
|
|
|
|
},
|
|
|
|
});
|
2022-07-14 19:32:28 +08:00
|
|
|
setTimeout(
|
|
|
|
() => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-07-14 19:32:28 +08:00
|
|
|
type: "dismiss-notification",
|
|
|
|
id: id,
|
|
|
|
});
|
|
|
|
},
|
2023-05-24 02:53:53 +08:00
|
|
|
type === "info" ? 4000 : 5000,
|
2022-07-14 19:32:28 +08:00
|
|
|
);
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2024-07-19 23:06:40 +08:00
|
|
|
startPageNavigate(mode: "page" | "meta" | "all") {
|
2023-12-22 01:37:50 +08:00
|
|
|
// Then show the page navigator
|
2024-01-21 02:16:07 +08:00
|
|
|
this.ui.viewDispatch({ type: "start-navigate", mode });
|
2024-07-26 02:29:13 +08:00
|
|
|
// And update the page list cache asynchronously
|
2023-12-22 22:55:50 +08:00
|
|
|
this.updatePageListCache().catch(console.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async updatePageListCache() {
|
|
|
|
console.log("Updating page list cache");
|
2024-02-07 21:50:01 +08:00
|
|
|
if (!this.clientSystem.system.loadedPlugs.has("index")) {
|
2024-02-06 23:51:04 +08:00
|
|
|
console.warn("Index plug not loaded, cannot update page list cache");
|
|
|
|
return;
|
|
|
|
}
|
2024-02-07 21:50:01 +08:00
|
|
|
const allPages = await this.clientSystem.queryObjects<PageMeta>("page", {});
|
2024-07-25 21:18:58 +08:00
|
|
|
const allBrokenLinkPages = (await this.clientSystem.queryObjects<
|
|
|
|
LinkObject
|
|
|
|
>("link", {
|
|
|
|
filter: ["and", ["attr", "toPage"], ["not", ["call", "pageExists", [[
|
|
|
|
"attr",
|
|
|
|
"toPage",
|
|
|
|
]]]]],
|
|
|
|
select: [{ name: "toPage" }],
|
|
|
|
})).map((link): PageMeta => ({
|
|
|
|
ref: link.toPage!,
|
|
|
|
tag: "page",
|
|
|
|
_isBrokenLink: true,
|
|
|
|
name: link.toPage!,
|
|
|
|
created: "",
|
|
|
|
lastModified: "",
|
|
|
|
perm: "rw",
|
|
|
|
}));
|
|
|
|
|
2023-12-22 22:55:50 +08:00
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "update-page-list",
|
2024-07-25 21:18:58 +08:00
|
|
|
allPages: allPages.concat(allBrokenLinkPages),
|
2023-12-22 22:55:50 +08:00
|
|
|
});
|
2023-12-22 01:37:50 +08:00
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Progress circle handling
|
2023-07-27 23:02:53 +08:00
|
|
|
private progressTimeout?: number;
|
2024-07-26 02:29:13 +08:00
|
|
|
|
2023-06-15 02:58:08 +08:00
|
|
|
showProgress(progressPerc: number) {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2023-06-15 02:58:08 +08:00
|
|
|
type: "set-progress",
|
|
|
|
progressPerc,
|
|
|
|
});
|
|
|
|
if (this.progressTimeout) {
|
|
|
|
clearTimeout(this.progressTimeout);
|
|
|
|
}
|
|
|
|
this.progressTimeout = setTimeout(
|
|
|
|
() => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2023-06-15 02:58:08 +08:00
|
|
|
type: "set-progress",
|
|
|
|
});
|
|
|
|
},
|
|
|
|
10000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Various UI elements
|
2022-04-13 20:46:52 +08:00
|
|
|
filterBox(
|
|
|
|
label: string,
|
|
|
|
options: FilterOption[],
|
2022-10-10 20:50:21 +08:00
|
|
|
helpText = "",
|
|
|
|
placeHolder = "",
|
2022-04-13 20:46:52 +08:00
|
|
|
): Promise<FilterOption | undefined> {
|
|
|
|
return new Promise((resolve) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-04-13 20:46:52 +08:00
|
|
|
type: "show-filterbox",
|
2022-04-21 17:46:33 +08:00
|
|
|
label,
|
2022-04-13 20:46:52 +08:00
|
|
|
options,
|
|
|
|
placeHolder,
|
|
|
|
helpText,
|
2022-12-09 00:04:07 +08:00
|
|
|
onSelect: (option: any) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({ type: "hide-filterbox" });
|
2022-04-19 22:54:47 +08:00
|
|
|
this.focus();
|
2022-04-13 20:46:52 +08:00
|
|
|
resolve(option);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-12-21 23:08:51 +08:00
|
|
|
prompt(
|
|
|
|
message: string,
|
|
|
|
defaultValue = "",
|
|
|
|
): Promise<string | undefined> {
|
|
|
|
return new Promise((resolve) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-12-21 23:08:51 +08:00
|
|
|
type: "show-prompt",
|
|
|
|
message,
|
|
|
|
defaultValue,
|
|
|
|
callback: (value: string | undefined) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({ type: "hide-prompt" });
|
2022-12-21 23:08:51 +08:00
|
|
|
this.focus();
|
|
|
|
resolve(value);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
confirm(
|
|
|
|
message: string,
|
|
|
|
): Promise<boolean> {
|
|
|
|
return new Promise((resolve) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-12-21 23:08:51 +08:00
|
|
|
type: "show-confirm",
|
|
|
|
message,
|
|
|
|
callback: (value: boolean) => {
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({ type: "hide-confirm" });
|
2022-12-21 23:08:51 +08:00
|
|
|
this.focus();
|
|
|
|
resolve(value);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-07-27 21:25:33 +08:00
|
|
|
async loadPlugs() {
|
2024-02-07 21:50:01 +08:00
|
|
|
await this.clientSystem.reloadPlugsFromSpace(this.space);
|
2023-10-03 20:16:33 +08:00
|
|
|
await this.eventHook.dispatchEvent("system:ready");
|
2022-07-11 15:08:22 +08:00
|
|
|
await this.dispatchAppEvent("plugs:loaded");
|
2022-04-27 01:04:36 +08:00
|
|
|
}
|
|
|
|
|
2023-07-07 21:46:11 +08:00
|
|
|
rebuildEditorState() {
|
|
|
|
const editorView = this.editorView;
|
|
|
|
console.log("Rebuilding editor state");
|
|
|
|
|
2023-07-27 21:25:33 +08:00
|
|
|
if (this.currentPage) {
|
2022-03-31 23:25:34 +08:00
|
|
|
editorView.setState(
|
2023-07-14 19:58:16 +08:00
|
|
|
createEditorState(
|
|
|
|
this,
|
2023-01-16 18:28:59 +08:00
|
|
|
this.currentPage,
|
|
|
|
editorView.state.sliceDoc(),
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewState.currentPageMeta?.perm === "ro",
|
2023-01-16 18:28:59 +08:00
|
|
|
),
|
2022-03-31 23:25:34 +08:00
|
|
|
);
|
2022-04-20 16:56:43 +08:00
|
|
|
if (editorView.contentDOM) {
|
2022-05-17 17:53:17 +08:00
|
|
|
this.tweakEditorDOM(
|
|
|
|
editorView.contentDOM,
|
|
|
|
);
|
2022-04-20 16:56:43 +08:00
|
|
|
}
|
2022-03-31 23:25:34 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-21 21:55:24 +08:00
|
|
|
// Code completion support
|
2023-11-06 16:14:16 +08:00
|
|
|
async completeWithEvent(
|
2022-12-21 21:55:24 +08:00
|
|
|
context: CompletionContext,
|
|
|
|
eventName: AppEvent,
|
2024-02-24 16:26:00 +08:00
|
|
|
): Promise<CompletionResult | SlashCompletions | null> {
|
2022-12-21 21:55:24 +08:00
|
|
|
const editorState = context.state;
|
|
|
|
const selection = editorState.selection.main;
|
|
|
|
const line = editorState.doc.lineAt(selection.from);
|
|
|
|
const linePrefix = line.text.slice(0, selection.from - line.from);
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Build up list of parent nodes, some completions need this
|
2023-08-02 03:35:19 +08:00
|
|
|
const parentNodes: string[] = [];
|
2023-10-03 20:16:33 +08:00
|
|
|
const sTree = syntaxTree(editorState);
|
|
|
|
const currentNode = sTree.resolveInner(selection.from);
|
2023-08-02 03:35:19 +08:00
|
|
|
if (currentNode) {
|
2023-10-03 20:16:33 +08:00
|
|
|
let node: SyntaxNode | null = currentNode;
|
|
|
|
do {
|
2023-12-22 01:37:50 +08:00
|
|
|
if (node.name === "FencedCode" || node.name === "FrontMatter") {
|
2023-10-04 23:14:24 +08:00
|
|
|
const body = editorState.sliceDoc(node.from + 3, node.to - 3);
|
2023-12-22 01:37:50 +08:00
|
|
|
parentNodes.push(`${node.name}:${body}`);
|
2023-10-03 20:16:33 +08:00
|
|
|
} else {
|
|
|
|
parentNodes.push(node.name);
|
|
|
|
}
|
2023-08-02 03:35:19 +08:00
|
|
|
node = node.parent;
|
2023-10-03 20:16:33 +08:00
|
|
|
} while (node);
|
2023-08-02 03:35:19 +08:00
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Dispatch the event
|
2022-12-21 21:55:24 +08:00
|
|
|
const results = await this.dispatchAppEvent(eventName, {
|
2024-01-24 21:44:39 +08:00
|
|
|
pageName: this.currentPage,
|
2022-12-21 21:55:24 +08:00
|
|
|
linePrefix,
|
|
|
|
pos: selection.from,
|
2023-08-02 03:35:19 +08:00
|
|
|
parentNodes,
|
2022-12-21 21:55:24 +08:00
|
|
|
} as CompleteEvent);
|
2024-07-26 02:29:13 +08:00
|
|
|
|
|
|
|
// Merge results
|
2024-02-23 20:42:02 +08:00
|
|
|
let currentResult: CompletionResult | null = null;
|
2022-04-19 22:54:47 +08:00
|
|
|
for (const result of results) {
|
2024-02-23 20:42:02 +08:00
|
|
|
if (!result) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (currentResult) {
|
|
|
|
// Let's see if we can merge results
|
|
|
|
if (currentResult.from !== result.from) {
|
|
|
|
console.error(
|
|
|
|
"Got completion results from multiple sources with different `from` locators, cannot deal with that",
|
|
|
|
);
|
2022-04-19 22:54:47 +08:00
|
|
|
console.error(
|
2024-02-23 20:42:02 +08:00
|
|
|
"Previously had",
|
|
|
|
currentResult,
|
|
|
|
"now also got",
|
|
|
|
result,
|
2022-04-19 22:54:47 +08:00
|
|
|
);
|
|
|
|
return null;
|
2024-02-23 20:42:02 +08:00
|
|
|
} else {
|
|
|
|
// Merge
|
|
|
|
currentResult = {
|
|
|
|
from: result.from,
|
|
|
|
options: [...currentResult.options, ...result.options],
|
|
|
|
};
|
2022-04-19 22:54:47 +08:00
|
|
|
}
|
2024-02-23 20:42:02 +08:00
|
|
|
} else {
|
|
|
|
currentResult = result;
|
2022-04-19 22:54:47 +08:00
|
|
|
}
|
|
|
|
}
|
2024-02-23 20:42:02 +08:00
|
|
|
return currentResult;
|
2022-04-19 22:54:47 +08:00
|
|
|
}
|
|
|
|
|
2022-12-21 21:55:24 +08:00
|
|
|
editorComplete(
|
|
|
|
context: CompletionContext,
|
|
|
|
): Promise<CompletionResult | null> {
|
2024-02-24 16:26:00 +08:00
|
|
|
return this.completeWithEvent(context, "editor:complete") as Promise<
|
|
|
|
CompletionResult | null
|
|
|
|
>;
|
2022-12-21 21:55:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
miniEditorComplete(
|
|
|
|
context: CompletionContext,
|
|
|
|
): Promise<CompletionResult | null> {
|
2024-02-24 16:26:00 +08:00
|
|
|
return this.completeWithEvent(context, "minieditor:complete") as Promise<
|
|
|
|
CompletionResult | null
|
|
|
|
>;
|
2022-12-21 21:55:24 +08:00
|
|
|
}
|
|
|
|
|
2022-10-11 00:19:08 +08:00
|
|
|
async reloadPage() {
|
2022-03-20 16:56:28 +08:00
|
|
|
console.log("Reloading page");
|
2022-10-11 00:19:08 +08:00
|
|
|
clearTimeout(this.saveTimeout);
|
2024-01-24 21:44:39 +08:00
|
|
|
await this.loadPage(this.currentPage);
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2024-07-26 02:29:13 +08:00
|
|
|
// Focus the editor
|
2022-03-20 16:56:28 +08:00
|
|
|
focus() {
|
2023-07-24 15:36:33 +08:00
|
|
|
const viewState = this.ui.viewState;
|
|
|
|
if (
|
|
|
|
[
|
|
|
|
viewState.showCommandPalette,
|
|
|
|
viewState.showPageNavigator,
|
|
|
|
viewState.showFilterBox,
|
|
|
|
viewState.showConfirm,
|
|
|
|
viewState.showPrompt,
|
|
|
|
].some(Boolean)
|
|
|
|
) {
|
2024-02-28 19:16:51 +08:00
|
|
|
// console.log("not focusing");
|
2023-07-24 15:36:33 +08:00
|
|
|
// Some other modal UI element is visible, don't focus editor now
|
|
|
|
return;
|
|
|
|
}
|
2023-07-27 17:41:44 +08:00
|
|
|
this.editorView.focus();
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2022-10-29 15:23:12 +08:00
|
|
|
async navigate(
|
2024-01-24 18:58:33 +08:00
|
|
|
pageRef: PageRef,
|
2022-10-29 15:23:12 +08:00
|
|
|
replaceState = false,
|
|
|
|
newWindow = false,
|
|
|
|
) {
|
2024-01-24 18:58:33 +08:00
|
|
|
if (!pageRef.page) {
|
|
|
|
pageRef.page = cleanPageRef(
|
2024-02-03 02:19:07 +08:00
|
|
|
await renderTheTemplate(
|
2024-08-02 22:47:36 +08:00
|
|
|
this.config.indexPage,
|
2024-02-03 02:19:07 +08:00
|
|
|
{},
|
|
|
|
{},
|
|
|
|
builtinFunctions,
|
|
|
|
),
|
2024-01-21 02:16:07 +08:00
|
|
|
);
|
2022-08-02 18:43:39 +08:00
|
|
|
}
|
2022-10-29 15:23:12 +08:00
|
|
|
|
2023-07-28 21:20:56 +08:00
|
|
|
try {
|
2024-01-24 18:58:33 +08:00
|
|
|
validatePageName(pageRef.page);
|
2023-07-28 21:20:56 +08:00
|
|
|
} catch (e: any) {
|
|
|
|
return this.flashNotification(e.message, "error");
|
2023-07-06 22:47:50 +08:00
|
|
|
}
|
|
|
|
|
2022-10-29 15:23:12 +08:00
|
|
|
if (newWindow) {
|
2024-01-24 21:44:39 +08:00
|
|
|
console.log(
|
|
|
|
"Navigating to new page in new window",
|
2024-08-20 15:38:56 +08:00
|
|
|
`${location.origin}/${encodePageURI(encodePageRef(pageRef))}`,
|
2024-01-24 21:44:39 +08:00
|
|
|
);
|
2024-02-07 21:50:01 +08:00
|
|
|
const win = globalThis.open(
|
2024-08-20 15:38:56 +08:00
|
|
|
`${location.origin}/${encodePageURI(encodePageRef(pageRef))}`,
|
2024-01-24 18:58:33 +08:00
|
|
|
"_blank",
|
|
|
|
);
|
2022-10-29 15:23:12 +08:00
|
|
|
if (win) {
|
|
|
|
win.focus();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2024-01-24 18:58:33 +08:00
|
|
|
|
|
|
|
await this.pageNavigator!.navigate(
|
|
|
|
pageRef,
|
|
|
|
replaceState,
|
|
|
|
);
|
|
|
|
this.focus();
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2024-01-24 18:58:33 +08:00
|
|
|
async loadPage(pageName: string) {
|
2022-05-17 21:54:55 +08:00
|
|
|
const loadingDifferentPage = pageName !== this.currentPage;
|
2022-03-20 16:56:28 +08:00
|
|
|
const editorView = this.editorView;
|
2022-09-06 22:33:00 +08:00
|
|
|
const previousPage = this.currentPage;
|
|
|
|
|
2022-03-20 16:56:28 +08:00
|
|
|
// Persist current page state and nicely close page
|
2022-09-06 22:33:00 +08:00
|
|
|
if (previousPage) {
|
2024-01-24 18:58:33 +08:00
|
|
|
// this.openPages.saveState(previousPage);
|
2022-09-06 22:33:00 +08:00
|
|
|
this.space.unwatchPage(previousPage);
|
2022-10-11 00:19:08 +08:00
|
|
|
if (previousPage !== pageName) {
|
|
|
|
await this.save(true);
|
|
|
|
}
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2023-07-14 20:22:26 +08:00
|
|
|
this.ui.viewDispatch({
|
2022-09-12 20:50:37 +08:00
|
|
|
type: "page-loading",
|
|
|
|
name: pageName,
|
|
|
|
});
|
|
|
|
|
2022-03-20 16:56:28 +08:00
|
|
|
// Fetch next page to open
|
2022-04-06 21:39:20 +08:00
|
|
|
let doc;
|
|
|
|
try {
|
|
|
|
doc = await this.space.readPage(pageName);
|
|
|
|
} catch (e: any) {
|
2023-07-27 17:41:44 +08:00
|
|
|
if (e.message.includes("Not found")) {
|
|
|
|
// Not found, new page
|
|
|
|
console.log("Page doesn't exist, creating new page:", pageName);
|
2024-01-21 02:16:07 +08:00
|
|
|
// Initialize page
|
2023-07-27 17:41:44 +08:00
|
|
|
doc = {
|
|
|
|
text: "",
|
2023-11-06 16:14:16 +08:00
|
|
|
meta: {
|
|
|
|
ref: pageName,
|
|
|
|
tags: ["page"],
|
|
|
|
name: pageName,
|
|
|
|
lastModified: "",
|
|
|
|
created: "",
|
|
|
|
perm: "rw",
|
|
|
|
} as PageMeta,
|
2023-07-27 17:41:44 +08:00
|
|
|
};
|
2024-07-26 02:29:13 +08:00
|
|
|
// Create new page based on a template
|
2024-02-07 21:50:01 +08:00
|
|
|
this.clientSystem.system.invokeFunction("template.newPage", [pageName])
|
|
|
|
.then(
|
|
|
|
() => {
|
|
|
|
this.focus();
|
|
|
|
},
|
|
|
|
).catch(
|
|
|
|
console.error,
|
|
|
|
);
|
2023-07-27 17:41:44 +08:00
|
|
|
} else {
|
2023-10-06 00:24:12 +08:00
|
|
|
this.flashNotification(
|
|
|
|
`Could not load page ${pageName}: ${e.message}`,
|
|
|
|
"error",
|
|
|
|
);
|
|
|
|
if (previousPage) {
|
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "page-loading",
|
|
|
|
name: previousPage,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2023-07-27 17:41:44 +08:00
|
|
|
}
|
2022-04-06 21:39:20 +08:00
|
|
|
}
|
|
|
|
|
2023-12-14 00:52:56 +08:00
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "page-loaded",
|
|
|
|
meta: doc.meta,
|
|
|
|
});
|
|
|
|
|
2024-07-14 17:29:43 +08:00
|
|
|
// Fetch (possibly) enriched meta data asynchronously
|
2024-07-26 02:29:13 +08:00
|
|
|
this.clientSystem.getObjectByRef<
|
2024-07-14 17:29:43 +08:00
|
|
|
PageMeta
|
|
|
|
>(
|
|
|
|
this.currentPage,
|
|
|
|
"page",
|
|
|
|
this.currentPage,
|
|
|
|
).then((enrichedMeta) => {
|
|
|
|
if (!enrichedMeta) {
|
|
|
|
// Nothing in the store, revert to default
|
|
|
|
enrichedMeta = doc.meta;
|
|
|
|
}
|
2024-07-31 17:28:31 +08:00
|
|
|
|
|
|
|
const bodyEl = this.parent.parentElement;
|
|
|
|
if (bodyEl) {
|
|
|
|
bodyEl.removeAttribute("class");
|
2024-07-31 19:06:10 +08:00
|
|
|
if (enrichedMeta.pageDecoration?.cssClasses) {
|
2024-07-31 17:28:31 +08:00
|
|
|
bodyEl.className = enrichedMeta.pageDecoration.cssClasses.join(" ")
|
|
|
|
.replaceAll(/[^a-zA-Z0-9-_ ]/g, "");
|
|
|
|
}
|
|
|
|
}
|
2024-07-14 17:29:43 +08:00
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "update-current-page-meta",
|
|
|
|
meta: enrichedMeta,
|
|
|
|
});
|
|
|
|
}).catch(console.error);
|
|
|
|
|
2024-08-22 20:24:14 +08:00
|
|
|
// When loading a different page OR if the page is read-only (in which case we don't want to apply local patches, because there's no point)
|
|
|
|
if (loadingDifferentPage || doc.meta.perm === "ro") {
|
2024-08-22 03:36:50 +08:00
|
|
|
const editorState = createEditorState(
|
|
|
|
this,
|
|
|
|
pageName,
|
|
|
|
doc.text,
|
|
|
|
doc.meta.perm === "ro",
|
|
|
|
);
|
|
|
|
editorView.setState(editorState);
|
|
|
|
if (editorView.contentDOM) {
|
|
|
|
this.tweakEditorDOM(editorView.contentDOM);
|
|
|
|
}
|
|
|
|
this.space.watchPage(pageName);
|
|
|
|
} else {
|
2024-08-22 20:24:14 +08:00
|
|
|
// Just apply minimal patches so that the cursor is preserved
|
2024-08-22 03:36:50 +08:00
|
|
|
await editor.setText(doc.text);
|
2022-04-20 16:56:43 +08:00
|
|
|
}
|
2022-03-31 20:28:07 +08:00
|
|
|
|
2022-11-24 23:55:30 +08:00
|
|
|
// Note: these events are dispatched asynchronously deliberately (not waiting for results)
|
2022-05-17 21:54:55 +08:00
|
|
|
if (loadingDifferentPage) {
|
2023-06-14 02:47:05 +08:00
|
|
|
this.eventHook.dispatchEvent("editor:pageLoaded", pageName, previousPage)
|
|
|
|
.catch(
|
|
|
|
console.error,
|
|
|
|
);
|
2022-07-18 22:48:36 +08:00
|
|
|
} else {
|
2022-11-24 23:55:30 +08:00
|
|
|
this.eventHook.dispatchEvent("editor:pageReloaded", pageName).catch(
|
|
|
|
console.error,
|
|
|
|
);
|
2022-05-17 21:54:55 +08:00
|
|
|
}
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|
|
|
|
|
2023-01-16 18:28:59 +08:00
|
|
|
tweakEditorDOM(contentDOM: HTMLElement) {
|
2022-05-09 16:45:36 +08:00
|
|
|
contentDOM.spellcheck = true;
|
|
|
|
contentDOM.setAttribute("autocorrect", "on");
|
|
|
|
contentDOM.setAttribute("autocapitalize", "on");
|
|
|
|
}
|
|
|
|
|
2023-02-28 17:07:20 +08:00
|
|
|
async loadCustomStyles() {
|
2024-03-09 19:26:58 +08:00
|
|
|
const spaceStyles = await this.clientSystem.queryObjects<StyleObject>(
|
|
|
|
"space-style",
|
|
|
|
{},
|
|
|
|
);
|
|
|
|
if (!spaceStyles) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort stylesheets (last declared styles take precedence)
|
|
|
|
// Order is 1: Imported styles, 2: Other styles, 3: customStyles from Settings
|
2024-08-02 22:47:36 +08:00
|
|
|
const sortOrder = ["library", "user", "config"];
|
2024-03-09 19:26:58 +08:00
|
|
|
spaceStyles.sort((a, b) =>
|
|
|
|
sortOrder.indexOf(a.origin) - sortOrder.indexOf(b.origin)
|
|
|
|
);
|
|
|
|
|
|
|
|
const accumulatedCSS: string[] = [];
|
|
|
|
for (const s of spaceStyles) {
|
|
|
|
accumulatedCSS.push(s.style);
|
2023-02-28 17:07:20 +08:00
|
|
|
}
|
2024-03-09 19:26:58 +08:00
|
|
|
const customStylesContent = accumulatedCSS.join("\n\n");
|
|
|
|
this.ui.viewDispatch({
|
|
|
|
type: "set-ui-option",
|
|
|
|
key: "customStyles",
|
|
|
|
value: customStylesContent,
|
|
|
|
});
|
|
|
|
document.getElementById("custom-styles")!.innerHTML = customStylesContent;
|
2023-02-28 17:07:20 +08:00
|
|
|
}
|
|
|
|
|
2023-12-22 20:22:25 +08:00
|
|
|
async runCommandByName(name: string, args?: any[]) {
|
2023-07-14 20:22:26 +08:00
|
|
|
const cmd = this.ui.viewState.commands.get(name);
|
2022-07-11 15:08:22 +08:00
|
|
|
if (cmd) {
|
2023-11-26 01:57:00 +08:00
|
|
|
if (args) {
|
|
|
|
await cmd.run(args);
|
|
|
|
} else {
|
|
|
|
await cmd.run();
|
|
|
|
}
|
2022-07-11 15:08:22 +08:00
|
|
|
} else {
|
|
|
|
throw new Error(`Command ${name} not found`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-14 20:22:26 +08:00
|
|
|
getCommandsByContext(
|
2023-01-04 23:37:09 +08:00
|
|
|
state: AppViewState,
|
|
|
|
): Map<string, AppCommand> {
|
|
|
|
const commands = new Map(state.commands);
|
|
|
|
for (const [k, v] of state.commands.entries()) {
|
|
|
|
if (
|
|
|
|
v.command.contexts &&
|
|
|
|
(!state.showCommandPaletteContext ||
|
|
|
|
!v.command.contexts.includes(state.showCommandPaletteContext))
|
|
|
|
) {
|
|
|
|
commands.delete(k);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return commands;
|
|
|
|
}
|
|
|
|
|
2023-07-14 19:58:16 +08:00
|
|
|
getContext(): string | undefined {
|
2023-07-27 17:41:44 +08:00
|
|
|
const state = this.editorView.state;
|
2022-10-10 20:50:21 +08:00
|
|
|
const selection = state.selection.main;
|
2022-04-21 17:46:33 +08:00
|
|
|
if (selection.empty) {
|
2022-12-16 23:35:05 +08:00
|
|
|
return syntaxTree(state).resolveInner(selection.from).type.name;
|
2022-04-21 17:46:33 +08:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2023-12-27 20:38:38 +08:00
|
|
|
|
2023-12-28 23:14:30 +08:00
|
|
|
// Widget and image height caching
|
|
|
|
private widgetCache = new LimitedMap<WidgetCacheItem>(100); // bodyText -> WidgetCacheItem
|
|
|
|
private widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
|
|
|
|
|
|
|
|
async loadCaches() {
|
2024-01-02 18:32:57 +08:00
|
|
|
const [widgetHeightCache, widgetCache] = await this
|
|
|
|
.stateDataStore.batchGet([[
|
2023-12-28 23:14:30 +08:00
|
|
|
"cache",
|
|
|
|
"widgetHeight",
|
|
|
|
], ["cache", "widgets"]]);
|
|
|
|
this.widgetHeightCache = new LimitedMap(100, widgetHeightCache || {});
|
|
|
|
this.widgetCache = new LimitedMap(100, widgetCache || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
debouncedWidgetHeightCacheFlush = throttle(() => {
|
|
|
|
this.stateDataStore.set(
|
|
|
|
["cache", "widgetHeight"],
|
|
|
|
this.widgetHeightCache.toJSON(),
|
|
|
|
)
|
|
|
|
.catch(
|
|
|
|
console.error,
|
|
|
|
);
|
|
|
|
// console.log("Flushed widget height cache to store");
|
2023-12-30 03:03:54 +08:00
|
|
|
}, 2000);
|
2023-12-28 23:14:30 +08:00
|
|
|
|
|
|
|
setCachedWidgetHeight(bodyText: string, height: number) {
|
|
|
|
this.widgetHeightCache.set(bodyText, height);
|
|
|
|
this.debouncedWidgetHeightCacheFlush();
|
|
|
|
}
|
|
|
|
getCachedWidgetHeight(bodyText: string): number {
|
|
|
|
return this.widgetHeightCache.get(bodyText) ?? -1;
|
|
|
|
}
|
2023-12-27 20:38:38 +08:00
|
|
|
|
|
|
|
debouncedWidgetCacheFlush = throttle(() => {
|
|
|
|
this.stateDataStore.set(["cache", "widgets"], this.widgetCache.toJSON())
|
|
|
|
.catch(
|
|
|
|
console.error,
|
|
|
|
);
|
|
|
|
console.log("Flushed widget cache to store");
|
2023-12-30 03:03:54 +08:00
|
|
|
}, 2000);
|
2023-12-27 20:38:38 +08:00
|
|
|
|
2023-12-28 23:14:30 +08:00
|
|
|
setWidgetCache(key: string, cacheItem: WidgetCacheItem) {
|
|
|
|
this.widgetCache.set(key, cacheItem);
|
2023-12-27 20:38:38 +08:00
|
|
|
this.debouncedWidgetCacheFlush();
|
|
|
|
}
|
|
|
|
|
|
|
|
getWidgetCache(key: string): WidgetCacheItem | undefined {
|
|
|
|
return this.widgetCache.get(key);
|
|
|
|
}
|
2022-03-20 16:56:28 +08:00
|
|
|
}
|