diff --git a/src/CodeBlock.ts b/src/CodeBlock.ts index 3e4a728..8059e7d 100644 --- a/src/CodeBlock.ts +++ b/src/CodeBlock.ts @@ -1,12 +1,16 @@ -import { type MarkdownPostProcessorContext, MarkdownRenderChild } from 'obsidian'; +import { type MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownView, Notice } from 'obsidian'; import type ShikiPlugin from 'src/main'; +// css class name of obsidian edit block button in live preview +const EDIT_BLOCK_CLASSNAME = '.edit-block-button'; + export class CodeBlock extends MarkdownRenderChild { plugin: ShikiPlugin; source: string; language: string; ctx: MarkdownPostProcessorContext; cachedMetaString: string; + lineStart?: number; constructor(plugin: ShikiPlugin, containerEl: HTMLElement, source: string, language: string, ctx: MarkdownPostProcessorContext) { super(containerEl); @@ -25,6 +29,7 @@ export class CodeBlock extends MarkdownRenderChild { return ''; } + this.lineStart = sectionInfo.lineStart; const lines = sectionInfo.text.split('\n'); const startLine = lines[sectionInfo.lineStart]; @@ -40,6 +45,8 @@ export class CodeBlock extends MarkdownRenderChild { private async render(metaString: string): Promise { await this.plugin.highlighter.renderWithEc(this.source, this.language, metaString, this.containerEl); + this.addRowEditButtons(); + this.hideCopyButtons(); } public async rerenderOnNoteChange(): Promise { @@ -58,6 +65,69 @@ export class CodeBlock extends MarkdownRenderChild { await this.render(this.cachedMetaString); } + /** + * In live preview, add a row edit button + * to each line of code block to improve editability, + * except lines in collapsible section summary. + */ + private addRowEditButtons(): void { + if (!this.plugin.settings.rowEditButtons) + return; + + const lineStart = this.lineStart; + const editBlock = this.containerEl.parentElement?.find(EDIT_BLOCK_CLASSNAME); + const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView); + + if (lineStart && editBlock && view?.getMode() === 'source') { + const lines = this.containerEl.getElementsByClassName('ec-line'); + for (let i = 0, lineNo = 0; i < lines.length; i++) { + // ignore lines in collapsible section summary + if (lines[i].parentElement?.tagName === 'SUMMARY') + continue; + const editBtn = lines[i].createEl("div", { cls: "ec-edit-btn", attr: { "data-line": lineNo+1 } }); + editBtn.addEventListener("click", (e: Event) => { + const lineNo = (e.currentTarget as HTMLElement).getAttribute("data-line"); + if (!lineNo) return; + + // a workaround to break the non-editable state + // of embed code block by clicking this 'edit block' button + editBlock.click(); + + // select the row in editor + let _i = lineStart + parseInt(lineNo); + let _from = { line: _i, ch: 0 }; + let _to = { line: _i, ch: view.editor.getLine(_i).length }; + view.editor.setSelection(_from, _to); + }); + lineNo++; + } + // Hide native buttons to avoid blocking row edit buttons + if (this.plugin.settings.hideNativeBlockEdit) { + editBlock.style.display = 'none'; + } + } + } + + /** + * Hide copy buttons for shiki code blocks. + * Now right-click on a code block to copy the entire code. + */ + private hideCopyButtons(): void { + if (!this.plugin.settings.hideNativeCopy) + return; + const copyBtn = this.containerEl.find('.copy>button'); + if (copyBtn) { + copyBtn.style.display = 'none'; + this.containerEl.addEventListener('contextmenu', () => { + // only copy entire code when there is no selection. + // if there is selection, obsidian will show a "Ctrl+C" context menu. + if (!window.getSelection()?.toString()) { + copyBtn.click(); + } + }); + } + } + public onload(): void { super.onload(); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index a4e3e13..a47adf2 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -5,6 +5,9 @@ export interface Settings { theme: string; preferThemeColors: boolean; inlineHighlighting: boolean; + rowEditButtons: boolean; + hideNativeBlockEdit: boolean; + hideNativeCopy: boolean; } export const DEFAULT_SETTINGS: Settings = { @@ -14,4 +17,7 @@ export const DEFAULT_SETTINGS: Settings = { theme: 'obsidian-theme', preferThemeColors: true, inlineHighlighting: true, + rowEditButtons: false, + hideNativeBlockEdit: false, + hideNativeCopy: false }; diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index c27c5ad..dd9d794 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -127,6 +127,41 @@ export class ShikiSettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + + new Setting(this.containerEl).setHeading().setName('Button Settings').setDesc('Configure code block button settings. Changes will apply to NEWLY RENDERED CODE.'); + + new Setting(this.containerEl) + .setName('Show Row Edit Buttons in Live Preivew') + .setDesc('Whether to add a row edit button to each line of code block to improve editability.') + .addToggle(toggle => { + toggle.setValue(this.plugin.settings.rowEditButtons).onChange(async value => { + hideNativeBlockEdit.setDisabled(!value); + this.plugin.settings.rowEditButtons = value; + await this.plugin.saveSettings(); + }); + }); + + const hideNativeBlockEdit = new Setting(this.containerEl) + .setName('Hide Native Block Edit Buttons') + .setDesc('When row edit buttons are enabled, whether to hide native block edit buttons to avoid blocking.') + .setClass('shiki-foldable-setting') + .setDisabled(!this.plugin.settings.rowEditButtons) + .addToggle(toggle => { + toggle.setValue(this.plugin.settings.hideNativeBlockEdit).onChange(async value => { + this.plugin.settings.hideNativeBlockEdit = value; + await this.plugin.saveSettings(); + }); + }); + + new Setting(this.containerEl) + .setName('Hide Native Copy Buttons') + .setDesc('Whether to hide copy buttons for shiki code blocks. When enabled, right-click on a code block to copy the entire code.') + .addToggle(toggle => { + toggle.setValue(this.plugin.settings.hideNativeCopy).onChange(async value => { + this.plugin.settings.hideNativeCopy = value; + await this.plugin.saveSettings(); + }); + }); new Setting(this.containerEl).setHeading().setName('Language Settings').setDesc('Configure language settings. RESTART REQUIRED AFTER CHANGES.'); diff --git a/styles.css b/styles.css index 73aaaff..9ffd0b2 100644 --- a/styles.css +++ b/styles.css @@ -92,7 +92,30 @@ span.shiki-ul { text-decoration: underline; } +/* Row edit buttons in live preview mode */ +.ec-line:hover .ec-edit-btn { + cursor: pointer; + position: fixed; + right: 0; + &::before { + content: var(--ec-edit-btn-content, "✏️"); + padding-inline: var(--ec-edit-btn-padding, 3px 5px); + background: var(--tmLineBgCol, var(--shiki-code-background)); + } +} + /* Settings tab */ .setting-item-control input.shiki-custom-theme-folder { min-width: 250px; } + +.shiki-foldable-setting { + transition: opacity 1s ease-out; + + &.is-disabled { + padding: 0; + opacity: 0; + height: 0; + overflow: hidden; + } +} \ No newline at end of file