diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5fd185c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9ee3ee3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2fd7c5e..d20ad76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# vscode +.vscode + # Intellij *.iml .idea @@ -12,3 +15,6 @@ main.js # obsidian data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b973752 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +tag-version-prefix="" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1f2bdb5..68cdcab 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Daniel Barenholz +Copyright (c) 2022-2023 Daniel Barenholz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 34f1b19..9c26fbc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Plaintext for Obsidian This is an [Obisidan](https://obsidian.md) plugin that allows you to open _any_ file as plaintext. -It has been developed for Obsidian **v0.13.14**, and tested on **Windows**. +It has been developed for Obsidian **v1.3.5**, and tested on **Windows**. Honestly, as long as you can run any Obsidian version you can _probably_ run this plugin as well. The only requirements are that we can register extensions (this existed in v0.12.12 for instance), and that the `viewRegistry` exists, which I'm assuming has been there since the beginning of Obsidian. @@ -17,7 +17,7 @@ Since 0.2.0 by default you can no longer accidentally break other plugins with v ## Installing Interested in editing files in Obsidian? Great. -Grab the latest release from the [releases](#) page, and copy `main.js` and `manifest.json` to `.obsidian/plugins/obsidian-plaintext/`. That's it! +Grab the latest release from the [releases](https://github.com/dbarenholz/obsidian-plaintext/releases) page, and copy `main.js` and `manifest.json` to `.obsidian/plugins/obsidian-plaintext/`. That's it! You can also install the plugin through Obsidian by searching for **plaintext**. @@ -35,25 +35,33 @@ This is free. Keep your money, I don't want it. ## Changelog -**Version 0.2.0 (current)**: +**Version 0.3.0 (current)**: -- Long overdue: plugin is enabled for mobile! -- Extra protection: by default, extensions that other plugins add (such as .csv) are not allowed anymore! -- We have over 500 downloads! +- Rewrite to use CM6 in stead of CM5. A first step towards https://github.com/dbarenholz/obsidian-plaintext/issues/1. +- Fixed https://github.com/dbarenholz/obsidian-plaintext/issues/5 by upgrading to CM6. +- Fixed https://github.com/dbarenholz/obsidian-plaintext/issues/11 - shame on me for getting the logic wrong. +- Possibly implement https://github.com/dbarenholz/obsidian-plaintext/issues/7? I don't have Obsidian on my phone, so let me know :). + + +**Version 0.2.0**: + +- Long overdue: plugin is enabled for mobile! +- Extra protection: by default, extensions that other plugins add (such as .csv) are not allowed anymore! +- We have over 500 downloads! **Version 0.1.0**: -- Complete rewrite of registering and deregistering. -- Now _actually_ removes views when deregistering a particular extension. -- Correctly filters out default obsidian extensions: No more accidentally overwriting the default markdown editor. +- Complete rewrite of registering and deregistering. +- Now _actually_ removes views when deregistering a particular extension. +- Correctly filters out default obsidian extensions: No more accidentally overwriting the default markdown editor. **Version 0.0.2**: -- First actual release. -- Code is functional! You can open and edit files as plaintext. +- First actual release. +- Code is functional! You can open and edit files as plaintext. **Version 0.0.1**: -- Not a release. -- Initial testing code. -- This included the functionality for parsing user-inputted extensions. +- Not a release. +- Initial testing code. +- This included the functionality for parsing user-inputted extensions. diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..c69c848 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,60 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = process.argv[2] === "production"; + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["./src/main.ts"], + bundle: true, + external: [ + "obsidian", + "codemirror", + "@codemirror/autocomplete", + "@codemirror/closebrackets", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/comment", + "@codemirror/fold", + "@codemirror/gutter", + "@codemirror/highlight", + "@codemirror/history", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/matchbrackets", + "@codemirror/panel", + "@codemirror/rangeset", + "@codemirror/rectangular-selection", + "@codemirror/search", + "@codemirror/state", + "@codemirror/stream-parser", + "@codemirror/text", + "@codemirror/tooltip", + "@codemirror/view", + "@lezer/common", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/manifest.json b/manifest.json index 6b73234..1106f3c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "obsidian-plaintext", "name": "Plaintext", - "version": "0.2.0", - "minAppVersion": "0.13.14", - "description": "Allow opening specified files as plaintext (RAW mode).", + "version": "0.3.0", + "minAppVersion": "0.15.9", + "description": "Open any file as if it was plaintext directly in Obsidian.", "author": "dbarenholz", "authorUrl": "https://github.com/dbarenholz/dbarenholz", "isDesktopOnly": false diff --git a/package.json b/package.json index ce220b9..38d9d6a 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,26 @@ { "name": "obsidian-plaintext", - "version": "0.2.0", - "description": "Allow opening specified files as plaintext.", + "version": "0.3.0", + "description": "Open any file as if it was plaintext directly in Obsidian.", "main": "main.js", "scripts": { - "dev": "rollup --config rollup.config.js -w", - "build": "rollup --config rollup.config.js --environment BUILD:production" + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], "author": "dbarenholz", "license": "MIT", "dependencies": { - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-node-resolve": "^13.1.1", - "@rollup/plugin-typescript": "^8.3.0", - "@types/node": "^17.0.4", - "codemirror": "^5.65.0", - "obsidian": "^0.13.11", - "rollup": "^2.62.0", - "tslib": "^2.3.1", - "typescript": "^4.5.4" + "@codemirror/lang-python": "^6.1.2", + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "codemirror": "^6.0.1", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" } } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 0ca1a3f..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import typescript from "@rollup/plugin-typescript"; -import { nodeResolve } from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; - -const isProd = process.env.BUILD === "production"; - -const banner = `/* -THIS IS A GENERATED/BUNDLED FILE BY ROLLUP -if you want to view the source visit the plugins github repository -*/ -`; - -export default { - input: "./src/main.ts", - output: { - dir: ".", - sourcemap: "inline", - sourcemapExcludeSources: isProd, - format: "cjs", - exports: "default", - banner, - }, - external: ["obsidian"], - plugins: [typescript(), nodeResolve({ browser: true }), commonjs()], -}; diff --git a/src/codemirror.ts b/src/codemirror.ts new file mode 100644 index 0000000..74f9f6c --- /dev/null +++ b/src/codemirror.ts @@ -0,0 +1,221 @@ +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, +} from "@codemirror/commands"; +import { + bracketMatching, + foldGutter, + foldKeymap, + indentOnInput, +} from "@codemirror/language"; +import { EditorState, Extension, Compartment } from "@codemirror/state"; +import { dropCursor, EditorView, keymap } from "@codemirror/view"; +import { + closeBrackets, + closeBracketsKeymap, + completionKeymap, +} from "@codemirror/autocomplete"; +import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; +import { lintKeymap } from "@codemirror/lint"; +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { tags as t } from "@lezer/highlight"; + +import { python } from "@codemirror/lang-python"; + +const codemirrorConfig = { + name: "obsidian", + dark: false, + background: "var(--background-primary)", + foreground: "var(--text-normal)", + selection: "var(--text-selection)", + cursor: "var(--text-normal)", + dropdownBackground: "var(--background-primary)", + dropdownBorder: "var(--background-modifier-border)", + activeLine: "var(--background-primary)", + matchingBracket: "var(--background-modifier-accent)", + keyword: "#d73a49", + storage: "#d73a49", + variable: "var(--text-normal)", + parameter: "var(--text-accent-hover)", + function: "var(--text-accent-hover)", + string: "var(--text-accent)", + constant: "var(--text-accent-hover)", + type: "var(--text-accent-hover)", + class: "#6f42c1", + number: "var(--text-accent-hover)", + comment: "var(--text-faint)", + invalid: "var(--text-error)", + regexp: "#032f62", +}; + +const obsidianTheme = EditorView.theme( + { + "&": { + color: codemirrorConfig.foreground, + backgroundColor: codemirrorConfig.background, + }, + + ".cm-content": { caretColor: codemirrorConfig.cursor }, + + "&.cm-focused .cm-cursor": { borderLeftColor: codemirrorConfig.cursor }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, & ::selection": + { + backgroundColor: codemirrorConfig.selection, + }, + + ".cm-panels": { + backgroundColor: codemirrorConfig.dropdownBackground, + color: codemirrorConfig.foreground, + }, + ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, + ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, + + ".cm-searchMatch": { + backgroundColor: codemirrorConfig.dropdownBackground, + outline: `1px solid ${codemirrorConfig.dropdownBorder}`, + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: codemirrorConfig.selection, + }, + + ".cm-activeLine": { backgroundColor: codemirrorConfig.activeLine }, + ".cm-activeLineGutter": { + backgroundColor: codemirrorConfig.background, + }, + ".cm-selectionMatch": { backgroundColor: codemirrorConfig.selection }, + + ".cm-matchingBracket, .cm-nonmatchingBracket": { + backgroundColor: codemirrorConfig.matchingBracket, + outline: "none", + }, + ".cm-gutters": { + backgroundColor: codemirrorConfig.background, + color: codemirrorConfig.comment, + borderRight: "1px solid var(--background-modifier-border)", + }, + ".cm-lineNumbers, .cm-gutterElement": { color: "inherit" }, + + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", + color: codemirrorConfig.foreground, + }, + + ".cm-tooltip": { + border: `1px solid ${codemirrorConfig.dropdownBorder}`, + backgroundColor: codemirrorConfig.dropdownBackground, + color: codemirrorConfig.foreground, + }, + ".cm-tooltip.cm-tooltip-autocomplete": { + "& > ul > li[aria-selected]": { + background: codemirrorConfig.selection, + color: codemirrorConfig.foreground, + }, + }, + }, + { dark: codemirrorConfig.dark } +); + +const obsidianHighlightStyle = HighlightStyle.define([ + { tag: t.keyword, color: codemirrorConfig.keyword }, + { + tag: [t.name, t.deleted, t.character, t.macroName], + color: codemirrorConfig.variable, + }, + { tag: [t.propertyName], color: codemirrorConfig.function }, + { + tag: [ + t.processingInstruction, + t.string, + t.inserted, + t.special(t.string), + ], + color: codemirrorConfig.string, + }, + { + tag: [t.function(t.variableName), t.labelName], + color: codemirrorConfig.function, + }, + { + tag: [t.color, t.constant(t.name), t.standard(t.name)], + color: codemirrorConfig.constant, + }, + { + tag: [t.definition(t.name), t.separator], + color: codemirrorConfig.variable, + }, + { tag: [t.className], color: codemirrorConfig.class }, + { + tag: [ + t.number, + t.changed, + t.annotation, + t.modifier, + t.self, + t.namespace, + ], + color: codemirrorConfig.number, + }, + { + tag: [t.typeName], + color: codemirrorConfig.type, + fontStyle: codemirrorConfig.type, + }, + { tag: [t.operator, t.operatorKeyword], color: codemirrorConfig.keyword }, + { + tag: [t.url, t.escape, t.regexp, t.link], + color: codemirrorConfig.regexp, + }, + { tag: [t.meta, t.comment], color: codemirrorConfig.comment }, + { + tag: [t.atom, t.bool, t.special(t.variableName)], + color: codemirrorConfig.variable, + }, + { tag: t.invalid, color: codemirrorConfig.invalid }, +]); + +export const basicExtensions: Extension[] = [ + history(), + foldGutter(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + EditorView.lineWrapping, + bracketMatching(), + closeBrackets(), + highlightSelectionMatches(), + obsidianTheme, + syntaxHighlighting(obsidianHighlightStyle), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + indentWithTab, + ...foldKeymap, + ...completionKeymap, + ...lintKeymap, + ]), +]; + +// TODO: Do I need to use compartments, or is there a better way? +const language = new Compartment(); + +// TODO: Set default to something more sane, even though it doesn't technically matter once everything works +export const languageExtension: Extension[] = [language.of(python())]; + +// TODO: Add all languages that the plugin should support +const LANGUAGES: Map = new Map([["py", python()]]); + +// TODO: This currently does not work. +export const updateLanguage = (view: EditorView, ext: string) => { + const LANG = LANGUAGES.get(ext); + if (LANG) { + // Note: this _does_ get run, but I'm not sure how the dispatch works. + view.dispatch({ + effects: language.reconfigure(LANG), + }); + } +}; diff --git a/src/helper.ts b/src/helper.ts index 7dd3129..fb4a4b5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,108 +1,135 @@ +import PlaintextPlugin from "./main"; + /** * Extensions obsidian supports natively. * Taken from the help page: https://help.obsidian.md/Advanced+topics/Accepted+file+formats - * - * @version 0.2.0 + * * @author dbarenholz - * @since 2022/08/13 + * @version 0.3.0 + * + * @since 2023/06/01 */ -export const obsidianExts: Set = new Set([ - "md", - "png", - "jpg", - "jpeg", - "gif", - "bmp", - "svg", - "mp3", - "webm", - "wav", - "m4a", - "ogg", - "3gp", - "flac", - "mp4", - "webm", - "ogv", - "mov", - "mkv", - "pdf", -]); +export const OBSIDIAN_EXTENSIONS: Set = new Set([ + "md", -/** - * Takes in a list of extensions, and removes extensions if they are present in the obsidianExts set. - * - * @param exts extensions to process. - * @returns All extensions in exts, except if they're present in obsidianExts. - */ -export const removeObsidianExtensions = (exts: string[]): string[] => { - return exts.filter(ext => !obsidianExts.has(ext)) -} + "png", + "webp", + "jpg", + "jpeg", + "gif", + "bmp", + "svg", + + "mp3", + "webm", + "wav", + "m4a", + "ogg", + "3gp", + "flac", + + "mp4", + "webm", + "ogv", + "mov", + "mkv", + + "pdf", +]); /** * Maps pluginIds to extensions that they use. * These extensions will be filtered out by default. - * - * @version 0.2.0 + * * @author dbarenholz + * @version 0.3.0 + * * @since 2022/08/13 */ -export const otherExts: Map = new Map([ - // https://github.com/deathau/cooklang-obsidian - ["cooklang-obsidian", "cook"], - // https://github.com/deathau/csv-obsidian - ["csv-obsidian", "csv"], - // https://github.com/caronchen/obsidian-chartsview-plugin - ["obsidian-chartsview-plugin", "csv"], - // https://github.com/Darakah/obsidian-fountain - ["obsidian-fountain", "fountain"], - // https://github.com/deathau/ini-obsidian - ["ini-obsidian", "ini"], - // https://github.com/deathau/txt-as-md-obsidian - ["txt-as-md-obsidian", "txt"], - // https://github.com/mkozhukharenko/mdx-as-md-obsidian - ["mdx-as-md-obsidian", "mdx"], - // https://github.com/ryanpcmcquen/obsidian-org-mode - ["obsidian-org-mode", "org"], - // https://github.com/tgrosinger/ledger-obsidian - ["ledger-obsidian", "ledger"], - // https://github.com/zsviczian/obsidian-excalidraw-plugin - ["obsidian-excalidraw-plugin", "excalidraw"], +export const PROBLEMATIC_PLUGINS: Map = new Map([ + // https://github.com/deathau/cooklang-obsidian + ["cooklang-obsidian", "cook"], + // https://github.com/deathau/csv-obsidian + ["csv-obsidian", "csv"], + // https://github.com/caronchen/obsidian-chartsview-plugin + ["obsidian-chartsview-plugin", "csv"], + // https://github.com/Darakah/obsidian-fountain + ["obsidian-fountain", "fountain"], + // https://github.com/deathau/ini-obsidian + ["ini-obsidian", "ini"], + // https://github.com/deathau/txt-as-md-obsidian + ["txt-as-md-obsidian", "txt"], + // https://github.com/mkozhukharenko/mdx-as-md-obsidian + ["mdx-as-md-obsidian", "mdx"], + // https://github.com/ryanpcmcquen/obsidian-org-mode + ["obsidian-org-mode", "org"], + // https://github.com/tgrosinger/ledger-obsidian + ["ledger-obsidian", "ledger"], + // https://github.com/zsviczian/obsidian-excalidraw-plugin + ["obsidian-excalidraw-plugin", "excalidraw"], ]); -// Helper to make removeOtherExtensions easier. -export const otherExtsSet: Set = new Set(Array.from(otherExts.values())) +export const removeObsidianExtensions = (exts: string[]): string[] => { + return exts.filter((ext) => !OBSIDIAN_EXTENSIONS.has(ext)); +}; /** - * Takes in a list of extensions, and removes extensions if they are present in the values of otherExts. - * - * @param exts extensions to process. - * @returns All extensions in exts, except if they're present in the values of otherExts. + * Remove extensions registered by other plugins + * + * @param exts current list of extensions (unfiltered) + * @param enabledPlugins set of enabled plugins (app.plugins.enabledPlugins) + * @returns list of extensions without those used by any other enabled plugin */ -export const removeOtherExtensions = (exts: string[]): string[] => { - return exts.filter(ext => !otherExtsSet.has(ext)) -} +export const removeOtherExtensions = ( + exts: string[], + enabledPlugins: Set +): string[] => { + for (const enabledPlugin of enabledPlugins) { + // Grab the extension to remove if it exists + const extToRemove = PROBLEMATIC_PLUGINS.has(enabledPlugin) + ? PROBLEMATIC_PLUGINS.get(enabledPlugin) + : null; + // Remove if it exists + if (extToRemove) { + exts = exts.filter((ext) => ext !== extToRemove); + } + } + + return exts; +}; + +export const craftLogMessage = ( + plugin: PlaintextPlugin, + message: string | DocumentFragment +): string => { + const VERSION = plugin.manifest.version; + return `[Plaintext v${VERSION}]: ${message}`; +}; /** * Add typings for a better developer experience. */ -declare module 'obsidian' { - interface App { - // Thank you javalent#3452 for suggestions on better typing - viewRegistry: { - unregisterView: (e: string) => void - unregisterExtensions: (e: string[]) => void - }, - plugins: { - manifests: [{ - id: string - }] - } - } +declare module "obsidian" { + interface App { + // Thank you javalent#3452 for suggestions on better typing + viewRegistry: { + unregisterView: (e: string) => void; + unregisterExtensions: (e: string[]) => void; + }; + plugins: { + manifests: [ + { + id: string; + } + ]; + enabledPlugins: Set; + }; + } - interface View { - file: { - extension: string - } - } -} \ No newline at end of file + interface View { + file: { + basename: string; + extension: string; + }; + } +} diff --git a/src/main.ts b/src/main.ts index 5d2ad76..482542c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,153 +1,232 @@ -import { Plugin, WorkspaceLeaf, ViewCreator } from "obsidian"; -import { removeObsidianExtensions, removeOtherExtensions } from "./helper"; -import { PlaintextSettings, PlaintextSettingTab, DEFAULT_SETTINGS } from "./settings"; +import { + Plugin, + WorkspaceLeaf, + TFolder, + TAbstractFile, + normalizePath, +} from "obsidian"; +import { + removeObsidianExtensions, + removeOtherExtensions, + craftLogMessage, +} from "./helper"; +import { + PlaintextSettings, + PlaintextSettingTab, + DEFAULT_SETTINGS, +} from "./settings"; import { PlaintextView } from "./view"; +import { CreateNewPlaintextFileModal } from "./modal"; /** * Plaintext plugin. * * Allows you to edit files with specified extensions as if they are plaintext files. - * There are _absolutely no_ checks to see whether or not you should actually do so. - * - * Use common sense, and don't edit `.exe` or similar binaries. + * There are a few checks to see whether or not you should actually do so: + * 1. Default obsidian extensions are automatically filtered out. + * 2. By default, extensions that other plugins use (e.g. csv or fountain) are filtered out. + * There's an option to "override" the views that those other plugins make + * in case you prefer to use the PlaintextView. + * + * There are NO checks to see if the file you wish to edit is actually a plaintext file. + * Use common sense, and don't edit obviously non-plaintext files. * * @author dbarenholz - * @version 0.2.0 + * @version 0.3.0 */ export default class PlaintextPlugin extends Plugin { - // The settings of the plugin. - public settings: PlaintextSettings; - - /** - * Code that runs (once) when plugin is loaded. - */ - async onload(): Promise { - console.log("[Plaintext]: loaded plugin."); - - // Load the settings - await this.loadSettings(); - - // Add settings tab - this.addSettingTab(new PlaintextSettingTab(this.app, this)); - - // Add extensions that we need to add. - this.addExtensions(this.settings.extensions); - } - - /** - * Code that runs (once) when the plugin is unloaded. - */ - onunload(): void { - this.removeExtensions(this.settings.extensions); - console.log("[Plaintext]: unloaded plugin."); - } - - /** - * Loads the settings. - */ - async loadSettings(): Promise { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - /** - * Saves the settings. - */ - async saveSettings(): Promise { - await this.saveData(this.settings); - } - - /** - * Creates a view for a plaintext file. - * - * @param leaf The leaf to create the view at - * @returns Plaintext view - */ - viewCreator: ViewCreator = (leaf: WorkspaceLeaf): PlaintextView => { - return new PlaintextView(leaf); - }; - - /** - * Processes the list of extensions that the user inputs, and removes conflicting ones that should not be added. - * - * @param exts Extensions that are about to be added. - * @returns A finalised list of exts to add. - */ - processConflictingExtensions = (exts: string[]): string[] => { - exts = removeObsidianExtensions(exts) - if (!this.settings.destroyOtherPlugins) { - exts = removeOtherExtensions(exts) - } - return exts - } - - /** - * Registers extensions, and makes views for them. - * - * @param exts The extensions to register and add views for. - */ - addExtensions = (exts: string[]): void => { - // Process extensions that may conflict with Obsidian or enabled plugins - exts = this.processConflictingExtensions(exts) - - // Loop through extensions - exts.forEach((ext) => { - // Try to register view - try { - this.registerView(ext, this.viewCreator); - } catch { - console.log(`[Plaintext]: Extension '${ext}' already has a view registered, ignoring...`); - } - - // Try to register extension - try { - // Note: viewtype is set to 'ext' here for possible future expansion to include syntax highlighting based on extension type. - this.registerExtensions([ext], ext); - } catch { - console.log(`[Plaintext]: Extension '${ext}' is already registered, ignoring...`); - } - - // DEBUG - console.log(`[Plaintext]: added=${ext}`); - }) - }; - - /** - * Deregisters extensions, and removes views made for them. - * - * @param exts The extensions to deregister and remove views for. - */ - removeExtensions = (exts: string[]): void => { - // Process extensions that may conflict with Obsidian or enabled plugins - exts = this.processConflictingExtensions(exts) - - // Try to deregister the views - exts.forEach((ext) => { - // Before unregistering the view: close active leaf if of type ext - // Thank you Licat#1607: activeLeaf could be null here causing a crash => Replaced with getActiveViewOfType - const view = this.app.workspace.getActiveViewOfType(PlaintextView) - if (view && ext == view.getViewType()) { - this.app.workspace.activeLeaf.detach(); - } - - try { - this.app.viewRegistry.unregisterView(ext) - } catch { - console.log(`[Plaintext]: View for extension '${ext}' cannot be deregistered...`); - - } - }); - - // Try to deregister the extensions - try { - this.app.viewRegistry.unregisterExtensions(exts) - } catch { - console.log(`[Plaintext]: Cannot deregister extensions...`); - - } - - // DEBUG - console.log(`[Plaintext]: removed=${exts}`); - - }; - + public settings: PlaintextSettings; + + async onload(): Promise { + console.log(craftLogMessage(this, "loaded plugin")); + + // 1. Load Settings + await this.loadSettings(); + this.addSettingTab(new PlaintextSettingTab(this.app, this)); + + // 2. Add commands; automatically cleaned up + this.addCommand({ + id: "new-plaintext-file", + name: "Create new plaintext file", + callback: () => { + this.createNewFile(app.vault.getRoot()); + }, + }); + + // 3. Add events; automatically cleaned up + this.registerEvent( + this.app.workspace.on("file-menu", (menu, file) => { + // But not if clicked on folder + if (!(file instanceof TFolder)) { + return; + } + menu.addItem((item) => { + item.setTitle("New plaintext file") + .setIcon("file-plus") + .onClick(async () => this.createNewFile(file)); + }); + }) + ); + + // 4. Other initialization + this.registerViewsForExtensions(this.settings.extensions); + } + + onunload(): void { + this.deregisterViewsForExtensions(this.settings.extensions); + console.log(craftLogMessage(this, "unloaded plugin")); + } + + async loadSettings(): Promise { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + } + + removeConflictingExtensions = (exts: string[]): string[] => { + // Remove default obisidian extensions from list + exts = removeObsidianExtensions(exts); + + // If we are not destroying other plugins + if (!this.settings.overrideViewsFromOtherPlugins) { + // Then also remove those extensions + exts = removeOtherExtensions(exts, app.plugins.enabledPlugins); + } + return exts; + }; + + viewCreator = (leaf: WorkspaceLeaf) => new PlaintextView(leaf); + + registerViewsForExtensions = (exts: string[]): void => { + exts = this.removeConflictingExtensions(exts); + + exts.forEach((ext) => { + // Try to register view + try { + this.registerView(`${ext}-view`, this.viewCreator); + } catch { + console.log( + craftLogMessage( + this, + `Extension '${ext}' already has a view registered, ignoring...` + ) + ); + } + + // Try to register extension + try { + this.registerExtensions([ext], `${ext}-view`); + } catch { + console.log( + craftLogMessage( + this, + `Extension '${ext}' is already registered` + ) + ); + if (this.settings.overrideViewsFromOtherPlugins) { + console.log( + craftLogMessage( + this, + `Attempting to override '${ext}'.` + ) + ); + try { + // deregister the thing + this.app.viewRegistry.unregisterExtensions(exts); + // then register for myself + this.registerExtensions([ext], `${ext}-view`); + } catch { + console.log( + craftLogMessage( + this, + `Could not override '${ext}'; did not register!` + ) + ); + } + } + } + + // DEBUG + console.log(craftLogMessage(this, `added=${ext}`)); + }); + }; + + deregisterViewsForExtensions = (exts: string[]): void => { + // Only do work if there is work + if (exts.length == 0) { + return; + } + + exts = this.removeConflictingExtensions(exts); + + exts.forEach((ext) => { + // Before unregistering the view: close active leaf if of type ext + // Thank you Licat#1607: activeLeaf could be null here causing a crash => Replaced with getActiveViewOfType + const view = this.app.workspace.getActiveViewOfType(PlaintextView); + if (view) { + view.leaf.detach(); + } + + try { + this.app.viewRegistry.unregisterView(`${ext}-view`); + } catch { + console.log( + craftLogMessage( + this, + `View for extension '${ext}' cannot be deregistered...` + ) + ); + } + }); + + // Try to deregister the extensions + try { + this.app.viewRegistry.unregisterExtensions(exts); + } catch { + console.log( + craftLogMessage(this, `Cannot deregister extensions...`) + ); + } + + // DEBUG + console.log(craftLogMessage(this, `removed=${exts}`)); + }; + + createNewFile = async (file: TAbstractFile): Promise => { + console.log(craftLogMessage(this, "Create new plaintext file")); + + new CreateNewPlaintextFileModal( + this.app, + "plaintext file.txt", + async (res) => { + // Retrieve filename from user input + const filename = normalizePath(`${file.path}/${res}`); + + // Create TFile from it + let newFile = null; + + try { + newFile = await this.app.vault.create(filename, ""); + } catch { + console.log(craftLogMessage(this, "File already exists")); + return; + } + + // Create a new leaf + const newLeaf = this.app.workspace.getLeaf(true); + + // Set the type + await newLeaf.setViewState({ type: "text/plain" }); + + // Focus it + await newLeaf.openFile(newFile); + } + ).open(); + }; } diff --git a/src/modal.ts b/src/modal.ts new file mode 100644 index 0000000..6c04483 --- /dev/null +++ b/src/modal.ts @@ -0,0 +1,62 @@ +import { App, Modal, Setting } from "obsidian"; + +/** + * Modal to create a new Plaintext file. + * + * Code from here: https://github.com/GamerGirlandCo/obsidian-fountain-revived/blob/main/src/createModal.ts + * Changes made according to https://marcus.se.net/obsidian-plugin-docs/examples/insert-link + * + * @version 0.3.0 + * @author dbarenholz + */ +export class CreateNewPlaintextFileModal extends Modal { + filename: string; + onSubmit: (filename: string) => void; + + constructor( + app: App, + defaultFilename: string, + onSubmit: (result: string) => Promise + ) { + super(app); + this.filename = defaultFilename; + this.onSubmit = onSubmit; + } + + onOpen() { + const { contentEl } = this; + contentEl.createEl("h3", { text: "New Plaintext File" }); + new Setting(contentEl) + .setName("File Name (include extension!)") + .addText((text) => + text.setValue(this.filename).onChange((value) => { + this.filename = value; + }) + ); + new Setting(contentEl).addButton((btn) => + btn + .setButtonText("Create") + .setCta() + .onClick(() => this.createFile(this.filename)) + ); + contentEl.createEl("span", { + text: "If you used the file browser, then the file will be created in the selected folder. If you used the command, it will be created in the main vault folder.", + }); + + // Listen to keyboard events + contentEl.onkeydown = (e: KeyboardEvent) => { + if (e.key === "Enter") this.createFile(this.filename); + if (e.key === "Escape") this.close(); + }; + } + + createFile(filename: string) { + this.onSubmit(filename); + this.close(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/notice.ts b/src/notice.ts new file mode 100644 index 0000000..3c25b19 --- /dev/null +++ b/src/notice.ts @@ -0,0 +1,25 @@ +import { Notice } from "obsidian"; +import { craftLogMessage } from "./helper"; +import PlaintextPlugin from "./main"; + +const DEFAULT_NOTICE_TIMEOUT_SECONDS = 5; + +/** + * A very simple notice. + * Uses a helper function to craft a message, so that + * the messages in the console and notices are consistent. + * + * @version 0.3.0 + * @author dbarenholz + */ +export class PlaintextNotice extends Notice { + constructor( + plugin: PlaintextPlugin, + message: string | DocumentFragment, + timeout = DEFAULT_NOTICE_TIMEOUT_SECONDS + ) { + super(message, timeout * 1000); + const msg = craftLogMessage(plugin, message); + console.log(msg); + } +} diff --git a/src/settings.ts b/src/settings.ts index d7e92e7..916a8e8 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,165 +1,181 @@ import PlaintextPlugin from "./main"; -import { App, PluginSettingTab, Setting, ToggleComponent } from "obsidian"; -import { obsidianExts, otherExts, otherExtsSet, removeObsidianExtensions, removeOtherExtensions } from "./helper"; -import { nextTick } from "process"; +import { App, PluginSettingTab, Setting } from "obsidian"; +import { removeObsidianExtensions, removeOtherExtensions } from "./helper"; +import { PlaintextNotice } from "./notice"; /** * Plaintext plugin settings. - * + * * Currently, there are only settings on whether to print debug prints to console, * and a list of extensions that should be considered plaintext. * - * @version 0.2.0 + * @version 0.3.0 * @author dbarenholz */ export interface PlaintextSettings { - // Whether or not you want to actively destroy other plugins by demolishing their views. - destroyOtherPlugins: boolean; + // Whether or not you want to actively destroy other plugins by demolishing their views. + overrideViewsFromOtherPlugins: boolean; - // Extensions to be seen as plaintext documents. - extensions: string[]; + // Extensions to be seen as plaintext documents. + extensions: string[]; } /** - * The defaults: don't destroy other plugins, no extensions to consider for the plaintext plugin. + * The defaults: + * * don't destroy other plugins + * * no extensions to consider for the plaintext plugin. * - * @version 0.2.0 + * @version 0.3.0 * @author dbarenholz */ export const DEFAULT_SETTINGS: PlaintextSettings = { - destroyOtherPlugins: false, - extensions: [], + overrideViewsFromOtherPlugins: false, + extensions: [], }; /** - * Processes all extensions. - * @param _this the settings tab - * - * @version 0.2.0 - * @author dbarenholz + * Process the user-inputted extensions + * + * @param _this passes "this" because JS is stupid in anonymous functions. */ const processExts = async (_this: PlaintextSettingTab) => { - // Get the currently enabled extensions from the plaintext plugin. - let current_exts = Array.from(_this.plugin.settings.extensions); - current_exts = - current_exts == [] || current_exts == null || current_exts == undefined - ? [] - : Array.from(new Set(current_exts)); - - // DEBUG - // console.log(`[Plaintext]: Current exts=${Array.from(_this.plugin.settings.extensions).toString()}`); - - // Grab the set of new extensions - let new_exts = _this.changes == null || _this.changes == undefined ? [] : _this.changes - .split(",") // split on comma - .map((s) => s.toLowerCase().trim()) // convert to lowercase and remove spaces - .filter((s) => s != "") // remove empty elements - - // Remove obsidian extensions from it - new_exts = removeObsidianExtensions(new_exts) - - // If set to NOT destroy, remove other extensions - if (!_this.plugin.settings.destroyOtherPlugins) { - new_exts = removeOtherExtensions(new_exts) - } - - // DEBUG - // console.log(`[Plaintext]: New exts=${new_exts}`); - - // Find which extensions to add. - let to_add = new_exts.filter(nExt => !current_exts.includes(nExt)) - - // DEBUG - // console.log(`[Plaintext]: add=${to_add}`); - - // Actually add the extensions - _this.plugin.addExtensions(to_add) - - // Find which extensions to remove. - let to_remove = current_exts.filter(cExt => !new_exts.includes(cExt)) - - // DEBUG - // console.log(`[Plaintext]: remove=${to_remove}`); - - // Actually remove the extensions - _this.plugin.removeExtensions(to_remove) - - // Save settings - const updated_exts = current_exts.concat(to_add).filter((ext) => !to_remove.includes(ext)) - _this.plugin.settings.extensions = updated_exts; - await _this.plugin.saveSettings(); - - // TODO: Somehow update visible extensions in settings -} + // Get the currently enabled extensions from the plaintext plugin. + let currentExtensionList = Array.from(_this.plugin.settings.extensions); + currentExtensionList = + currentExtensionList == null || currentExtensionList == undefined + ? [] + : Array.from(new Set(currentExtensionList)); + + // Grab the set of new extensions + let newExtensionList = + _this.changes == null || _this.changes == undefined + ? [] + : _this.changes + .split(",") // split on comma + .map((s) => s.toLowerCase().trim()) // convert to lowercase and remove spaces + .filter((s) => s != ""); // remove empty elements + + // Remove obsidian extensions from it + newExtensionList = removeObsidianExtensions(newExtensionList); + + // If set to NOT destroy, remove other extensions + if (!_this.plugin.settings.overrideViewsFromOtherPlugins) { + newExtensionList = removeOtherExtensions( + newExtensionList, + _this.app.plugins.enabledPlugins + ); + } + + // Find which extensions to add. + const extensionsToAdd = newExtensionList.filter( + (ext) => !currentExtensionList.includes(ext) + ); + + // Actually add the extensions + _this.plugin.registerViewsForExtensions(extensionsToAdd); + + // Find which extensions to remove. + const extensionsToRemove = currentExtensionList.filter( + (ext) => !newExtensionList.includes(ext) + ); + + // Actually remove the extensions + _this.plugin.deregisterViewsForExtensions(extensionsToRemove); + + // Save settings + const updated_exts = currentExtensionList + .concat(extensionsToAdd) + .filter((ext) => !extensionsToRemove.includes(ext)); + + _this.plugin.settings.extensions = updated_exts; + await _this.plugin.saveSettings(); + + // Communicate that extensions have been updated. + new PlaintextNotice( + _this.plugin, + `Extensions updated to: ${_this.plugin.settings.extensions}` + ); +}; /** * The settings tab itself. * - * @version 0.2.0 + * @version 0.3.0 * @author dbarenholz */ export class PlaintextSettingTab extends PluginSettingTab { - // The plugin itself (cannot be private due to processExts method) - plugin: PlaintextPlugin; - - // Changes made to the extension array (cannot be private due to processExts method) - changes: string; - - // Constructor: Creates a settingtab for this plugin. - constructor(app: App, plugin: PlaintextPlugin) { - super(app, plugin); - this.plugin = plugin; - this.changes = null; - } - - display(): void { - // Retrieve the container element - let { containerEl } = this; - containerEl.empty(); - - // Write the title of the settings page. - containerEl.createEl("h2", { text: "Plaintext" }); - - // Add extension setting - new Setting(containerEl) - .setName("Extensions") - .setDesc( - "List of extensions to interpret as plaintext, comma-separated." - + " Will automatically convert to a set when reopening the Obsidian Plaintext settings window." - + " Obsidian's default extensions and extensions other plugins use are filtered out by default!" - ) - .addText((text) => { - text - .setPlaceholder("Extensions") - .setValue(Array.from(this.plugin.settings.extensions).toString()) - .onChange((value) => (this.changes = value.toLowerCase().trim())); - - - // Need to use anonymous function calling separate function - text.inputEl.onblur = async () => { await processExts(this) } - }); - - - // Add destroy setting - new Setting(containerEl) - .setName("Destroy Other Plugins") - .setDesc( - "There may be other plugins that already have registered extensions." - + " By turning this setting ON, you willingly disregard those plugins, and will highly likely break them." - + " **ONLY TURN THIS ON IF YOU KNOW WHAT YOU'RE DOING!" - ).addToggle((toggle) => { - toggle.setValue(this.plugin.settings.destroyOtherPlugins); - toggle.onChange(async (destroy) => { - this.plugin.settings.destroyOtherPlugins = destroy - if (destroy) { - console.log(`[Plaintext]: Happily destroying plugins.`); - } else { - console.log(`[Plaintext]: Protects your from destroying other plugins.`); - // TODO: Somehow remove potentially created views, created by this plugin. - } - await this.plugin.saveSettings(); - }) - }) - - } + // The plugin itself (cannot be private due to processExts method) + plugin: PlaintextPlugin; + + // Changes made to the extension array (cannot be private due to processExts method) + changes: string; + + // Constructor: Creates a settingtab for this plugin. + constructor(app: App, plugin: PlaintextPlugin) { + super(app, plugin); + this.plugin = plugin; + this.changes = ""; + } + + display(): void { + // Retrieve the container element + const { containerEl } = this; + containerEl.empty(); + + // Write the title of the settings page. + containerEl.createEl("h2", { text: "Plaintext" }); + + // Add extension setting + new Setting(containerEl) + .setName("Extensions") + .setDesc( + "List of extensions to interpret as plaintext, comma-separated." + + " Will automatically convert to a set when reopening the Obsidian Plaintext settings window." + + " Obsidian's default extensions and extensions other plugins use are filtered out by default!" + ) + .addText((text) => { + text.setPlaceholder("Extensions") + .setValue( + Array.from(this.plugin.settings.extensions).toString() + ) + .onChange( + (value) => (this.changes = value.toLowerCase().trim()) + ); + + // Need to use anonymous function calling separate function + text.inputEl.onblur = async () => { + await processExts(this); + }; + }); + + // Add destroy setting + new Setting(containerEl) + .setName("Destroy Other Plugins") + .setDesc( + "There may be other plugins that already have registered extensions." + + " By turning this setting ON, you willingly disregard those plugins, and will highly likely break them." + + " **ONLY TURN THIS ON IF YOU KNOW WHAT YOU'RE DOING!**" + ) + .addToggle((toggle) => { + toggle.setValue( + this.plugin.settings.overrideViewsFromOtherPlugins + ); + toggle.onChange(async (destroy) => { + this.plugin.settings.overrideViewsFromOtherPlugins = + destroy; + if (destroy) { + new PlaintextNotice( + this.plugin, + "Happily overriding views made by other plugins.Are you really sure you want this?" + ); + } else { + new PlaintextNotice( + this.plugin, + "Disallow overrinding views made by other plugins. YOU NEED TO DISABLE AND RE-ENABLE PLUGINS!" + ); + } + await this.plugin.saveSettings(); + }); + }); + } } diff --git a/src/view.ts b/src/view.ts index 40766df..1ca3d41 100644 --- a/src/view.ts +++ b/src/view.ts @@ -1,110 +1,140 @@ -import CodeMirror from "codemirror"; -import { TextFileView, WorkspaceLeaf } from "obsidian"; +import { TFile, TextFileView, WorkspaceLeaf } from "obsidian"; +import { EditorView } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; +import { basicExtensions, languageExtension } from "./codemirror"; /** - * The view used for plaintext files. Uses a CodeMirror 5 instance. - * Perhaps this can be updated to CodeMirror 6 in the future. - * + * The plaintext view shows a plaintext file, hence it extends the text file view. + * Rewritten to use CM6. + * + * Code from here: https://github.com/Zachatoo/obsidian-css-editor/blob/main/src/CssEditorView.ts + * + * @version 0.3.0 * @author dbarenholz - * @version 0.1.0 */ export class PlaintextView extends TextFileView { - // Internal codemirror instance - public cm: CodeMirror.Editor; + private editorView: EditorView; + private editorState: EditorState; + file: TFile; - // Constructor - constructor(leaf: WorkspaceLeaf) { - // Call super - super(leaf); + constructor(leaf: WorkspaceLeaf) { + super(leaf); - // Create code mirror instance and add listener to it. - this.cm = CodeMirror(this.contentEl); - this.cm.on("changes", this.changed); - } + this.editorState = EditorState.create({ + extensions: [ + basicExtensions, + // TODO: Figure out how to nicely set language modes. + languageExtension, + EditorView.updateListener.of((update) => { + if (update.docChanged) { + this.save(false); + } + }), + ], + }); - /** - * Event handler for CodeMirror editor. - * Requests a save. - * - * @param _ unused - * @param __ unused - */ - changed = async (_: CodeMirror.Editor, __: CodeMirror.EditorChangeLinkedList[]): Promise => { - this.requestSave(); - }; + this.editorView = new EditorView({ + state: this.editorState, + parent: this.contentEl, + }); + } - /** - * Event handler for resizing a view. - * Refreshes codemirror instance. - */ - onResize(): void { - this.cm.refresh(); - } + /** + * Gets the type of this view. + * We use `extension-view`, where extension is the file extension. + * This is also used in main.ts, where the view types are registered and deregistered. + * + * @returns The view-type constructed fron the file extension if it exists, otherwise "text/plain". + */ + getViewType(): string { + return this.file ? `${this.file.extension}-view` : "text/plain"; + } - /** - * Getter for the data in the view. - * Called when saving the contents. - * - * @returns The file contents as string. - */ - getViewData = (): string => { - return this.cm.getValue(); - }; + /** + * A string identifier of the Lucide icon that is shown in the tab of this view. + * We use "file-code". + * + * @returns The string "file-code". + */ + getIcon(): string { + return "file-code"; + } - /** - * Setter for the data in the view. - * Called when loading file contents. - * - * If clear is set, then it means we're opening a completely different file. - * In that case, you should call clear(), or implement a slightly more efficient - * clearing mechanism given the new data to be set. - * - * @param data - * @param clear - */ - setViewData = (data: string, clear?: boolean): void => { - if (clear) { - this.cm.swapDoc(CodeMirror.Doc(data, "text/plain")); // everything is plaintext - } else { - this.cm.setValue(data); - } - }; + /** + * Gets the text to display in the header of the tab. + * This is the filename if it exists. + * + * @returns The filename if it exists, otherwise "(no file)". + */ + getDisplayText(): string { + return this.file ? this.file.basename : "(no file)"; + } - /** - * Clears the current codemirror instance. - */ - clear = (): void => { - this.cm.setValue(""); - this.cm.clearHistory(); - }; + /** + * Grabs data from the editor. + * This essentially implements the getViewData method. + * + * @returns Content in the editor. + */ + getEditorData(): string { + return this.editorView.state.doc.toString(); + } - /** - * Provides a boolean to indicate if a particular extension can be opened in this instance. - * - * @param extension the extension to check - * @returns `true` if `extension` is identical to `this.ext`, `false` otherwise. - */ - canAcceptExtension(extension: string): boolean { - return extension == this.file.extension; - } + /** + * Method that dispatches editor data. + * This essentially implements the setViewData method. + * + * @param data Content to set in the view. + */ + dispatchEditorData(data: string) { + this.editorView.dispatch({ + changes: { + from: 0, + to: this.editorView.state.doc.length, + insert: data, + }, + }); + } - /** - * Returns the viewtype of this codemirror instance. - * The viewtype is the extension of the file that is opened. - * - * @returns The viewtype (file extension) of this codemirror instance. - */ - getViewType(): string { - return this.file ? this.file.extension : "text/plain (no file)"; - } + /** + * Gets the data from the editor. + * This will be called to save the editor contents to the file. + * + * @returns A string representing the content of the editor. + */ + getViewData(): string { + return this.getEditorData(); + } - /** - * Returns a string indicating which file is currently open, if any. - * If no file is open, returns that. - * - * @returns A string indicating the opened file, if any. - */ - getDisplayText(): string { - return this.file ? this.file.basename : "text/plain (no file)"; - } + /** + * Set the data to the editor. + * This is used to load the file contents. + * + * If clear is set, then it means we're opening a completely different file. + * In that case, you should call clear(), or implement a slightly more efficient + * clearing mechanism given the new data to be set. + * + * @param data data to load + * @param clear whether or not to clear the editor + */ + setViewData(data: string, clear: boolean): void { + if (clear) { + // Note: this.clear() destroys the editor completely - this is inaccurate + // as we only want to change the editor data. + this.dispatchEditorData(""); + } + + this.dispatchEditorData(data); + } + + /** + * Clear the editor. + * + * This is called when we're about to open a completely different file, + * so it's best to clear any editor states like undo-redo history, + * and any caches/indexes associated with the previous file contents. + */ + clear(): void { + this.editorView.destroy(); + } } diff --git a/tsconfig.json b/tsconfig.json index 6576790..8ca8c85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,17 @@ { - "compilerOptions": { - "baseUrl": ".", - "inlineSourceMap": true, - "inlineSources": true, - "module": "ESNext", - "target": "es6", - "allowJs": true, - "noImplicitAny": true, - "moduleResolution": "node", - "importHelpers": true, - "lib": [ - "dom", - "es5", - "scripthost", - "es2015" - ], - "allowSyntheticDefaultImports": true, - }, - "include": [ - "**/*.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": ["DOM", "ES5", "ES6", "ES7"] + }, + "include": ["**/*.ts"] +} diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..371fc31 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,14 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +// read minAppVersion from manifest.json and bump version to target version +let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +// update versions.json with target version and minAppVersion from manifest.json +let versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); \ No newline at end of file diff --git a/versions.json b/versions.json index 0a4a38d..a4988f0 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ { + "0.3.0": "0.15.9", "0.2.0": "0.15.9", "0.1.0": "0.13.14", "0.0.2": "0.12.12",