diff --git a/data.json b/data.json index 38b0437..53175c3 100644 --- a/data.json +++ b/data.json @@ -1,12 +1,12 @@ { - "autoStopTimers": "quit", - "timerInsertLocation": "head", - "enableCheckboxToTimer": true, - "runningCheckboxState": "/", - "pausedCheckboxState": "-xX", - "checkboxToTimerPathRestriction": "disable", - "pathRestrictionPaths": [], - "runningIcon": "⏳", - "pausedIcon": "πŸ’", - "timeDisplayFormat": "full" + "autoStopTimers": "quit", + "timerInsertLocation": "head", + "enableCheckboxToTimer": true, + "runningCheckboxState": "/", + "pausedCheckboxState": "-xX", + "checkboxToTimerPathRestriction": "disable", + "pathRestrictionPaths": [], + "runningIcon": "⏳", + "pausedIcon": "πŸ’", + "timeDisplayFormat": "smart" } \ No newline at end of file diff --git a/main.js b/main.js index a1baa26..6cb90fc 100644 --- a/main.js +++ b/main.js @@ -2,11 +2,205 @@ const obsidian = require('obsidian'); -const { EditorView } = require('@codemirror/view'); -const { Transaction } = require('@codemirror/state'); +const { EditorView, Decoration, WidgetType } = require('@codemirror/view'); +const { Transaction, RangeSetBuilder, StateField, StateEffect } = require('@codemirror/state'); +const { syntaxTree } = require('@codemirror/language'); +const { keymap } = require('@codemirror/view'); -// Regex constants +// β€”β€” Timer Widget: Display folded timer span β€”β€” // +class TimerWidget extends WidgetType { + constructor(htmlText, startPos, endPos) { + super(); + this.htmlText = htmlText; + this.startPos = startPos; + this.endPos = endPos; + } + + toDOM() { + const span = document.createElement('span'); + span.className = 'timer-widget-display'; + + // Extract visible content from HTML (【⏳00:00:00 】) + const match = this.htmlText.match(/>([^<]*)<\/span>/); + span.textContent = match ? match[1] : '[Timer]'; + + // Add hover title to show what it is + span.title = 'Timer (click to edit)'; + + return span; + } + + ignoreEvent(event) { + // Don't ignore events - let them propagate for proper handling + return false; + } +} + +// β€”β€” Timer Span Folding Field β€”β€” // +const timerFoldingField = StateField.define({ + create(state) { + return buildTimerFolding(state); + }, + + update(decorations, tr) { + if (tr.docChanged) { + return buildTimerFolding(tr.state); + } + return decorations.map(tr.changes); + } +}); + +function buildTimerFolding(state) { + const builder = new RangeSetBuilder(); + const timerRegex = /【[^<]*】<\/span>/g; + + for (let pos = 0; pos < state.doc.length;) { + const line = state.doc.lineAt(pos); + const lineText = line.text; + + let match; + timerRegex.lastIndex = 0; + while ((match = timerRegex.exec(lineText)) !== null) { + const start = line.from + match.index; + const end = start + match[0].length; + const htmlText = match[0]; + + // Replace the entire HTML span with a widget that acts as an atomic unit + builder.add(start, end, Decoration.replace({ + widget: new TimerWidget(htmlText, start, end), + inclusive: true, + block: false, + side: 1 // Position cursor after the widget + })); + } + + pos = line.to + 1; + } + + return builder.finish(); +} + +// β€”β€” Timer Widget Keymap Handler β€”β€” // +const timerWidgetKeymap = keymap.of([{ + key: 'Backspace', + run(view) { + const { state, dispatch } = view; + const pos = state.selection.main.head; + + // Check if cursor is right after a timer widget + const line = state.doc.lineAt(pos); + const timerRegex = /【[^<]*】<\/span>/g; + + let match; + timerRegex.lastIndex = 0; + while ((match = timerRegex.exec(line.text)) !== null) { + const spanStart = line.from + match.index; + const spanEnd = spanStart + match[0].length; + + // If cursor is right after timer, delete the entire timer + if (pos === spanEnd) { + dispatch(state.update({ + changes: { from: spanStart, to: spanEnd, insert: '' } + })); + return true; + } + } + + return false; // Let default behavior handle it + } + }, + { + key: 'Delete', + run(view) { + const { state, dispatch } = view; + const pos = state.selection.main.head; + + // Check if cursor is right before a timer widget + const line = state.doc.lineAt(pos); + const timerRegex = /【[^<]*】<\/span>/g; + + let match; + timerRegex.lastIndex = 0; + while ((match = timerRegex.exec(line.text)) !== null) { + const spanStart = line.from + match.index; + const spanEnd = spanStart + match[0].length; + + // If cursor is right before timer, delete the entire timer + if (pos === spanStart) { + dispatch(state.update({ + changes: { from: spanStart, to: spanEnd, insert: '' } + })); + return true; + } + } + + return false; // Let default behavior handle it + } + }, + { + key: 'ArrowLeft', + run(view) { + const { state, dispatch } = view; + const pos = state.selection.main.head; + + // Check if cursor is at or inside a timer widget + const line = state.doc.lineAt(pos); + const timerRegex = /【[^<]*】<\/span>/g; + + let match; + timerRegex.lastIndex = 0; + while ((match = timerRegex.exec(line.text)) !== null) { + const spanStart = line.from + match.index; + const spanEnd = spanStart + match[0].length; + + // If cursor is within timer span, jump to before it + if (pos > spanStart && pos <= spanEnd) { + dispatch(state.update({ + selection: { anchor: spanStart } + })); + return true; + } + } + + return false; // Let default behavior handle it + } + }, + { + key: 'ArrowRight', + run(view) { + const { state, dispatch } = view; + const pos = state.selection.main.head; + + // Check if cursor is at or inside a timer widget + const line = state.doc.lineAt(pos); + const timerRegex = /【[^<]*】<\/span>/g; + + let match; + timerRegex.lastIndex = 0; + while ((match = timerRegex.exec(line.text)) !== null) { + const spanStart = line.from + match.index; + const spanEnd = spanStart + match[0].length; + + // If cursor is within timer span, jump to after it + if (pos >= spanStart && pos < spanEnd) { + dispatch(state.update({ + selection: { anchor: spanEnd } + })); + return true; + } + } + + return false; // Let default behavior handle it + } + } +]); + +// β€”β€” Regex constants β€”β€” // const UPDATE_INTERVAL = 1000; + +// β€”β€” Base62 ε·₯具函数常量 β€”β€” // +const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + // β€”β€” Checkbox regexes β€”β€” // const ORDERED_LIST = /(^\s*#*\d+\.\s)/ const UNORDERED_LIST = /(^\s*#*[-/+/*]\s)/ @@ -14,11 +208,9 @@ const HEADER = /(^\s*#+\s)/ const POTENTIAL_CHECKBOX_REGEX = /^(?:(\s*)(?:[-+*]|\d+\.)\s+)\[(.{0,2})\]/ const CHECKBOX_REGEX = /^(?:(\s*)(?:[-+*]|\d+\.)\s+)\[(.{1})\]\s+/ -// β€”β€” Base62 ε·₯具函数 β€”β€” // -const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const DEBUG = false; + // Performance monitor: lightweight sync/async timing collector class PerfMonitor { constructor() { @@ -1377,6 +1569,19 @@ class TimerPlugin extends obsidian.Plugin { // Register settings this.addSettingTab(new TimerSettingTab(this.app, this)); + // Register timer folding decoration to hide HTML in edit mode + this.registerEditorExtension([ + timerFoldingField, + EditorView.decorations.of((view) => { + try { + return view.state.field(timerFoldingField); + } catch (e) { + return null; + } + }), + timerWidgetKeymap + ]); + // Judge enableCheckboxToTimer this.registerEditorExtension( EditorView.updateListener.of((update) => { diff --git a/styles.css b/styles.css index c87169f..38a37ed 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,27 @@ .timer-r { color: #10b981; +} + + +/* Timer widget display in edit mode */ + +.timer-widget-display { + display: inline-block; + padding: 0 4px; + margin: 0 2px; + border-radius: 3px; + background-color: rgba(16, 185, 129, 0.15); + color: #10b981; + font-weight: 500; + cursor: pointer; + user-select: none; + border: 1px solid rgba(16, 185, 129, 0.3); + font-family: system-ui, -apple-system, sans-serif; + font-size: 0.95em; + transition: background-color 0.2s ease; +} + +.timer-widget-display:hover { + background-color: rgba(16, 185, 129, 0.25); + border-color: rgba(16, 185, 129, 0.5); } \ No newline at end of file