create file modal

This commit is contained in:
Lukas Bach 2023-03-23 17:14:26 +01:00
parent dfe0e5a7b7
commit 5206272008
9 changed files with 428 additions and 294 deletions

View file

@ -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: {

286
main.ts
View file

@ -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();
}));
}
}

View file

@ -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
}

121
src/codeEditorView.ts Normal file
View file

@ -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
}, "*");
}
}

95
src/codeFilesPlugin.ts Normal file
View file

@ -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);
}
}

View file

@ -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();
}));
}
}

23
src/common.ts Normal file
View file

@ -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";

View file

@ -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();
}
}

3
src/main.ts Normal file
View file

@ -0,0 +1,3 @@
import plugin from "./codeFilesPlugin";
export default plugin;