feat(editor): add Markdown rendering for ghost text and optimize prompt system

- Implement Markdown parsing for ghost text using Milkdown parser
- Add support for both block and inline content in ghost text
- Refactor prompt system with comprehensive rules and examples
- Adjust LLM parameters: increase temperature to 0.7, add repeat_penalty
- Add CSS styles for formatted ghost text (bold, italic, code, links)
- Add documentation for Copilot prompt system and ghost text rendering
This commit is contained in:
“ydy0615”
2026-02-13 21:17:45 +08:00
parent 65d4a57d33
commit 7cddfaba30
8 changed files with 966 additions and 30 deletions
+122 -2
View File
@@ -1,5 +1,7 @@
import { Plugin, PluginKey, Selection } from '@milkdown/prose/state'
import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils'
import { parserCtx } from '@milkdown/kit/core'
import { Node as ProseNode, Fragment, Slice } from '@milkdown/prose/model'
import type { Ctx } from '@milkdown/kit/core'
import type { EditorView } from '@milkdown/prose/view'
@@ -51,7 +53,74 @@ function clearGhostText(view: EditorView) {
}
}
function insertGhostText(view: EditorView, suggestion: string, from: number) {
function isBlockNode(node: ProseNode): boolean {
return node.type.isBlock && node.type.name !== 'paragraph'
}
function hasBlockNodes(doc: ProseNode): boolean {
let hasBlock = false
doc.forEach((node) => {
if (isBlockNode(node)) {
hasBlock = true
}
})
return hasBlock
}
function addGhostMarkToNode(node: ProseNode, ghostMarkType: any): ProseNode {
if (node.isText) {
return node.mark(node.marks.concat(ghostMarkType.create()))
}
if (node.isLeaf) {
return node
}
const newContent: ProseNode[] = []
node.forEach((child) => {
newContent.push(addGhostMarkToNode(child, ghostMarkType))
})
return node.copy(Fragment.from(newContent))
}
function extractInlineContent(doc: ProseNode, ghostMarkType: any, schema: any): Fragment {
const nodes: ProseNode[] = []
let isFirstBlock = true
doc.forEach((blockNode) => {
if (!isFirstBlock) {
const hardBreak = schema.nodes.hard_break?.create()
if (hardBreak) {
nodes.push(hardBreak)
} else {
nodes.push(schema.text('\n', [ghostMarkType.create()]))
}
}
isFirstBlock = false
blockNode.forEach((inlineNode) => {
if (inlineNode.isText) {
const combinedMarks = inlineNode.marks.concat(ghostMarkType.create())
nodes.push(inlineNode.mark(combinedMarks))
} else if (inlineNode.type.name === 'hard_break') {
nodes.push(inlineNode)
} else if (inlineNode.isLeaf) {
nodes.push(inlineNode)
} else if (inlineNode.content.size > 0) {
inlineNode.forEach((nestedNode) => {
if (nestedNode.isText) {
const combinedMarks = nestedNode.marks.concat(ghostMarkType.create())
nodes.push(nestedNode.mark(combinedMarks))
} else if (nestedNode.isLeaf) {
nodes.push(nestedNode)
}
})
}
})
})
return Fragment.from(nodes)
}
async function insertGhostText(view: EditorView, suggestion: string, from: number) {
if (!currentCtx || !suggestion) return
const schema = view.state.schema
@@ -62,6 +131,47 @@ function insertGhostText(view: EditorView, suggestion: string, from: number) {
return
}
try {
const parser = currentCtx.get(parserCtx)
const parsedDoc = await parser(suggestion)
if (!parsedDoc) {
insertPlainText(view, suggestion, from, markType)
return
}
const containsBlocks = hasBlockNodes(parsedDoc)
if (containsBlocks) {
const $from = view.state.doc.resolve(from)
const insertPos = $from.after($from.depth)
const blockNodes: ProseNode[] = []
parsedDoc.forEach((node) => {
blockNodes.push(addGhostMarkToNode(node, markType))
})
const fragment = Fragment.from(blockNodes)
const tr = view.state.tr
tr.insert(insertPos, fragment)
const endPos = insertPos + fragment.size
tr.setMeta(COPILOT_PLUGIN_KEY, { from: insertPos, to: endPos, suggestion })
view.dispatch(tr)
} else {
const inlineFragment = extractInlineContent(parsedDoc, markType, schema)
const tr = view.state.tr
tr.insert(from, inlineFragment)
const endPos = from + inlineFragment.size
tr.setMeta(COPILOT_PLUGIN_KEY, { from, to: endPos, suggestion })
view.dispatch(tr)
}
} catch (e) {
console.error('[Copilot] Parser error:', e)
insertPlainText(view, suggestion, from, markType)
}
}
function insertPlainText(view: EditorView, suggestion: string, from: number, markType: any) {
const tr = view.state.tr
tr.insertText(suggestion, from)
const endPos = from + suggestion.length
@@ -118,7 +228,17 @@ function acceptSuggestion(view: EditorView) {
const state = COPILOT_PLUGIN_KEY.getState(view.state)
if (!state?.suggestion || state.from >= state.to) return false
const tr = view.state.tr.removeMark(state.from, state.to, view.state.schema.marks.copilot_ghost)
const tr = view.state.tr
const doc = tr.doc
const from = state.from
const to = state.to
doc.nodesBetween(from, to, (node, pos) => {
if (node.marks.some((m: any) => m.type.name === 'copilot_ghost')) {
tr.removeMark(pos, pos + node.nodeSize, view.state.schema.marks.copilot_ghost)
}
})
const endPos = Math.min(state.to, tr.doc.content.size)
tr.setSelection(Selection.near(tr.doc.resolve(endPos)))
tr.setMeta(COPILOT_PLUGIN_KEY, { ...initialState })