一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
117 行
3.2 KiB
TypeScript
117 行
3.2 KiB
TypeScript
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<Decoration>();
|
||
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;
|