diff --git a/common/spaces/fallback_space_primitives.ts b/common/spaces/fallback_space_primitives.ts index 08df744..9b0ef9d 100644 --- a/common/spaces/fallback_space_primitives.ts +++ b/common/spaces/fallback_space_primitives.ts @@ -17,14 +17,19 @@ export class FallbackSpacePrimitives implements SpacePrimitives { fetchFileList(): Promise { 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 diff --git a/common/spaces/http_space_primitives.ts b/common/spaces/http_space_primitives.ts index ac37af1..0d80d58 100644 --- a/common/spaces/http_space_primitives.ts +++ b/common/spaces/http_space_primitives.ts @@ -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 { diff --git a/web/client.ts b/web/client.ts index 6933072..58dd87b 100644 --- a/web/client.ts +++ b/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 { 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 { 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 { 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; diff --git a/web/cm_plugins/admonition.ts b/web/cm_plugins/admonition.ts index f176427..3e206a3 100644 --- a/web/cm_plugins/admonition.ts +++ b/web/cm_plugins/admonition.ts @@ -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), diff --git a/web/cm_plugins/command_link.ts b/web/cm_plugins/command_link.ts index cfb0eab..097e081 100644 --- a/web/cm_plugins/command_link.ts +++ b/web/cm_plugins/command_link.ts @@ -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 }, }); } diff --git a/web/cm_plugins/editor_paste.ts b/web/cm_plugins/editor_paste.ts index c436a3a..d739f53 100644 --- a/web/cm_plugins/editor_paste.ts +++ b/web/cm_plugins/editor_paste.ts @@ -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, }, ], }); diff --git a/web/cm_plugins/fenced_code.ts b/web/cm_plugins/fenced_code.ts index 77dab6a..8fcbba1 100644 --- a/web/cm_plugins/fenced_code.ts +++ b/web/cm_plugins/fenced_code.ts @@ -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(); diff --git a/web/cm_plugins/table.ts b/web/cm_plugins/table.ts index b1798bb..2f20698 100644 --- a/web/cm_plugins/table.ts +++ b/web/cm_plugins/table.ts @@ -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, }, diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index a0e3596..6fa5f90 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -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 }, }); } diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index f25ba62..a0ed573 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -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(); diff --git a/web/hooks/slash_command.ts b/web/hooks/slash_command.ts index 96026aa..b074287 100644 --- a/web/hooks/slash_command.ts +++ b/web/hooks/slash_command.ts @@ -75,7 +75,7 @@ export class SlashCommandHook implements Hook { 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, diff --git a/web/service_worker.ts b/web/service_worker.ts index 3e9df4d..6e48a17 100644 --- a/web/service_worker.ts +++ b/web/service_worker.ts @@ -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 + }); + }), ); }); diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index 3ba6a1c..6a37f86 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -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); }, };