From 52062720087b7a324afc73ae36ef825bbccdccfa Mon Sep 17 00:00:00 2001 From: Lukas Bach Date: Thu, 23 Mar 2023 17:14:26 +0100 Subject: [PATCH] create file modal --- esbuild.config.mjs | 2 +- main.ts | 286 ------------------------------------ manifest.json | 14 +- src/codeEditorView.ts | 121 +++++++++++++++ src/codeFilesPlugin.ts | 95 ++++++++++++ src/codeFilesSettingsTab.ts | 92 ++++++++++++ src/common.ts | 23 +++ src/createCodeFileModal.ts | 86 +++++++++++ src/main.ts | 3 + 9 files changed, 428 insertions(+), 294 deletions(-) delete mode 100644 main.ts create mode 100644 src/codeEditorView.ts create mode 100644 src/codeFilesPlugin.ts create mode 100644 src/codeFilesSettingsTab.ts create mode 100644 src/common.ts create mode 100644 src/createCodeFileModal.ts create mode 100644 src/main.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 6c959bb..c6c7cb5 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -16,7 +16,7 @@ const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ["main.ts"], + entryPoints: ["src/main.ts"], bundle: true, // plugins: [monacoPlugin({languages: ["typescript", "json", "css", "html"]})], // loader: { diff --git a/main.ts b/main.ts deleted file mode 100644 index 420d9fe..0000000 --- a/main.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { - App, DropdownComponent, - Editor, - MarkdownView, - Modal, - Notice, - Plugin, - PluginSettingTab, - Setting, TAbstractFile, TextComponent, - TextFileView, TFile, - View -} from 'obsidian'; -import * as path from "path"; -import {EditorState, Compartment} from "@codemirror/state" -import {EditorView, keymap} from "@codemirror/view" -import {defaultKeymap} from "@codemirror/commands" -import { javascript } from "@codemirror/lang-javascript"; -import { json } from "@codemirror/lang-json"; -import { python } from "@codemirror/lang-python"; - -// Remember to rename these classes and interfaces! - - - -interface MyPluginSettings { - mySetting: string; -} - -const DEFAULT_SETTINGS: MyPluginSettings = { - mySetting: 'default' -} - -const viewType = "code-editor"; - -let i = 0; -class CodeEditorView extends TextFileView { - id = i++; - value: string = ""; - iframe: HTMLIFrameElement; - - getDisplayText(): string { - console.log("!!getDisplayText", this.id) - return this.file?.name; - } - - getViewType(): string { - return viewType; - } - - async onClose() { - console.log("!!onClose", this.id) - this.iframe.remove(); - } - - async onLoadFile(file: TFile) { - await super.onLoadFile(file); - console.log("!!onLoadFile", this.id, file.name) - this.send("change-language", { language: this.getLanguage() }); - } - - async onUnloadFile(file: TFile) { - console.log("!!onUnloadFile", this.id, file.name) - } - - async onOpen() { - console.log("!!onOpen", this.id) - this.iframe = document.createElement("iframe"); - this.iframe.src = `https://embeddable-monaco.lukasbach.com?lang=${this.getLanguage()}`; - this.iframe.style.width = "100%"; - this.iframe.style.height = "100%"; - (this.containerEl.getElementsByClassName("view-content")[0] as HTMLElement).style.overflow = "hidden"; - this.containerEl.getElementsByClassName("view-content")[0].append(this.iframe); - window.addEventListener("message", ({ data }) => { - switch(data.type) { - case "ready": { - this.send("change-value", { value: this.value }); - this.send("change-background", { background: "transparent", theme: "vs-dark" }); - break; - } - case "change": { - this.value = data.value; - this.requestSave(); - break; - } - } - }); - } - - clear(): void { - console.log("!!clear", this.id) - this.value = ""; - this.send("change-value", { value: "" }); - } - - getViewData(): string { - return this.value; - } - - setViewData(data: string, clear = false): void { - console.log("!!set view data", this.id, data, clear) - this.value = data; - this.send("change-value", { value: data }); - } - - getLanguage() { - switch (this.file?.extension) { - case "js": - case "jsx": - return "javascript"; - case "ts": - case "tsx": - return "typescript"; - case "json": - return "json"; - case "py": - return "python"; - case "css": - return "css"; - case "html": - return "html"; - case "cpp": - return "cpp"; - case "graphql": - return "graphql"; - case "java": - return "java"; - case "php": - return "php"; - case "sql": - return "sql"; - case "yaml": - case "yml": - return "yaml"; - default: - return "plaintext"; - } - } - - send(type: string, payload: any) { - console.log("!!send", !!this.iframe.contentWindow) - this.iframe.contentWindow?.postMessage({ - type, - ...payload - }, "*"); - } -} - -export default class CodeFilesPlugin extends Plugin { - settings: MyPluginSettings; - - - async onload() { - await this.loadSettings(); - - - this.registerView(viewType, leaf => new CodeEditorView(leaf)); - this.registerExtensions(["ts", "tsx", "txt"], viewType); - - - this.registerEvent( - this.app.workspace.on("file-menu", (menu, file) => { - menu.addItem((item) => { - item - .setTitle("Print file path 👈") - .setIcon("document") - .onClick(async () => { - new SampleModal(this.app, file).open(); - }); - }); - }) - ); - - - - // This creates an icon in the left ribbon. - const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => { - // Called when the user clicks the icon. - new Notice('This is a notice!'); - }); - // Perform additional things with the ribbon - ribbonIconEl.addClass('my-plugin-ribbon-class'); - - // This adds a status bar item to the bottom of the app. Does not work on mobile apps. - const statusBarItemEl = this.addStatusBarItem(); - statusBarItemEl.setText('Status Bar Text'); - - // This adds a simple command that can be triggered anywhere - this.addCommand({ - id: 'open-sample-modal-simple', - name: 'Open sample modal (simple)', - callback: () => { - } - }); - // This adds an editor command that can perform some operation on the current editor instance - this.addCommand({ - id: 'sample-editor-command', - name: 'Sample editor command', - editorCallback: (editor: Editor, view: MarkdownView) => { - console.log(editor.getSelection()); - editor.replaceSelection('Sample Editor Command'); - } - }); - // This adds a complex command that can check whether the current state of the app allows execution of the command - this.addCommand({ - id: 'open-sample-modal-complex', - name: 'Open sample modal (complex)', - checkCallback: (checking: boolean) => { - // Conditions to check - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); - if (markdownView) { - // If checking is true, we're simply "checking" if the command can be run. - // If checking is false, then we want to actually perform the operation. - if (!checking) { - } - - // This command will only show up in Command Palette when the check function returns true - return true; - } - } - }); - - // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new CodeFilesSettingsTab(this.app, this)); - } - - onunload() { - - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - } -} - -class SampleModal extends Modal { - constructor(app: App, private file: TAbstractFile) { - super(app); - } - - onOpen() { - const {contentEl} = this; - contentEl.style.display = "flex" - const fileNameInput = new TextComponent(contentEl); - - const fileExtensionInput = new DropdownComponent(contentEl); - - } - - onClose() { - const {contentEl} = this; - contentEl.empty(); - } -} - -class CodeFilesSettingsTab extends PluginSettingTab { - plugin: CodeFilesPlugin; - - constructor(app: App, plugin: CodeFilesPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const {containerEl} = this; - - containerEl.empty(); - - containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'}); - - new Setting(containerEl) - .setName('Setting #1') - .setDesc('It\'s a secret') - .addText(text => text - .setPlaceholder('Enter your secret') - .setValue(this.plugin.settings.mySetting) - .onChange(async (value) => { - console.log('Secret: ' + value); - this.plugin.settings.mySetting = value; - await this.plugin.saveSettings(); - })); - } -} diff --git a/manifest.json b/manifest.json index 0897aff..1c8fa78 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { - "id": "obsidian-sample-plugin", - "name": "Sample Plugin", + "id": "obsidian-code-files-plugin", + "name": "Code Files", "version": "1.0.0", "minAppVersion": "0.15.0", - "description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", - "author": "Obsidian", - "authorUrl": "https://obsidian.md", - "fundingUrl": "https://obsidian.md/pricing", - "isDesktopOnly": false + "description": "Edit Code Files in Obsidian with VSCode's powerful Monaco Editor", + "author": "Lukas Bach", + "authorUrl": "https://lukasbach.com", + "fundingUrl": "https://github.com/lukasbach/.github/blob/main/funding.md", + "isDesktopOnly": true } diff --git a/src/codeEditorView.ts b/src/codeEditorView.ts new file mode 100644 index 0000000..56f010b --- /dev/null +++ b/src/codeEditorView.ts @@ -0,0 +1,121 @@ +import {TextFileView, TFile, WorkspaceLeaf} from "obsidian"; +import {viewType} from "./common"; +import CodeFilesPlugin from "./codeFilesPlugin"; + +export class CodeEditorView extends TextFileView { + static i = 0; + id = CodeEditorView.i++; + value: string = ""; + iframe: HTMLIFrameElement; + + constructor(leaf: WorkspaceLeaf, private plugin: CodeFilesPlugin) { + super(leaf); + } + + getDisplayText(): string { + console.log("!!getDisplayText", this.id) + return this.file?.name; + } + + getViewType(): string { + return viewType; + } + + async onClose() { + console.log("!!onClose", this.id) + this.iframe.remove(); + } + + async onLoadFile(file: TFile) { + await super.onLoadFile(file); + console.log("!!onLoadFile", this.id, file.name) + this.send("change-language", {language: this.getLanguage()}); + } + + async onUnloadFile(file: TFile) { + console.log("!!onUnloadFile", this.id, file.name) + } + + async onOpen() { + console.log("!!onOpen", this.id) + const theme = this.plugin.settings.isDark ? "vs-dark" : "vs-light"; + this.iframe = document.createElement("iframe"); + this.iframe.src = `https://embeddable-monaco.lukasbach.com?lang=${this.getLanguage()}&theme=${theme}&background=transparent`; + this.iframe.style.width = "100%"; + this.iframe.style.height = "100%"; + (this.containerEl.getElementsByClassName("view-content")[0] as HTMLElement).style.overflow = "hidden"; + this.containerEl.getElementsByClassName("view-content")[0].append(this.iframe); + window.addEventListener("message", ({data}) => { + switch (data.type) { + case "ready": { + this.send("change-value", {value: this.value}); + this.send("change-background", {background: "transparent", theme: "vs-dark"}); + break; + } + case "change": { + this.value = data.value; + this.requestSave(); + break; + } + } + }); + } + + clear(): void { + console.log("!!clear", this.id) + this.value = ""; + this.send("change-value", {value: ""}); + } + + getViewData(): string { + return this.value; + } + + setViewData(data: string, clear = false): void { + console.log("!!set view data", this.id, data, clear) + this.value = data; + this.send("change-value", {value: data}); + } + + getLanguage() { + switch (this.file?.extension) { + case "js": + case "jsx": + return "javascript"; + case "ts": + case "tsx": + return "typescript"; + case "json": + return "json"; + case "py": + return "python"; + case "css": + return "css"; + case "html": + return "html"; + case "cpp": + return "cpp"; + case "graphql": + return "graphql"; + case "java": + return "java"; + case "php": + return "php"; + case "sql": + return "sql"; + case "yaml": + case "yml": + return "yaml"; + default: + return "plaintext"; + } + } + + send(type: string, payload: any) { + console.log("!!send", !!this.iframe.contentWindow) + this.iframe.contentWindow?.postMessage({ + type, + ...payload + }, "*"); + } +} diff --git a/src/codeFilesPlugin.ts b/src/codeFilesPlugin.ts new file mode 100644 index 0000000..3c7d65c --- /dev/null +++ b/src/codeFilesPlugin.ts @@ -0,0 +1,95 @@ +import {Editor, MarkdownView, Notice, Plugin} from "obsidian"; +import {DEFAULT_SETTINGS, MyPluginSettings} from "./common"; +import {CodeEditorView} from "./codeEditorView"; +import {CreateCodeFileModal} from "./createCodeFileModal"; +import {CodeFilesSettingsTab} from "./codeFilesSettingsTab"; +import {viewType} from "./common"; +export default class CodeFilesPlugin extends Plugin { + settings: MyPluginSettings; + + + async onload() { + await this.loadSettings(); + + + this.registerView(viewType, leaf => new CodeEditorView(leaf, this)); + this.registerExtensions(this.settings.extensions, viewType); + + + this.registerEvent( + this.app.workspace.on("file-menu", (menu, file) => { + menu.addItem((item) => { + item + .setTitle("Create Code File") + .setIcon("file-json") + .onClick(async () => { + new CreateCodeFileModal(this, file).open(); + }); + }); + }) + ); + + + // This creates an icon in the left ribbon. + const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => { + // Called when the user clicks the icon. + new Notice('This is a notice!'); + }); + // Perform additional things with the ribbon + ribbonIconEl.addClass('my-plugin-ribbon-class'); + + // This adds a status bar item to the bottom of the app. Does not work on mobile apps. + const statusBarItemEl = this.addStatusBarItem(); + statusBarItemEl.setText('Status Bar Text'); + + // This adds a simple command that can be triggered anywhere + this.addCommand({ + id: 'open-sample-modal-simple', + name: 'Open sample modal (simple)', + callback: () => { + } + }); + // This adds an editor command that can perform some operation on the current editor instance + this.addCommand({ + id: 'sample-editor-command', + name: 'Sample editor command', + editorCallback: (editor: Editor, view: MarkdownView) => { + console.log(editor.getSelection()); + editor.replaceSelection('Sample Editor Command'); + } + }); + // This adds a complex command that can check whether the current state of the app allows execution of the command + this.addCommand({ + id: 'open-sample-modal-complex', + name: 'Open sample modal (complex)', + checkCallback: (checking: boolean) => { + // Conditions to check + const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); + if (markdownView) { + // If checking is true, we're simply "checking" if the command can be run. + // If checking is false, then we want to actually perform the operation. + if (!checking) { + } + + // This command will only show up in Command Palette when the check function returns true + return true; + } + } + }); + + // This adds a settings tab so the user can configure various aspects of the plugin + this.addSettingTab(new CodeFilesSettingsTab(this.app, this)); + } + + onunload() { + + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } +} diff --git a/src/codeFilesSettingsTab.ts b/src/codeFilesSettingsTab.ts new file mode 100644 index 0000000..7b13543 --- /dev/null +++ b/src/codeFilesSettingsTab.ts @@ -0,0 +1,92 @@ +import {App, PluginSettingTab, Setting} from "obsidian"; +import CodeFilesPlugin from "./codeFilesPlugin"; + +export class CodeFilesSettingsTab extends PluginSettingTab { + plugin: CodeFilesPlugin; + + constructor(app: App, plugin: CodeFilesPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const {containerEl} = this; + + containerEl.empty(); + + containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'}); + + + new Setting(containerEl) + .setName('File Extensions') + .setDesc('Files with these extensions will show up in the sidebar, and will ' + + 'be available to create new files from. Seperated by commas.') + .addText(text => text + .setPlaceholder('js,ts') + .setValue(this.plugin.settings.extensions.join(",")) + .onChange(async (value) => { + this.plugin.settings.extensions = value.split(","); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Folding') + .setDesc('Editor will support code block folding.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.folding) + .onChange(async (value) => { + this.plugin.settings.folding = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Line Numbers') + .setDesc('Editor will show line numbers.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.lineNumbers) + .onChange(async (value) => { + this.plugin.settings.lineNumbers = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Minimap') + .setDesc('Editor will show a minimap.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.minimap) + .onChange(async (value) => { + this.plugin.settings.minimap = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Semantic Validation') + .setDesc('Editor will show semantic validation errors.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.semanticValidation) + .onChange(async (value) => { + this.plugin.settings.semanticValidation = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Syntax Validation') + .setDesc('Editor will show syntax validation errors.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.syntaxValidation) + .onChange(async (value) => { + this.plugin.settings.syntaxValidation = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Dark Mode') + .setDesc('Is obsidian running with a dark theme?') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.isDark) + .onChange(async (value) => { + this.plugin.settings.isDark = value; + await this.plugin.saveSettings(); + })); + } +} diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..07e48b2 --- /dev/null +++ b/src/common.ts @@ -0,0 +1,23 @@ + +export interface MyPluginSettings { + extensions: string[]; + folding: boolean; + lineNumbers: boolean; + minimap: boolean; + semanticValidation: boolean; + syntaxValidation: boolean; + isDark: boolean; +} + +export const DEFAULT_SETTINGS: MyPluginSettings = { + extensions: ["ts", "tsx", "js", "jsx", "json", "html", "py"], + folding: true, + lineNumbers: true, + minimap: true, + semanticValidation: true, + syntaxValidation: true, + isDark: true, +} + + +export const viewType = "code-editor"; diff --git a/src/createCodeFileModal.ts b/src/createCodeFileModal.ts new file mode 100644 index 0000000..f90205e --- /dev/null +++ b/src/createCodeFileModal.ts @@ -0,0 +1,86 @@ +import { + App, + ButtonComponent, + DropdownComponent, + Modal, + Notice, + TAbstractFile, + TextComponent, + TFile, + TFolder +} from "obsidian"; +import CodeFilesPlugin from "./codeFilesPlugin"; + +export class CreateCodeFileModal extends Modal { + fileName = "My Code File"; + fileExtension = this.plugin.settings.extensions[0]; + + constructor(private plugin: CodeFilesPlugin, private parent: TAbstractFile) { + super(plugin.app); + } + + onOpen() { + const {contentEl} = this; + contentEl.style.display = "flex" + contentEl.style.alignItems = "center"; + const fileNameInput = new TextComponent(contentEl); + fileNameInput.inputEl.style.flexGrow = "1"; + fileNameInput.inputEl.style.marginRight = "10px"; + fileNameInput.setValue(this.fileName); + fileNameInput.inputEl.addEventListener("keypress", e => { + if (e.key === "Enter") { + this.complete(); + } + }); + fileNameInput.onChange(value => this.fileName = value); + + const fileExtensionInput = new DropdownComponent(contentEl); + fileExtensionInput.selectEl.style.marginRight = "10px"; + fileExtensionInput.addOptions(this.plugin.settings.extensions.reduce((acc, ext) => { + acc[ext] = ext; + return acc; + }, {} as any)); + fileExtensionInput.setValue(this.fileExtension); + fileExtensionInput.onChange(value => this.fileExtension = value); + + fileExtensionInput.selectEl.addEventListener("keypress", e => { + if (e.key === "Enter") { + this.complete(); + } + }); + + const submitButton = new ButtonComponent(contentEl); + submitButton.setCta(); + submitButton.setButtonText("Create"); + submitButton.onClick(() => this.complete()); + + fileNameInput.inputEl.focus(); + } + + async complete() { + this.close(); + const parent = (this.parent instanceof TFile ? this.parent.parent : this.parent) as TFolder; + const newPath = `${parent.path}/${this.fileName}.${this.fileExtension}`; + + const existingFile = this.app.vault.getAbstractFileByPath(newPath); + if (existingFile) { + new Notice("File already exists"); + const leaf = this.app.workspace.getLeaf(true); + leaf.openFile(existingFile as any); + return; + } + + const newFile = await this.app.vault.create( + newPath, + "", + {} + ); + const leaf = this.app.workspace.getLeaf(true); + leaf.openFile(newFile); + } + + onClose() { + const {contentEl} = this; + contentEl.empty(); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b8450cd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,3 @@ +import plugin from "./codeFilesPlugin"; + +export default plugin;