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:
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user