1
0

Refactoring of offline handling

This commit is contained in:
Zef Hemel 2023-07-27 11:41:44 +02:00
parent 7b8d8af2c1
commit 4d0f36d475
13 changed files with 251 additions and 206 deletions

View File

@ -17,14 +17,19 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
fetchFileList(): Promise<FileMeta[]> { fetchFileList(): Promise<FileMeta[]> {
return this.primary.fetchFileList(); return this.primary.fetchFileList();
} }
async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> { async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
try { try {
return await this.primary.readFile(name); return await this.primary.readFile(name);
} catch (e) { } catch (e) {
console.info(
`Could not read file ${name} from primary, trying fallback`,
e,
);
try { try {
return this.fallback.readFile(name); return await this.fallback.readFile(name);
} catch (fallbackError) { } catch (fallbackError: any) {
console.error("Error during reaFile fallback", fallbackError); console.error("Error during readFile fallback", fallbackError);
// Fallback failed, so let's throw the original error // Fallback failed, so let's throw the original error
throw e; throw e;
} }
@ -34,8 +39,12 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
try { try {
return await this.primary.getFileMeta(name); return await this.primary.getFileMeta(name);
} catch (e) { } catch (e) {
console.info(
`Could not fetch file ${name} metadata from primary, trying fallback`,
e,
);
try { try {
return this.fallback.getFileMeta(name); return await this.fallback.getFileMeta(name);
} catch (fallbackError) { } catch (fallbackError) {
console.error("Error during getFileMeta fallback", fallbackError); console.error("Error during getFileMeta fallback", fallbackError);
// Fallback failed, so let's throw the original error // Fallback failed, so let's throw the original error

View File

@ -21,17 +21,35 @@ export class HttpSpacePrimitives implements SpacePrimitives {
options.headers = { ...options.headers, ...{ "X-Sync-Mode": "true" } }; options.headers = { ...options.headers, ...{ "X-Sync-Mode": "true" } };
} }
const result = await fetch(url, options); try {
if (result.redirected) { const result = await fetch(url, options);
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page if (result.status === 503) {
console.log( throw new Error("Offline");
"Got a redirect via the API so will redirect to URL", }
result.url, if (result.redirected) {
); // Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
location.href = result.url; console.log(
throw new Error("Invalid credentials"); "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[]> { async fetchFileList(): Promise<FileMeta[]> {

View File

@ -49,29 +49,31 @@ declare global {
// TODO: Oh my god, need to refactor this // TODO: Oh my god, need to refactor this
export class Client { export class Client {
editorView?: EditorView; system: ClientSystem;
pageNavigator?: PathPageNavigator;
space: Space; editorView: EditorView;
remoteSpacePrimitives: HttpSpacePrimitives; private pageNavigator!: PathPageNavigator;
plugSpaceRemotePrimitives: PlugSpacePrimitives;
private dbPrefix: string;
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
localSpacePrimitives!: FilteredSpacePrimitives;
remoteSpacePrimitives!: HttpSpacePrimitives;
space!: Space;
saveTimeout?: number; saveTimeout?: number;
debouncedUpdateEvent = throttle(() => { debouncedUpdateEvent = throttle(() => {
this.eventHook this.eventHook
.dispatchEvent("editor:updated") .dispatchEvent("editor:updated")
.catch((e) => console.error("Error dispatching editor:updated event", e)); .catch((e) => console.error("Error dispatching editor:updated event", e));
}, 1000); }, 1000);
system: ClientSystem;
// Track if plugs have been updated since sync cycle // Track if plugs have been updated since sync cycle
fullSyncCompleted = false; fullSyncCompleted = false;
// Runtime state (that doesn't make sense in viewState)
syncService: SyncService; syncService: SyncService;
settings?: BuiltinSettings; settings!: BuiltinSettings;
kvStore: DexieKVStore; kvStore: DexieKVStore;
// Event bus used to communicate between components // Event bus used to communicate between components
@ -83,13 +85,11 @@ export class Client {
constructor( constructor(
parent: Element, parent: Element,
) { ) {
const runtimeConfig = window.silverBulletConfig;
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths // 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( this.kvStore = new DexieKVStore(
`${dbPrefix}_store`, `${this.dbPrefix}_store`,
"data", "data",
globalThis.indexedDB, globalThis.indexedDB,
); );
@ -101,55 +101,14 @@ export class Client {
this.system = new ClientSystem( this.system = new ClientSystem(
this, this,
this.kvStore, this.kvStore,
dbPrefix, this.dbPrefix,
this.eventHook, this.eventHook,
); );
// Setup space this.initSpace();
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.syncService = new SyncService( this.syncService = new SyncService(
localSpacePrimitives, this.localSpacePrimitives,
this.plugSpaceRemotePrimitives, this.plugSpaceRemotePrimitives,
this.kvStore, this.kvStore,
this.eventHook, this.eventHook,
@ -172,105 +131,33 @@ export class Client {
}); });
this.openPages = new OpenPages(this.editorView); this.openPages = new OpenPages(this.editorView);
}
get currentPage(): string | undefined {
return this.ui.viewState.currentPage;
}
async init() {
this.focus(); this.focus();
this.space.on({ // This constructor will always be followed by an invocatition of init()
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,
});
},
});
/**
* Initialize the client
* This is a separated from the constructor to allow for async initialization
*/
async init() {
// Load settings // Load settings
this.settings = await this.loadSettings(); this.settings = await this.loadSettings();
this.pageNavigator = new PathPageNavigator( this.initNavigator();
this.settings.indexPage,
);
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page // Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
try { try {
await this.remoteSpacePrimitives.getFileMeta("SETTINGS"); await this.remoteSpacePrimitives.getFileMeta("SETTINGS");
} catch { } catch {
console.info( console.warn(
"Could not reach remote server, either we're offline or not authenticated", "Could not reach remote server, either we're offline or not authenticated",
); );
} }
await this.reloadPlugs(); 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); this.loadCustomStyles().catch(console.error);
// Kick off background sync // Kick off background sync
@ -319,13 +206,134 @@ export class Client {
await this.dispatchAppEvent("editor:init"); 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> { async loadSettings(): Promise<BuiltinSettings> {
let settingsText: string | undefined; let settingsText: string | undefined;
try { try {
settingsText = (await this.space.readPage("SETTINGS")).text; settingsText = (await this.space.readPage("SETTINGS")).text;
} catch { } catch (e) {
console.log("No SETTINGS page, falling back to default"); console.info("No SETTINGS page, falling back to default", e);
settingsText = "```yaml\nindexPage: index\n```\n"; settingsText = "```yaml\nindexPage: index\n```\n";
} }
const settings = parseYamlSettings(settingsText!) as BuiltinSettings; const settings = parseYamlSettings(settingsText!) as BuiltinSettings;
@ -336,6 +344,10 @@ export class Client {
return settings; return settings;
} }
get currentPage(): string | undefined {
return this.ui.viewState.currentPage;
}
save(immediate = false): Promise<void> { save(immediate = false): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.saveTimeout) { if (this.saveTimeout) {
@ -355,7 +367,7 @@ export class Client {
this.space this.space
.writePage( .writePage(
this.currentPage, this.currentPage,
this.editorView!.state.sliceDoc(0), this.editorView.state.sliceDoc(0),
true, true,
) )
.then(async (meta) => { .then(async (meta) => {
@ -583,7 +595,7 @@ export class Client {
// Some other modal UI element is visible, don't focus editor now // Some other modal UI element is visible, don't focus editor now
return; return;
} }
this.editorView!.focus(); this.editorView.focus();
} }
async navigate( async navigate(
@ -616,10 +628,6 @@ export class Client {
async loadPage(pageName: string): Promise<boolean> { async loadPage(pageName: string): Promise<boolean> {
const loadingDifferentPage = pageName !== this.currentPage; const loadingDifferentPage = pageName !== this.currentPage;
const editorView = this.editorView; const editorView = this.editorView;
if (!editorView) {
return false;
}
const previousPage = this.currentPage; const previousPage = this.currentPage;
// Persist current page state and nicely close page // Persist current page state and nicely close page
@ -644,12 +652,20 @@ export class Client {
throw new Error("Got HTML page, not markdown"); throw new Error("Got HTML page, not markdown");
} }
} catch (e: any) { } catch (e: any) {
// Not found, new page if (e.message.includes("Not found")) {
console.log("Creating new page", pageName); // Not found, new page
doc = { console.log("Page doesn't exist, creating new page:", pageName);
text: "", doc = {
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta, 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( const editorState = createEditorState(
@ -737,7 +753,7 @@ export class Client {
} }
getContext(): string | undefined { getContext(): string | undefined {
const state = this.editorView!.state; const state = this.editorView.state;
const selection = state.selection.main; const selection = state.selection.main;
if (selection.empty) { if (selection.empty) {
return syntaxTree(state).resolveInner(selection.from).type.name; return syntaxTree(state).resolveInner(selection.from).type.name;

View File

@ -170,7 +170,7 @@ export function admonitionPlugin(editor: Client) {
widget: new AdmonitionIconWidget( widget: new AdmonitionIconWidget(
iconRange.from + 1, iconRange.from + 1,
admonitionType, admonitionType,
editor.editorView!, editor.editorView,
), ),
inclusive: true, inclusive: true,
}).range(iconRange.from, iconRange.to), }).range(iconRange.from, iconRange.to),

View File

@ -49,7 +49,7 @@ export function cleanCommandLinkPlugin(editor: Client) {
(e) => { (e) => {
if (e.altKey) { if (e.altKey) {
// Move cursor into the link // Move cursor into the link
return editor.editorView!.dispatch({ return editor.editorView.dispatch({
selection: { anchor: from + 2 }, selection: { anchor: from + 2 },
}); });
} }

View File

@ -118,15 +118,15 @@ export function attachmentExtension(editor: Client) {
// Only do rich text paste if shift is NOT down // Only do rich text paste if shift is NOT down
if (richText && !shiftDown) { if (richText && !shiftDown) {
// Are we in a fencede code block? // Are we in a fencede code block?
const editorText = editor.editorView!.state.sliceDoc(); const editorText = editor.editorView.state.sliceDoc();
const tree = lezerToParseTree( const tree = lezerToParseTree(
editorText, editorText,
syntaxTree(editor.editorView!.state).topNode, syntaxTree(editor.editorView.state).topNode,
); );
addParentPointers(tree); addParentPointers(tree);
const currentNode = nodeAtPos( const currentNode = nodeAtPos(
tree, tree,
editor.editorView!.state.selection.main.from, editor.editorView.state.selection.main.from,
); );
if (currentNode) { if (currentNode) {
const fencedParentNode = findParentMatching( const fencedParentNode = findParentMatching(
@ -141,7 +141,7 @@ export function attachmentExtension(editor: Client) {
const markdown = striptHtmlComments(turndownService.turndown(richText)) const markdown = striptHtmlComments(turndownService.turndown(richText))
.trim(); .trim();
const view = editor.editorView!; const view = editor.editorView;
const selection = view.state.selection.main; const selection = view.state.selection.main;
view.dispatch({ view.dispatch({
changes: [ changes: [
@ -221,11 +221,11 @@ export function attachmentExtension(editor: Client) {
if (mimeType.startsWith("image/")) { if (mimeType.startsWith("image/")) {
attachmentMarkdown = `![](${encodeURI(finalFileName)})`; attachmentMarkdown = `![](${encodeURI(finalFileName)})`;
} }
editor.editorView!.dispatch({ editor.editorView.dispatch({
changes: [ changes: [
{ {
insert: attachmentMarkdown, insert: attachmentMarkdown,
from: editor.editorView!.state.selection.main.from, from: editor.editorView.state.selection.main.from,
}, },
], ],
}); });

View File

@ -41,7 +41,7 @@ class IFrameWidget extends WidgetType {
iframe.style.height = data.height + "px"; iframe.style.height = data.height + "px";
break; break;
case "setBody": case "setBody":
this.editor.editorView!.dispatch({ this.editor.editorView.dispatch({
changes: { changes: {
from: this.from, from: this.from,
to: this.to, to: this.to,
@ -50,7 +50,7 @@ class IFrameWidget extends WidgetType {
}); });
break; break;
case "blur": case "blur":
this.editor.editorView!.dispatch({ this.editor.editorView.dispatch({
selection: { anchor: this.from }, selection: { anchor: this.from },
}); });
this.editor.focus(); this.editor.focus();

View File

@ -26,7 +26,7 @@ class TableViewWidget extends WidgetType {
// Pulling data-pos to put the cursor in the right place, falling back // Pulling data-pos to put the cursor in the right place, falling back
// to the start of the table. // to the start of the table.
const dataAttributes = (e.target as any).dataset; const dataAttributes = (e.target as any).dataset;
this.editor.editorView!.dispatch({ this.editor.editorView.dispatch({
selection: { selection: {
anchor: dataAttributes.pos ? +dataAttributes.pos : this.pos, anchor: dataAttributes.pos ? +dataAttributes.pos : this.pos,
}, },

View File

@ -90,7 +90,7 @@ export function cleanWikiLinkPlugin(editor: Client) {
callback: (e) => { callback: (e) => {
if (e.altKey) { if (e.altKey) {
// Move cursor into the link // Move cursor into the link
return editor.editorView!.dispatch({ return editor.editorView.dispatch({
selection: { anchor: from + 2 }, selection: { anchor: from + 2 },
}); });
} }

View File

@ -26,12 +26,12 @@ export class MainUI {
constructor(private editor: Client) { constructor(private editor: Client) {
// Make keyboard shortcuts work even when the editor is in read only mode or not focused // Make keyboard shortcuts work even when the editor is in read only mode or not focused
globalThis.addEventListener("keydown", (ev) => { globalThis.addEventListener("keydown", (ev) => {
if (!editor.editorView?.hasFocus) { if (!editor.editorView.hasFocus) {
if ((ev.target as any).closest(".cm-editor")) { if ((ev.target as any).closest(".cm-editor")) {
// In some cm element, let's back out // In some cm element, let's back out
return; return;
} }
if (runScopeHandlers(editor.editorView!, ev, "editor")) { if (runScopeHandlers(editor.editorView, ev, "editor")) {
ev.preventDefault(); ev.preventDefault();
} }
} }
@ -70,11 +70,9 @@ export class MainUI {
}, [viewState.currentPage]); }, [viewState.currentPage]);
useEffect(() => { useEffect(() => {
if (editor.editorView) { editor.tweakEditorDOM(
editor.tweakEditorDOM( editor.editorView.contentDOM,
editor.editorView.contentDOM, );
);
}
}, [viewState.uiOptions.forcedROMode]); }, [viewState.uiOptions.forcedROMode]);
useEffect(() => { useEffect(() => {
@ -190,7 +188,7 @@ export class MainUI {
onRename={async (newName) => { onRename={async (newName) => {
if (!newName) { if (!newName) {
// Always move cursor to the start of the page // Always move cursor to the start of the page
editor.editorView?.dispatch({ editor.editorView.dispatch({
selection: { anchor: 0 }, selection: { anchor: 0 },
}); });
editor.focus(); editor.focus();

View File

@ -75,7 +75,7 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
boost: def.slashCommand.boost, boost: def.slashCommand.boost,
apply: () => { apply: () => {
// Delete slash command part // Delete slash command part
this.editor.editorView?.dispatch({ this.editor.editorView.dispatch({
changes: { changes: {
from: prefix!.from + prefixText.indexOf("/"), from: prefix!.from + prefixText.indexOf("/"),
to: ctx.pos, to: ctx.pos,

View File

@ -106,7 +106,12 @@ self.addEventListener("fetch", (event: any) => {
// Must be a page URL, let's serve index.html which will handle it // Must be a page URL, let's serve index.html which will handle it
return (await caches.match(precacheFiles["/"])) || fetch(request); return (await caches.match(precacheFiles["/"])) || fetch(request);
} }
})(), })().catch((e) => {
console.warn("[Service worker]", "Fetch failed:", e);
return new Response("Offline", {
status: 503, // Service Unavailable
});
}),
); );
}); });

View File

@ -19,13 +19,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
return editor.currentPage!; return editor.currentPage!;
}, },
"editor.getText": () => { "editor.getText": () => {
return editor.editorView?.state.sliceDoc(); return editor.editorView.state.sliceDoc();
}, },
"editor.getCursor": (): number => { "editor.getCursor": (): number => {
return editor.editorView!.state.selection.main.from; return editor.editorView.state.selection.main.from;
}, },
"editor.getSelection": (): { from: number; to: number } => { "editor.getSelection": (): { from: number; to: number } => {
return editor.editorView!.state.selection.main; return editor.editorView.state.selection.main;
}, },
"editor.save": () => { "editor.save": () => {
return editor.save(true); return editor.save(true);
@ -97,7 +97,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}); });
}, },
"editor.insertAtPos": (_ctx, text: string, pos: number) => { "editor.insertAtPos": (_ctx, text: string, pos: number) => {
editor.editorView!.dispatch({ editor.editorView.dispatch({
changes: { changes: {
insert: text, insert: text,
from: pos, from: pos,
@ -105,7 +105,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}); });
}, },
"editor.replaceRange": (_ctx, from: number, to: number, text: string) => { "editor.replaceRange": (_ctx, from: number, to: number, text: string) => {
editor.editorView!.dispatch({ editor.editorView.dispatch({
changes: { changes: {
insert: text, insert: text,
from: from, from: from,
@ -114,13 +114,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}); });
}, },
"editor.moveCursor": (_ctx, pos: number, center = false) => { "editor.moveCursor": (_ctx, pos: number, center = false) => {
editor.editorView!.dispatch({ editor.editorView.dispatch({
selection: { selection: {
anchor: pos, anchor: pos,
}, },
}); });
if (center) { if (center) {
editor.editorView!.dispatch({ editor.editorView.dispatch({
effects: [ effects: [
EditorView.scrollIntoView( EditorView.scrollIntoView(
pos, pos,
@ -133,8 +133,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
} }
}, },
"editor.setSelection": (_ctx, from: number, to: number) => { "editor.setSelection": (_ctx, from: number, to: number) => {
const editorView = editor.editorView!; editor.editorView.dispatch({
editorView.dispatch({
selection: { selection: {
anchor: from, anchor: from,
head: to, head: to,
@ -143,7 +142,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}, },
"editor.insertAtCursor": (_ctx, text: string) => { "editor.insertAtCursor": (_ctx, text: string) => {
const editorView = editor.editorView!; const editorView = editor.editorView;
const from = editorView.state.selection.main.from; const from = editorView.state.selection.main.from;
editorView.dispatch({ editorView.dispatch({
changes: { changes: {
@ -156,7 +155,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}); });
}, },
"editor.dispatch": (_ctx, change: Transaction) => { "editor.dispatch": (_ctx, change: Transaction) => {
editor.editorView!.dispatch(change); editor.editorView.dispatch(change);
}, },
"editor.prompt": ( "editor.prompt": (
_ctx, _ctx,
@ -182,7 +181,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}); });
}, },
"editor.vimEx": (_ctx, exCommand: string) => { "editor.vimEx": (_ctx, exCommand: string) => {
const cm = vimGetCm(editor.editorView!)!; const cm = vimGetCm(editor.editorView)!;
return Vim.handleEx(cm, exCommand); return Vim.handleEx(cm, exCommand);
}, },
// Sync // Sync
@ -191,19 +190,19 @@ export function editorSyscalls(editor: Client): SysCallMapping {
}, },
// Folding // Folding
"editor.fold": () => { "editor.fold": () => {
foldCode(editor.editorView!); foldCode(editor.editorView);
}, },
"editor.unfold": () => { "editor.unfold": () => {
unfoldCode(editor.editorView!); unfoldCode(editor.editorView);
}, },
"editor.toggleFold": () => { "editor.toggleFold": () => {
toggleFold(editor.editorView!); toggleFold(editor.editorView);
}, },
"editor.foldAll": () => { "editor.foldAll": () => {
foldAll(editor.editorView!); foldAll(editor.editorView);
}, },
"editor.unfoldAll": () => { "editor.unfoldAll": () => {
unfoldAll(editor.editorView!); unfoldAll(editor.editorView);
}, },
}; };