Refactoring of offline handling
This commit is contained in:
parent
7b8d8af2c1
commit
4d0f36d475
@ -17,14 +17,19 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
|
||||
fetchFileList(): Promise<FileMeta[]> {
|
||||
return this.primary.fetchFileList();
|
||||
}
|
||||
|
||||
async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
|
||||
try {
|
||||
return await this.primary.readFile(name);
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Could not read file ${name} from primary, trying fallback`,
|
||||
e,
|
||||
);
|
||||
try {
|
||||
return this.fallback.readFile(name);
|
||||
} catch (fallbackError) {
|
||||
console.error("Error during reaFile fallback", fallbackError);
|
||||
return await this.fallback.readFile(name);
|
||||
} catch (fallbackError: any) {
|
||||
console.error("Error during readFile fallback", fallbackError);
|
||||
// Fallback failed, so let's throw the original error
|
||||
throw e;
|
||||
}
|
||||
@ -34,8 +39,12 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
|
||||
try {
|
||||
return await this.primary.getFileMeta(name);
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Could not fetch file ${name} metadata from primary, trying fallback`,
|
||||
e,
|
||||
);
|
||||
try {
|
||||
return this.fallback.getFileMeta(name);
|
||||
return await this.fallback.getFileMeta(name);
|
||||
} catch (fallbackError) {
|
||||
console.error("Error during getFileMeta fallback", fallbackError);
|
||||
// Fallback failed, so let's throw the original error
|
||||
|
@ -21,17 +21,35 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
||||
options.headers = { ...options.headers, ...{ "X-Sync-Mode": "true" } };
|
||||
}
|
||||
|
||||
const result = await fetch(url, options);
|
||||
if (result.redirected) {
|
||||
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
|
||||
console.log(
|
||||
"Got a redirect via the API so will redirect to URL",
|
||||
result.url,
|
||||
);
|
||||
location.href = result.url;
|
||||
throw new Error("Invalid credentials");
|
||||
try {
|
||||
const result = await fetch(url, options);
|
||||
if (result.status === 503) {
|
||||
throw new Error("Offline");
|
||||
}
|
||||
if (result.redirected) {
|
||||
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
|
||||
console.log(
|
||||
"Got a redirect via the API so will redirect to URL",
|
||||
result.url,
|
||||
);
|
||||
location.href = result.url;
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
// Firefox: NetworkError when attempting to fetch resource (with SW and without)
|
||||
// Safari: FetchEvent.respondWith received an error: TypeError: Load failed (service worker)
|
||||
// Safari: Load failed (no service worker)
|
||||
// Chrome: Failed to fetch (with service worker and without)
|
||||
// Common substrings: "fetch" "load failed"
|
||||
const errorMessage = e.message.toLowerCase();
|
||||
if (
|
||||
errorMessage.includes("fetch") || errorMessage.includes("load failed")
|
||||
) {
|
||||
throw new Error("Offline");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchFileList(): Promise<FileMeta[]> {
|
||||
|
322
web/client.ts
322
web/client.ts
@ -49,29 +49,31 @@ declare global {
|
||||
|
||||
// TODO: Oh my god, need to refactor this
|
||||
export class Client {
|
||||
editorView?: EditorView;
|
||||
pageNavigator?: PathPageNavigator;
|
||||
system: ClientSystem;
|
||||
|
||||
space: Space;
|
||||
editorView: EditorView;
|
||||
|
||||
remoteSpacePrimitives: HttpSpacePrimitives;
|
||||
plugSpaceRemotePrimitives: PlugSpacePrimitives;
|
||||
private pageNavigator!: PathPageNavigator;
|
||||
|
||||
private dbPrefix: string;
|
||||
|
||||
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
|
||||
localSpacePrimitives!: FilteredSpacePrimitives;
|
||||
remoteSpacePrimitives!: HttpSpacePrimitives;
|
||||
space!: Space;
|
||||
|
||||
saveTimeout?: number;
|
||||
|
||||
debouncedUpdateEvent = throttle(() => {
|
||||
this.eventHook
|
||||
.dispatchEvent("editor:updated")
|
||||
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
||||
}, 1000);
|
||||
system: ClientSystem;
|
||||
|
||||
// Track if plugs have been updated since sync cycle
|
||||
fullSyncCompleted = false;
|
||||
|
||||
// Runtime state (that doesn't make sense in viewState)
|
||||
syncService: SyncService;
|
||||
settings?: BuiltinSettings;
|
||||
settings!: BuiltinSettings;
|
||||
kvStore: DexieKVStore;
|
||||
|
||||
// Event bus used to communicate between components
|
||||
@ -83,13 +85,11 @@ export class Client {
|
||||
constructor(
|
||||
parent: Element,
|
||||
) {
|
||||
const runtimeConfig = window.silverBulletConfig;
|
||||
|
||||
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
|
||||
const dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
||||
this.dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
||||
|
||||
this.kvStore = new DexieKVStore(
|
||||
`${dbPrefix}_store`,
|
||||
`${this.dbPrefix}_store`,
|
||||
"data",
|
||||
globalThis.indexedDB,
|
||||
);
|
||||
@ -101,55 +101,14 @@ export class Client {
|
||||
this.system = new ClientSystem(
|
||||
this,
|
||||
this.kvStore,
|
||||
dbPrefix,
|
||||
this.dbPrefix,
|
||||
this.eventHook,
|
||||
);
|
||||
|
||||
// Setup space
|
||||
this.remoteSpacePrimitives = new HttpSpacePrimitives(
|
||||
location.origin,
|
||||
runtimeConfig.spaceFolderPath,
|
||||
true,
|
||||
);
|
||||
|
||||
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
||||
this.remoteSpacePrimitives,
|
||||
this.system.namespaceHook,
|
||||
);
|
||||
|
||||
let fileFilterFn: (s: string) => boolean = () => true;
|
||||
|
||||
const localSpacePrimitives = new FilteredSpacePrimitives(
|
||||
new FileMetaSpacePrimitives(
|
||||
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(
|
||||
new IndexedDBSpacePrimitives(
|
||||
`${dbPrefix}_space`,
|
||||
globalThis.indexedDB,
|
||||
),
|
||||
this.plugSpaceRemotePrimitives,
|
||||
),
|
||||
this.eventHook,
|
||||
),
|
||||
this.system.indexSyscalls,
|
||||
),
|
||||
(meta) => fileFilterFn(meta.name),
|
||||
async () => {
|
||||
await this.loadSettings();
|
||||
if (typeof this.settings?.spaceIgnore === "string") {
|
||||
fileFilterFn = gitIgnoreCompiler(this.settings.spaceIgnore).accepts;
|
||||
} else {
|
||||
fileFilterFn = () => true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.space = new Space(localSpacePrimitives, this.kvStore);
|
||||
this.space.watch();
|
||||
this.initSpace();
|
||||
|
||||
this.syncService = new SyncService(
|
||||
localSpacePrimitives,
|
||||
this.localSpacePrimitives,
|
||||
this.plugSpaceRemotePrimitives,
|
||||
this.kvStore,
|
||||
this.eventHook,
|
||||
@ -172,105 +131,33 @@ export class Client {
|
||||
});
|
||||
|
||||
this.openPages = new OpenPages(this.editorView);
|
||||
}
|
||||
|
||||
get currentPage(): string | undefined {
|
||||
return this.ui.viewState.currentPage;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.focus();
|
||||
|
||||
this.space.on({
|
||||
pageChanged: (meta) => {
|
||||
// Only reload when watching the current page (to avoid reloading when switching pages)
|
||||
if (this.space.watchInterval && this.currentPage === meta.name) {
|
||||
console.log("Page changed elsewhere, reloading");
|
||||
this.flashNotification("Page changed elsewhere, reloading");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
pageListUpdated: (pages) => {
|
||||
this.ui.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: pages,
|
||||
});
|
||||
},
|
||||
});
|
||||
// This constructor will always be followed by an invocatition of init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the client
|
||||
* This is a separated from the constructor to allow for async initialization
|
||||
*/
|
||||
async init() {
|
||||
// Load settings
|
||||
this.settings = await this.loadSettings();
|
||||
|
||||
this.pageNavigator = new PathPageNavigator(
|
||||
this.settings.indexPage,
|
||||
);
|
||||
this.initNavigator();
|
||||
|
||||
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
||||
try {
|
||||
await this.remoteSpacePrimitives.getFileMeta("SETTINGS");
|
||||
} catch {
|
||||
console.info(
|
||||
console.warn(
|
||||
"Could not reach remote server, either we're offline or not authenticated",
|
||||
);
|
||||
}
|
||||
|
||||
await this.reloadPlugs();
|
||||
|
||||
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
||||
console.log("Now navigating to", pageName);
|
||||
if (!this.editorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateRestored = await this.loadPage(pageName);
|
||||
if (pos) {
|
||||
if (typeof pos === "string") {
|
||||
console.log("Navigating to anchor", pos);
|
||||
|
||||
// We're going to look up the anchor through a direct page store query...
|
||||
// TODO: This should be extracted
|
||||
const posLookup = await this.system.localSyscall(
|
||||
"index.get",
|
||||
[
|
||||
pageName,
|
||||
`a:${pageName}:${pos}`,
|
||||
],
|
||||
);
|
||||
|
||||
if (!posLookup) {
|
||||
return this.flashNotification(
|
||||
`Could not find anchor @${pos}`,
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
pos = +posLookup;
|
||||
}
|
||||
}
|
||||
this.editorView.dispatch({
|
||||
selection: { anchor: pos },
|
||||
effects: EditorView.scrollIntoView(pos, { y: "start" }),
|
||||
});
|
||||
} else if (!stateRestored) {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.loadCustomStyles().catch(console.error);
|
||||
|
||||
// Kick off background sync
|
||||
@ -319,13 +206,134 @@ export class Client {
|
||||
await this.dispatchAppEvent("editor:init");
|
||||
}
|
||||
|
||||
private initNavigator() {
|
||||
this.pageNavigator = new PathPageNavigator(
|
||||
this.settings.indexPage,
|
||||
);
|
||||
|
||||
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
||||
console.log("Now navigating to", pageName);
|
||||
|
||||
const stateRestored = await this.loadPage(pageName);
|
||||
if (pos) {
|
||||
if (typeof pos === "string") {
|
||||
console.log("Navigating to anchor", pos);
|
||||
|
||||
// We're going to look up the anchor through a direct page store query...
|
||||
// TODO: This should be extracted
|
||||
const posLookup = await this.system.localSyscall(
|
||||
"index.get",
|
||||
[
|
||||
pageName,
|
||||
`a:${pageName}:${pos}`,
|
||||
],
|
||||
);
|
||||
|
||||
if (!posLookup) {
|
||||
return this.flashNotification(
|
||||
`Could not find anchor @${pos}`,
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
pos = +posLookup;
|
||||
}
|
||||
}
|
||||
this.editorView.dispatch({
|
||||
selection: { anchor: pos },
|
||||
effects: EditorView.scrollIntoView(pos, { y: "start" }),
|
||||
});
|
||||
} else if (!stateRestored) {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initSpace() {
|
||||
this.remoteSpacePrimitives = new HttpSpacePrimitives(
|
||||
location.origin,
|
||||
window.silverBulletConfig.spaceFolderPath,
|
||||
true,
|
||||
);
|
||||
|
||||
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
||||
this.remoteSpacePrimitives,
|
||||
this.system.namespaceHook,
|
||||
);
|
||||
|
||||
let fileFilterFn: (s: string) => boolean = () => true;
|
||||
|
||||
this.localSpacePrimitives = new FilteredSpacePrimitives(
|
||||
new FileMetaSpacePrimitives(
|
||||
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(
|
||||
new IndexedDBSpacePrimitives(
|
||||
`${this.dbPrefix}_space`,
|
||||
globalThis.indexedDB,
|
||||
),
|
||||
this.plugSpaceRemotePrimitives,
|
||||
),
|
||||
this.eventHook,
|
||||
),
|
||||
this.system.indexSyscalls,
|
||||
),
|
||||
(meta) => fileFilterFn(meta.name),
|
||||
async () => {
|
||||
// Run when a list of files has been retrieved
|
||||
await this.loadSettings();
|
||||
if (typeof this.settings?.spaceIgnore === "string") {
|
||||
fileFilterFn = gitIgnoreCompiler(this.settings.spaceIgnore).accepts;
|
||||
} else {
|
||||
fileFilterFn = () => true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.space = new Space(this.localSpacePrimitives, this.kvStore);
|
||||
|
||||
this.space.on({
|
||||
pageChanged: (meta) => {
|
||||
// Only reload when watching the current page (to avoid reloading when switching pages)
|
||||
if (this.space.watchInterval && this.currentPage === meta.name) {
|
||||
console.log("Page changed elsewhere, reloading");
|
||||
this.flashNotification("Page changed elsewhere, reloading");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
pageListUpdated: (pages) => {
|
||||
this.ui.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: pages,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.space.watch();
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<BuiltinSettings> {
|
||||
let settingsText: string | undefined;
|
||||
|
||||
try {
|
||||
settingsText = (await this.space.readPage("SETTINGS")).text;
|
||||
} catch {
|
||||
console.log("No SETTINGS page, falling back to default");
|
||||
} catch (e) {
|
||||
console.info("No SETTINGS page, falling back to default", e);
|
||||
settingsText = "```yaml\nindexPage: index\n```\n";
|
||||
}
|
||||
const settings = parseYamlSettings(settingsText!) as BuiltinSettings;
|
||||
@ -336,6 +344,10 @@ export class Client {
|
||||
return settings;
|
||||
}
|
||||
|
||||
get currentPage(): string | undefined {
|
||||
return this.ui.viewState.currentPage;
|
||||
}
|
||||
|
||||
save(immediate = false): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.saveTimeout) {
|
||||
@ -355,7 +367,7 @@ export class Client {
|
||||
this.space
|
||||
.writePage(
|
||||
this.currentPage,
|
||||
this.editorView!.state.sliceDoc(0),
|
||||
this.editorView.state.sliceDoc(0),
|
||||
true,
|
||||
)
|
||||
.then(async (meta) => {
|
||||
@ -583,7 +595,7 @@ export class Client {
|
||||
// Some other modal UI element is visible, don't focus editor now
|
||||
return;
|
||||
}
|
||||
this.editorView!.focus();
|
||||
this.editorView.focus();
|
||||
}
|
||||
|
||||
async navigate(
|
||||
@ -616,10 +628,6 @@ export class Client {
|
||||
async loadPage(pageName: string): Promise<boolean> {
|
||||
const loadingDifferentPage = pageName !== this.currentPage;
|
||||
const editorView = this.editorView;
|
||||
if (!editorView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousPage = this.currentPage;
|
||||
|
||||
// Persist current page state and nicely close page
|
||||
@ -644,12 +652,20 @@ export class Client {
|
||||
throw new Error("Got HTML page, not markdown");
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Not found, new page
|
||||
console.log("Creating new page", pageName);
|
||||
doc = {
|
||||
text: "",
|
||||
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta,
|
||||
};
|
||||
if (e.message.includes("Not found")) {
|
||||
// Not found, new page
|
||||
console.log("Page doesn't exist, creating new page:", pageName);
|
||||
doc = {
|
||||
text: "",
|
||||
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta,
|
||||
};
|
||||
} else {
|
||||
console.error("Could not load page", pageName, e);
|
||||
doc = {
|
||||
text: `**ERROR**: ${e.message}`,
|
||||
meta: { name: pageName, lastModified: 0, perm: "ro" } as PageMeta,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const editorState = createEditorState(
|
||||
@ -737,7 +753,7 @@ export class Client {
|
||||
}
|
||||
|
||||
getContext(): string | undefined {
|
||||
const state = this.editorView!.state;
|
||||
const state = this.editorView.state;
|
||||
const selection = state.selection.main;
|
||||
if (selection.empty) {
|
||||
return syntaxTree(state).resolveInner(selection.from).type.name;
|
||||
|
@ -170,7 +170,7 @@ export function admonitionPlugin(editor: Client) {
|
||||
widget: new AdmonitionIconWidget(
|
||||
iconRange.from + 1,
|
||||
admonitionType,
|
||||
editor.editorView!,
|
||||
editor.editorView,
|
||||
),
|
||||
inclusive: true,
|
||||
}).range(iconRange.from, iconRange.to),
|
||||
|
@ -49,7 +49,7 @@ export function cleanCommandLinkPlugin(editor: Client) {
|
||||
(e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return editor.editorView!.dispatch({
|
||||
return editor.editorView.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
|
@ -118,15 +118,15 @@ export function attachmentExtension(editor: Client) {
|
||||
// Only do rich text paste if shift is NOT down
|
||||
if (richText && !shiftDown) {
|
||||
// Are we in a fencede code block?
|
||||
const editorText = editor.editorView!.state.sliceDoc();
|
||||
const editorText = editor.editorView.state.sliceDoc();
|
||||
const tree = lezerToParseTree(
|
||||
editorText,
|
||||
syntaxTree(editor.editorView!.state).topNode,
|
||||
syntaxTree(editor.editorView.state).topNode,
|
||||
);
|
||||
addParentPointers(tree);
|
||||
const currentNode = nodeAtPos(
|
||||
tree,
|
||||
editor.editorView!.state.selection.main.from,
|
||||
editor.editorView.state.selection.main.from,
|
||||
);
|
||||
if (currentNode) {
|
||||
const fencedParentNode = findParentMatching(
|
||||
@ -141,7 +141,7 @@ export function attachmentExtension(editor: Client) {
|
||||
|
||||
const markdown = striptHtmlComments(turndownService.turndown(richText))
|
||||
.trim();
|
||||
const view = editor.editorView!;
|
||||
const view = editor.editorView;
|
||||
const selection = view.state.selection.main;
|
||||
view.dispatch({
|
||||
changes: [
|
||||
@ -221,11 +221,11 @@ export function attachmentExtension(editor: Client) {
|
||||
if (mimeType.startsWith("image/")) {
|
||||
attachmentMarkdown = `![](${encodeURI(finalFileName)})`;
|
||||
}
|
||||
editor.editorView!.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
changes: [
|
||||
{
|
||||
insert: attachmentMarkdown,
|
||||
from: editor.editorView!.state.selection.main.from,
|
||||
from: editor.editorView.state.selection.main.from,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ class IFrameWidget extends WidgetType {
|
||||
iframe.style.height = data.height + "px";
|
||||
break;
|
||||
case "setBody":
|
||||
this.editor.editorView!.dispatch({
|
||||
this.editor.editorView.dispatch({
|
||||
changes: {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
@ -50,7 +50,7 @@ class IFrameWidget extends WidgetType {
|
||||
});
|
||||
break;
|
||||
case "blur":
|
||||
this.editor.editorView!.dispatch({
|
||||
this.editor.editorView.dispatch({
|
||||
selection: { anchor: this.from },
|
||||
});
|
||||
this.editor.focus();
|
||||
|
@ -26,7 +26,7 @@ class TableViewWidget extends WidgetType {
|
||||
// Pulling data-pos to put the cursor in the right place, falling back
|
||||
// to the start of the table.
|
||||
const dataAttributes = (e.target as any).dataset;
|
||||
this.editor.editorView!.dispatch({
|
||||
this.editor.editorView.dispatch({
|
||||
selection: {
|
||||
anchor: dataAttributes.pos ? +dataAttributes.pos : this.pos,
|
||||
},
|
||||
|
@ -90,7 +90,7 @@ export function cleanWikiLinkPlugin(editor: Client) {
|
||||
callback: (e) => {
|
||||
if (e.altKey) {
|
||||
// Move cursor into the link
|
||||
return editor.editorView!.dispatch({
|
||||
return editor.editorView.dispatch({
|
||||
selection: { anchor: from + 2 },
|
||||
});
|
||||
}
|
||||
|
@ -26,12 +26,12 @@ export class MainUI {
|
||||
constructor(private editor: Client) {
|
||||
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
|
||||
globalThis.addEventListener("keydown", (ev) => {
|
||||
if (!editor.editorView?.hasFocus) {
|
||||
if (!editor.editorView.hasFocus) {
|
||||
if ((ev.target as any).closest(".cm-editor")) {
|
||||
// In some cm element, let's back out
|
||||
return;
|
||||
}
|
||||
if (runScopeHandlers(editor.editorView!, ev, "editor")) {
|
||||
if (runScopeHandlers(editor.editorView, ev, "editor")) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
@ -70,11 +70,9 @@ export class MainUI {
|
||||
}, [viewState.currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.editorView) {
|
||||
editor.tweakEditorDOM(
|
||||
editor.editorView.contentDOM,
|
||||
);
|
||||
}
|
||||
editor.tweakEditorDOM(
|
||||
editor.editorView.contentDOM,
|
||||
);
|
||||
}, [viewState.uiOptions.forcedROMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -190,7 +188,7 @@ export class MainUI {
|
||||
onRename={async (newName) => {
|
||||
if (!newName) {
|
||||
// Always move cursor to the start of the page
|
||||
editor.editorView?.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
selection: { anchor: 0 },
|
||||
});
|
||||
editor.focus();
|
||||
|
@ -75,7 +75,7 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
||||
boost: def.slashCommand.boost,
|
||||
apply: () => {
|
||||
// Delete slash command part
|
||||
this.editor.editorView?.dispatch({
|
||||
this.editor.editorView.dispatch({
|
||||
changes: {
|
||||
from: prefix!.from + prefixText.indexOf("/"),
|
||||
to: ctx.pos,
|
||||
|
@ -106,7 +106,12 @@ self.addEventListener("fetch", (event: any) => {
|
||||
// Must be a page URL, let's serve index.html which will handle it
|
||||
return (await caches.match(precacheFiles["/"])) || fetch(request);
|
||||
}
|
||||
})(),
|
||||
})().catch((e) => {
|
||||
console.warn("[Service worker]", "Fetch failed:", e);
|
||||
return new Response("Offline", {
|
||||
status: 503, // Service Unavailable
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -19,13 +19,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
return editor.currentPage!;
|
||||
},
|
||||
"editor.getText": () => {
|
||||
return editor.editorView?.state.sliceDoc();
|
||||
return editor.editorView.state.sliceDoc();
|
||||
},
|
||||
"editor.getCursor": (): number => {
|
||||
return editor.editorView!.state.selection.main.from;
|
||||
return editor.editorView.state.selection.main.from;
|
||||
},
|
||||
"editor.getSelection": (): { from: number; to: number } => {
|
||||
return editor.editorView!.state.selection.main;
|
||||
return editor.editorView.state.selection.main;
|
||||
},
|
||||
"editor.save": () => {
|
||||
return editor.save(true);
|
||||
@ -97,7 +97,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
});
|
||||
},
|
||||
"editor.insertAtPos": (_ctx, text: string, pos: number) => {
|
||||
editor.editorView!.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
changes: {
|
||||
insert: text,
|
||||
from: pos,
|
||||
@ -105,7 +105,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
});
|
||||
},
|
||||
"editor.replaceRange": (_ctx, from: number, to: number, text: string) => {
|
||||
editor.editorView!.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
changes: {
|
||||
insert: text,
|
||||
from: from,
|
||||
@ -114,13 +114,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
});
|
||||
},
|
||||
"editor.moveCursor": (_ctx, pos: number, center = false) => {
|
||||
editor.editorView!.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
selection: {
|
||||
anchor: pos,
|
||||
},
|
||||
});
|
||||
if (center) {
|
||||
editor.editorView!.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
effects: [
|
||||
EditorView.scrollIntoView(
|
||||
pos,
|
||||
@ -133,8 +133,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
}
|
||||
},
|
||||
"editor.setSelection": (_ctx, from: number, to: number) => {
|
||||
const editorView = editor.editorView!;
|
||||
editorView.dispatch({
|
||||
editor.editorView.dispatch({
|
||||
selection: {
|
||||
anchor: from,
|
||||
head: to,
|
||||
@ -143,7 +142,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
},
|
||||
|
||||
"editor.insertAtCursor": (_ctx, text: string) => {
|
||||
const editorView = editor.editorView!;
|
||||
const editorView = editor.editorView;
|
||||
const from = editorView.state.selection.main.from;
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
@ -156,7 +155,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
});
|
||||
},
|
||||
"editor.dispatch": (_ctx, change: Transaction) => {
|
||||
editor.editorView!.dispatch(change);
|
||||
editor.editorView.dispatch(change);
|
||||
},
|
||||
"editor.prompt": (
|
||||
_ctx,
|
||||
@ -182,7 +181,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
});
|
||||
},
|
||||
"editor.vimEx": (_ctx, exCommand: string) => {
|
||||
const cm = vimGetCm(editor.editorView!)!;
|
||||
const cm = vimGetCm(editor.editorView)!;
|
||||
return Vim.handleEx(cm, exCommand);
|
||||
},
|
||||
// Sync
|
||||
@ -191,19 +190,19 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
||||
},
|
||||
// Folding
|
||||
"editor.fold": () => {
|
||||
foldCode(editor.editorView!);
|
||||
foldCode(editor.editorView);
|
||||
},
|
||||
"editor.unfold": () => {
|
||||
unfoldCode(editor.editorView!);
|
||||
unfoldCode(editor.editorView);
|
||||
},
|
||||
"editor.toggleFold": () => {
|
||||
toggleFold(editor.editorView!);
|
||||
toggleFold(editor.editorView);
|
||||
},
|
||||
"editor.foldAll": () => {
|
||||
foldAll(editor.editorView!);
|
||||
foldAll(editor.editorView);
|
||||
},
|
||||
"editor.unfoldAll": () => {
|
||||
unfoldAll(editor.editorView!);
|
||||
unfoldAll(editor.editorView);
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user