import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate } from '@codemirror/view'; import { RangeSetBuilder, Extension } from '@codemirror/state'; export const timestampChipClassName = 'cm-ext-timestamp-chip'; /** * 格式化当前时间为 HH:mm(如 16:30) */ export const getFormattedCurrentTime = (date = new Date()): string => { const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); return `${hours}:${minutes}`; }; /** * 检测光标前是否为 /now 或 /time 关键词,若是则瞬时展开为时间戳 */ export const tryExpandQuickTimestamp = ( view: EditorView, from: number, to: number, triggerChar: string, ): boolean => { if (view.composing || view.state.readOnly) return false; const doc = view.state.doc; const line = doc.lineAt(from); const lineTextBefore = doc.sliceString(line.from, from); let matchLength = 0; if (lineTextBefore.endsWith('/now')) { matchLength = 4; } else if (lineTextBefore.endsWith('/time')) { matchLength = 5; } if (matchLength === 0) return false; const matchFrom = from - matchLength; const timeStr = getFormattedCurrentTime(); const replacement = `[${timeStr}]${triggerChar}`; view.dispatch({ changes: { from: matchFrom, to, insert: replacement }, selection: { anchor: matchFrom + replacement.length }, userEvent: 'input.quickTimestamp', }); return true; }; const timestampDeco = Decoration.mark({ class: timestampChipClassName }); const buildTimestampDecorations = (view: EditorView): DecorationSet => { const builder = new RangeSetBuilder(); const TIMESTAMP_REGEX = /\[\d{1,2}:\d{2}\]/g; for (const { from, to } of view.visibleRanges) { const text = view.state.doc.sliceString(from, to); TIMESTAMP_REGEX.lastIndex = 0; let match: RegExpExecArray | null; while ((match = TIMESTAMP_REGEX.exec(text)) !== null) { const start = from + match.index; const end = start + match[0].length; builder.add(start, end, timestampDeco); } } return builder.finish(); }; const timestampChipPlugin = ViewPlugin.fromClass( class { public decorations: DecorationSet; public constructor(view: EditorView) { this.decorations = buildTimestampDecorations(view); } public update(update: ViewUpdate) { if (update.docChanged || update.viewportChanged) { this.decorations = buildTimestampDecorations(update.view); } } }, { decorations: v => v.decorations, }, ); const quickTimestampExtension = (): Extension => [ EditorView.theme({ [`& .${timestampChipClassName}`]: { fontVariantNumeric: 'tabular-nums', backgroundColor: 'var(--joplin-background-color-hover3, rgba(0, 0, 0, 0.06))', color: 'var(--joplin-color-accent, #007aff)', fontSize: '0.88em', fontWeight: '600', padding: '1px 6px', borderRadius: '4px', marginRight: '3px', letterSpacing: '0.02em', display: 'inline-block', verticalAlign: 'baseline', }, }), EditorView.inputHandler.of((view, from, to, text) => { if (text === ' ' || text === '\t' || text === '\n') { if (tryExpandQuickTimestamp(view, from, to, text)) { return true; } } return false; }), timestampChipPlugin, ]; export default quickTimestampExtension;