feat: 完成扩展资源验收与同步服务部署

This commit is contained in:
2026-09-10 02:57:38 +08:00
parent d703ab64e3
commit a3c098aa58
29 changed files with 464 additions and 112 deletions
@@ -25,17 +25,15 @@ function reconcile(view: EditorView) {
if (view.state.doc.rangeHasMark(start, end, mark)) return
const tr = view.state.tr.delete(end - 1, end).delete(start, start + 1)
tr.removeMark(start, end - 2).addMark(start, end - 2, mark.create())
// Filling an existing pair must keep subsequent letters inside code. Typing
// the closing delimiter explicitly should instead leave code as usual.
// 补全已有成对标记时,后续字母必须留在行内代码中;显式输入结束标记则应像通常一样离开代码范围。
if ($from.pos < end) tr.setStoredMarks([mark.create()])
else tr.removeStoredMark(mark)
view.dispatch(tr)
}
// DOM input can bypass handleTextInput (IME, replacement text, missing event.data).
// Observe it in capture phase, then wait for ProseMirror's DOM observer and its
// composition cleanup before inspecting the document. Never reconcile on load,
// paste, undo, or a selection change alone.
// DOM 输入可能绕过 handleTextInput(输入法、替换文本或缺少 event.data)。
// 在捕获阶段观察输入,等待 ProseMirror DOM 观察器和组合输入清理完成后再检查文档。
// 加载、粘贴、撤销或仅改变选区时不执行协调。
export const inlineCodeInputPlugin = $prose(() => new Plugin({
view(view) {
let timer: ReturnType<typeof setTimeout> | undefined
@@ -67,7 +65,7 @@ export const inlineCodeInputPlugin = $prose(() => new Plugin({
const start = () => { composing = true; cancel() }
const end = () => { composing = false; pending = true; attempts = 0; schedule() }
const keydown = (event: KeyboardEvent) => {
// ProseMirror handles undo/paste itself, so those actions need not emit input.
// ProseMirror 会自行处理撤销和粘贴,因此这些操作无需触发 input
if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Escape'].includes(event.key)) cancel()
else if (pending && !composing && !view.composing && event.key === 'Enter') {
cancel()
+4 -5
View File
@@ -82,7 +82,7 @@ async function loadCodeLanguage(requestedLanguage: string) {
return { shiki, language }
}
// Bounded LRU of dual-theme HTML. Large one-off blocks never remain in the cache.
// 双主题 HTML 使用有界 LRU;大型一次性代码块不会留在缓存中。
const highlightedBlocks = new Map<string, string>()
let highlightedCharacters = 0
const highlightBudget = 1_000_000
@@ -101,7 +101,7 @@ export async function highlightCode(source: string, requestedLanguage = 'text'):
})
const cost = key.length + html.length
if (cost <= highlightBudget / 4) {
// A concurrent caller may already have filled the same entry.
// 并发调用方可能已经填充同一条目。
const previous = highlightedBlocks.get(key)
if (previous !== undefined) { highlightedCharacters -= key.length + previous.length; highlightedBlocks.delete(key) }
while (highlightedBlocks.size && (highlightedBlocks.size >= 64 || highlightedCharacters + cost > highlightBudget)) {
@@ -114,7 +114,7 @@ export async function highlightCode(source: string, requestedLanguage = 'text'):
return html
}
/** Share the initialized grammar/theme registry with editable code blocks. */
/** 与可编辑代码块共享已初始化的语法和主题注册表。 */
export async function getCodeTokenizer(theme: 'github-light' | 'github-dark', requestedLanguage = 'text') {
const { shiki } = await loadCodeLanguage(requestedLanguage)
return (source: string, requestedLanguage: string) => {
@@ -158,8 +158,7 @@ export async function renderMarkdown(source: string, options?: { themeId?: strin
}
const highlighted = await highlightCode(code.textContent ?? '', requestedLanguage)
const fragment = document.createRange().createContextualFragment(highlighted)
// Shiki separates line spans with newlines. Block layout must not render those
// separators as additional blank rows; the untouched source remains available for copy.
// Shiki 用换行符分隔行 span。块布局不能把分隔符渲染成额外空行;复制时仍使用未改动的源码。
for (const node of [...(fragment.querySelector('code')?.childNodes ?? [])]) {
if (node.nodeType === Node.TEXT_NODE && !node.textContent?.trim()) node.remove()
}