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:
Binary file not shown.
+4
-4
@@ -6,7 +6,6 @@ from typing import AsyncGenerator
|
|||||||
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:20b')
|
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:20b')
|
||||||
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434')
|
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434')
|
||||||
|
|
||||||
# 移除 /v1/ 后缀(如果有的话),因为 Ollama Python 包使用原生 API
|
|
||||||
if OLLAMA_HOST.endswith('/v1/'):
|
if OLLAMA_HOST.endswith('/v1/'):
|
||||||
OLLAMA_HOST = OLLAMA_HOST[:-4]
|
OLLAMA_HOST = OLLAMA_HOST[:-4]
|
||||||
elif OLLAMA_HOST.endswith('/v1'):
|
elif OLLAMA_HOST.endswith('/v1'):
|
||||||
@@ -29,9 +28,10 @@ async def stream_openai(prompt: str) -> AsyncGenerator[str, None]:
|
|||||||
messages=[{'role': 'user', 'content': prompt}],
|
messages=[{'role': 'user', 'content': prompt}],
|
||||||
stream=True,
|
stream=True,
|
||||||
options={
|
options={
|
||||||
'num_predict': 8192,
|
'temperature': 0.7,
|
||||||
'temperature': 0.2,
|
'repeat_penalty': 1.1,
|
||||||
}
|
},
|
||||||
|
think='high'
|
||||||
)
|
)
|
||||||
print(f"[LLM] Got stream object, starting iteration...")
|
print(f"[LLM] Got stream object, starting iteration...")
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ async def create_completion(request: CompletionRequest):
|
|||||||
messages=[{'role': 'user', 'content': prompt}],
|
messages=[{'role': 'user', 'content': prompt}],
|
||||||
stream=False,
|
stream=False,
|
||||||
options={
|
options={
|
||||||
'num_predict': 8192,
|
|
||||||
'temperature': 0.2,
|
'temperature': 0.2,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+191
-21
@@ -3,39 +3,209 @@ from typing import Tuple
|
|||||||
|
|
||||||
def build_prompt(prefix: str, suffix: str) -> str:
|
def build_prompt(prefix: str, suffix: str) -> str:
|
||||||
"""
|
"""
|
||||||
改进后的提示词构建函数。
|
优化后的提示词构建函数。
|
||||||
使用更明确的指令来引导模型生成高质量的续写内容。
|
使用明确的分隔符区分指令部分和实际的 prefix/suffix 内容。
|
||||||
"""
|
"""
|
||||||
MAX_CONTEXT_LINES = 30
|
|
||||||
|
|
||||||
# 修正:把suffix的第一个字符移到prefix末尾(解决光标位置偏差)
|
# 修正:把suffix的第一个字符移到prefix末尾(解决光标位置偏差)
|
||||||
if suffix:
|
if suffix:
|
||||||
first_char = suffix[0]
|
first_char = suffix[0]
|
||||||
prefix = prefix + first_char
|
prefix = prefix + first_char
|
||||||
suffix = suffix[1:]
|
suffix = suffix[1:]
|
||||||
|
|
||||||
prefix_lines = prefix.split('\n')
|
recent_prefix = prefix
|
||||||
suffix_lines = suffix.split('\n') if suffix else []
|
recent_suffix = suffix
|
||||||
|
|
||||||
recent_prefix = '\n'.join(prefix_lines[-MAX_CONTEXT_LINES:])
|
prompt = f"""You are an expert writing assistant integrated into a text editor. Your task is to complete the text at the cursor position.
|
||||||
recent_suffix = '\n'.join(suffix_lines[:5])
|
|
||||||
|
|
||||||
prompt = f"""You are an expert writing assistant. Continue the text naturally.
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
RULES
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
CONTEXT:
|
RULE #1: SEAMLESS CONNECTION (MOST CRITICAL)
|
||||||
⟨CURSOR⟩ marks where to continue.
|
|
||||||
- Before ⟨CURSOR⟩: existing text
|
|
||||||
- After ⟨CURSOR⟩: following context (if any)
|
|
||||||
|
|
||||||
RULES:
|
Your continuation MUST seamlessly bridge the prefix and suffix. This is the MOST IMPORTANT rule.
|
||||||
- Match existing style, tone, and terminology
|
|
||||||
- Maintain logical flow
|
|
||||||
- Write only the continuation, nothing else
|
|
||||||
- IMPORTANT: Start continuation directly from ⟨CURSOR⟩ position
|
|
||||||
|
|
||||||
TEXT:
|
The "复读机" (Parrot) Error is when you repeat content that already exists in the suffix. This is the WORST mistake you can make.
|
||||||
{recent_prefix}⟨CURSOR⟩{recent_suffix}
|
|
||||||
|
|
||||||
CONTINUATION:"""
|
Requirements:
|
||||||
|
- Your output must connect prefix to suffix smoothly
|
||||||
|
- NEVER repeat content that already exists in the suffix
|
||||||
|
- If prefix already flows naturally into suffix, output NOTHING (empty string)
|
||||||
|
- The result should read as one coherent text, as if you never interrupted it
|
||||||
|
|
||||||
|
RULE #2: WHITESPACE & PUNCTUATION
|
||||||
|
|
||||||
|
You must carefully check the LAST character of prefix and FIRST character of suffix to ensure perfect docking.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- If prefix ends with space, do NOT start your output with space (prevents double spaces)
|
||||||
|
- If prefix does NOT end with space and suffix starts with a letter, you may need to add a space
|
||||||
|
- If suffix starts with punctuation, do NOT end your output with the same punctuation
|
||||||
|
- Check for existing spaces around operators before adding more
|
||||||
|
|
||||||
|
RULE #3: INDENTATION ALIGNMENT
|
||||||
|
|
||||||
|
You MUST match the indentation level of the current context.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Look at the line where cursor is positioned
|
||||||
|
- Count the leading spaces/tabs on that line
|
||||||
|
- Match that indentation for new lines
|
||||||
|
- Use the SAME type of indentation (spaces OR tabs) as the existing code
|
||||||
|
- For nested blocks, increase indentation appropriately
|
||||||
|
- For closing braces, match the opening brace's indentation
|
||||||
|
|
||||||
|
RULE #4: LIST MAINTENANCE
|
||||||
|
|
||||||
|
When the prefix ends with a list marker, you MUST recognize the pattern and continue it appropriately.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- "- [ ] " indicates an unchecked task → continue with task description
|
||||||
|
- "- [x] " indicates a checked task → continue with completed task description
|
||||||
|
- "1. ", "2. ", etc. indicates ordered list → increment the number
|
||||||
|
- "* " or "- " indicates bullet list → continue with same marker style
|
||||||
|
- "> " indicates blockquote → continue quoted text
|
||||||
|
- Maintain the same list format and indentation level
|
||||||
|
|
||||||
|
RULE #5: SYNTAX CLOSURE
|
||||||
|
|
||||||
|
Before generating content, CHECK if there are unclosed syntax elements. If so, you MUST close them FIRST.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Scan prefix for opening markers: **, *, `, [, 
|
||||||
|
- Image: 
|
||||||
|
|
||||||
|
RULE #6: OUTPUT FORMAT
|
||||||
|
|
||||||
|
Your output will be directly inserted into the document. Output ONLY the continuation text.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Output ONLY the text that should appear at the cursor position
|
||||||
|
- NO explanations, NO comments, NO meta-text
|
||||||
|
- NO code blocks wrapping your output
|
||||||
|
- NO phrases like "Here's the continuation:" or "I'll complete this for you:"
|
||||||
|
- Your output is inserted DIRECTLY into the user's document
|
||||||
|
|
||||||
|
RULE #7: ALWAYS OUTPUT SOMETHING (MANDATORY)
|
||||||
|
|
||||||
|
You MUST always output some content. Empty output is NOT allowed.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Even if the prefix seems complete, you should suggest a natural continuation
|
||||||
|
- If the prefix ends mid-sentence, complete the sentence
|
||||||
|
- If the prefix ends at a natural break point, suggest the next logical content
|
||||||
|
- Examples of valid continuations:
|
||||||
|
- Add the next word or phrase
|
||||||
|
- Complete an incomplete thought
|
||||||
|
- Add a relevant follow-up sentence
|
||||||
|
- Continue a list with the next item
|
||||||
|
- Add closing punctuation if missing
|
||||||
|
- NEVER output an empty string - always provide some useful continuation
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
EXAMPLES
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
EXAMPLE 1 - Seamless Connection:
|
||||||
|
<PREFIX>The quick brown fox </PREFIX>
|
||||||
|
<SUFFIX>jumps over the lazy dog.</SUFFIX>
|
||||||
|
Output: "" (empty - nothing needed, prefix already connects to suffix)
|
||||||
|
Result: "The quick brown fox jumps over the lazy dog."
|
||||||
|
|
||||||
|
EXAMPLE 2 - Seamless Connection with Space:
|
||||||
|
<PREFIX>Hello</PREFIX>
|
||||||
|
<SUFFIX>world!</SUFFIX>
|
||||||
|
Output: " "
|
||||||
|
Result: "Hello world!"
|
||||||
|
|
||||||
|
EXAMPLE 3 - Whitespace Docking:
|
||||||
|
<PREFIX>const a = </PREFIX>
|
||||||
|
<SUFFIX>1;</SUFFIX>
|
||||||
|
Output: "1;"
|
||||||
|
Result: "const a = 1;"
|
||||||
|
|
||||||
|
EXAMPLE 4 - Indentation Alignment:
|
||||||
|
<PREFIX>function test() {{\\n if (true) {{\\n console.log('hi');\\n </PREFIX>
|
||||||
|
<SUFFIX>\\n}}</SUFFIX>
|
||||||
|
Output: "}}\\n}}"
|
||||||
|
Result: " }}\\n}}" (correctly closes if with 4 spaces, then function)
|
||||||
|
|
||||||
|
EXAMPLE 5 - Task List:
|
||||||
|
<PREFIX>## TODO\\n- [ ] Buy groceries\\n- [ ] </PREFIX>
|
||||||
|
<SUFFIX></SUFFIX>
|
||||||
|
Output: "Call mom"
|
||||||
|
Result: "## TODO\\n- [ ] Buy groceries\\n- [ ] Call mom"
|
||||||
|
|
||||||
|
EXAMPLE 6 - Ordered List:
|
||||||
|
<PREFIX>1. First item\\n2. Second item\\n</PREFIX>
|
||||||
|
<SUFFIX></SUFFIX>
|
||||||
|
Output: "3. Third item"
|
||||||
|
Result: "1. First item\\n2. Second item\\n3. Third item"
|
||||||
|
|
||||||
|
EXAMPLE 7 - Bullet List:
|
||||||
|
<PREFIX>* Apple\\n* Banana\\n* </PREFIX>
|
||||||
|
<SUFFIX></SUFFIX>
|
||||||
|
Output: "Cherry"
|
||||||
|
Result: "* Apple\\n* Banana\\n* Cherry"
|
||||||
|
|
||||||
|
EXAMPLE 8 - Unclosed Bold:
|
||||||
|
<PREFIX>This is **important</PREFIX>
|
||||||
|
<SUFFIX> text continues here.</SUFFIX>
|
||||||
|
Output: "** "
|
||||||
|
Result: "This is **important** text continues here."
|
||||||
|
|
||||||
|
EXAMPLE 9 - Unclosed Link:
|
||||||
|
<PREFIX>Click [here for more</PREFIX>
|
||||||
|
<SUFFIX> information.</SUFFIX>
|
||||||
|
Output: "](https://example.com)"
|
||||||
|
Result: "Click [here for more](https://example.com) information."
|
||||||
|
|
||||||
|
EXAMPLE 10 - Unclosed Code Block:
|
||||||
|
<PREFIX>```python\\ndef hello():</PREFIX>
|
||||||
|
<SUFFIX>\\nprint('done')</SUFFIX>
|
||||||
|
Output: "\\n print('hello')\\n```"
|
||||||
|
Result: Code block properly closed with ```
|
||||||
|
|
||||||
|
EXAMPLE 11 - Clean Output:
|
||||||
|
For any completion, output ONLY the continuation text:
|
||||||
|
Output: "Hello world!"
|
||||||
|
NOT: "Here's what comes next: Hello world!"
|
||||||
|
NOT: "```Hello world```"
|
||||||
|
NOT: "I'll complete this for you: Hello world!"
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
FINAL CHECKLIST
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Before outputting, verify:
|
||||||
|
□ Does my output connect prefix and suffix WITHOUT repeating suffix content?
|
||||||
|
□ Are there no double spaces or missing spaces between prefix and suffix?
|
||||||
|
□ Does my indentation match the context?
|
||||||
|
□ If there's a list marker, did I continue the list pattern?
|
||||||
|
□ Did I close any unclosed Markdown syntax?
|
||||||
|
□ Is my output ONLY the continuation text, nothing else?
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
NOW COMPLETE THE FOLLOWING TEXT
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
<PREFIX>
|
||||||
|
{recent_prefix}
|
||||||
|
</PREFIX>
|
||||||
|
|
||||||
|
<SUFFIX>
|
||||||
|
{recent_suffix}
|
||||||
|
</SUFFIX>
|
||||||
|
|
||||||
|
Output:"""
|
||||||
|
|
||||||
return prompt.strip()
|
return prompt.strip()
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# GitHub Copilot 提示词系统分析
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
GitHub Copilot 的提示词系统是一个复杂的代码补全引擎,采用声明式组件架构来构建发送给 LLM 的提示词。本文档基于 `completions-sample-code/` 目录的源代码分析。
|
||||||
|
|
||||||
|
## 核心架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Input
|
||||||
|
A[用户光标位置] --> B[CompletionState]
|
||||||
|
C[当前文档] --> B
|
||||||
|
D[相似文件] --> B
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PromptFactory
|
||||||
|
B --> E[VirtualPrompt]
|
||||||
|
E --> F[组件树构建]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Components
|
||||||
|
F --> G[CompletionsContext]
|
||||||
|
G --> H[DocumentMarker]
|
||||||
|
G --> I[Traits]
|
||||||
|
G --> J[Diagnostics]
|
||||||
|
G --> K[CodeSnippets]
|
||||||
|
G --> L[SimilarFiles]
|
||||||
|
G --> M[RecentEdits]
|
||||||
|
F --> N[CurrentFile]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Rendering
|
||||||
|
H --> O[CompletionsPromptRenderer]
|
||||||
|
I --> O
|
||||||
|
J --> O
|
||||||
|
K --> O
|
||||||
|
L --> O
|
||||||
|
M --> O
|
||||||
|
N --> O
|
||||||
|
O --> P[Prompt对象]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Output
|
||||||
|
P --> Q[API请求]
|
||||||
|
Q --> R[LLM补全]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. 提示词基础配置
|
||||||
|
|
||||||
|
### 1.1 Token 限制
|
||||||
|
|
||||||
|
来源: [`prompt/src/prompt.ts`](../completions-sample-code/prompt/src/prompt.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 最大补全长度
|
||||||
|
export const DEFAULT_MAX_COMPLETION_LENGTH = 500;
|
||||||
|
|
||||||
|
// 最大提示词长度 (模型上下文窗口 - 补全长度)
|
||||||
|
export const DEFAULT_MAX_PROMPT_LENGTH = 8192 - DEFAULT_MAX_COMPLETION_LENGTH;
|
||||||
|
|
||||||
|
// 默认代码片段数量
|
||||||
|
export const DEFAULT_NUM_SNIPPETS = 4;
|
||||||
|
|
||||||
|
// 后缀匹配阈值
|
||||||
|
export const DEFAULT_SUFFIX_MATCH_THRESHOLD = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 提示词分配比例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const DEFAULT_PROMPT_ALLOCATION_PERCENT = {
|
||||||
|
prefix: 35, // 光标前代码
|
||||||
|
suffix: 15, // 光标后代码
|
||||||
|
stableContext: 35, // 稳定上下文
|
||||||
|
volatileContext: 15 // 动态上下文
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 语言标记系统
|
||||||
|
|
||||||
|
### 2.1 支持的语言
|
||||||
|
|
||||||
|
来源: [`prompt/src/languageMarker.ts`](../completions-sample-code/prompt/src/languageMarker.ts)
|
||||||
|
|
||||||
|
支持 60+ 种编程语言,每种语言定义了:
|
||||||
|
- `lineComment`: 单行注释标记 (start, end)
|
||||||
|
- `markdownLanguageIds`: Markdown 代码块语言标识符
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```typescript
|
||||||
|
python: {
|
||||||
|
lineComment: { start: '#', end: '' },
|
||||||
|
markdownLanguageIds: ['python', 'py', 'gyp'],
|
||||||
|
},
|
||||||
|
javascript: {
|
||||||
|
lineComment: { start: '//', end: '' },
|
||||||
|
markdownLanguageIds: ['javascript', 'js'],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 语言标记生成
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取语言标记
|
||||||
|
export function getLanguageMarker(doc: DocumentInfo): string {
|
||||||
|
if (dontAddLanguageMarker.indexOf(languageId) === -1 && !hasLanguageMarker(doc)) {
|
||||||
|
if (languageId in shebangLines) {
|
||||||
|
return shebangLines[languageId]; // 如 #!/usr/bin/env python3
|
||||||
|
} else {
|
||||||
|
return `Language: ${languageId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取路径标记
|
||||||
|
export function getPathMarker(doc: DocumentInfo): string {
|
||||||
|
if (doc.relativePath) {
|
||||||
|
return `Path: ${doc.relativePath}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 组件系统架构
|
||||||
|
|
||||||
|
### 3.1 声明式组件
|
||||||
|
|
||||||
|
来源: [`prompt/src/components/components.ts`](../completions-sample-code/prompt/src/components/components.ts)
|
||||||
|
|
||||||
|
Copilot 使用类似 React 的 JSX 语法来声明提示词组件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基础组件类型
|
||||||
|
export type PromptElementProps<P = object> = P & Readonly<PromptAttributes & { children?: PromptComponentChildren }>;
|
||||||
|
|
||||||
|
// 组件上下文,提供状态管理
|
||||||
|
export interface ComponentContext {
|
||||||
|
useState<S>(initialState: S): [S, Dispatch<StateUpdater<S>>];
|
||||||
|
useData<T>(typePredicate: TypePredicate<T>, consumer: DataConsumer<T>): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 默认提示词组件结构
|
||||||
|
|
||||||
|
来源: [`lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx`](../completions-sample-code/lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function defaultCompletionsPrompt(accessor: ServicesAccessor) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CompletionsContext>
|
||||||
|
<DocumentMarker tdms={tdms} weight={0.7} />
|
||||||
|
<Traits weight={0.6} />
|
||||||
|
<Diagnostics tdms={tdms} weight={0.65} />
|
||||||
|
<CodeSnippets tdms={tdms} weight={0.9} />
|
||||||
|
<SimilarFiles tdms={tdms} instantiationService={instantiationService} weight={0.8} />
|
||||||
|
<RecentEdits tdms={tdms} recentEditsProvider={recentEditsProvider} weight={0.99} />
|
||||||
|
</CompletionsContext>
|
||||||
|
<CurrentFile weight={1} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 组件权重说明
|
||||||
|
|
||||||
|
| 组件 | 权重 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| RecentEdits | 0.99 | 最近编辑内容,最高优先级 |
|
||||||
|
| CodeSnippets | 0.9 | 代码片段 |
|
||||||
|
| SimilarFiles | 0.8 | 相似文件内容 |
|
||||||
|
| DocumentMarker | 0.7 | 文档标记(语言/路径) |
|
||||||
|
| Diagnostics | 0.65 | 诊断信息(错误/警告) |
|
||||||
|
| Traits | 0.6 | 代码特征 |
|
||||||
|
| CurrentFile | 1.0 | 当前文件内容(必须包含) |
|
||||||
|
|
||||||
|
## 4. 当前文件组件
|
||||||
|
|
||||||
|
来源: [`lib/src/prompt/components/currentFile.tsx`](../completions-sample-code/lib/src/prompt/components/currentFile.tsx)
|
||||||
|
|
||||||
|
### 4.1 光标前代码 (BeforeCursor)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function BeforeCursor(props: {
|
||||||
|
document: CompletionRequestDocument | undefined;
|
||||||
|
position: Position | undefined;
|
||||||
|
maxCharacters: number;
|
||||||
|
}) {
|
||||||
|
let text = props.document.getText({ start: { line: 0, character: 0 }, end: props.position });
|
||||||
|
if (text.length > props.maxCharacters) {
|
||||||
|
text = text.slice(-props.maxCharacters); // 截取最后 maxCharacters 字符
|
||||||
|
}
|
||||||
|
return <Text>{text}</Text>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 光标后代码 (AfterCursor)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function AfterCursor(props: {...}, context: ComponentContext) {
|
||||||
|
// 获取光标后所有文本
|
||||||
|
let suffix = props.document.getText({
|
||||||
|
start: props.position,
|
||||||
|
end: { line: Number.MAX_VALUE, character: Number.MAX_VALUE },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 后缀缓存机制:使用编辑距离判断是否复用缓存
|
||||||
|
const dist = findEditDistanceScore(firstSuffixTokens.tokens, cachedSuffixTokens.tokens);
|
||||||
|
if (100 * dist < suffixMatchThreshold * tokens.length) {
|
||||||
|
suffixToUse = cachedSuffix; // 使用缓存的后缀
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text>{suffixToUse}</Text>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 相似文件与代码片段
|
||||||
|
|
||||||
|
### 5.1 相似文件选择
|
||||||
|
|
||||||
|
来源: [`prompt/src/snippetInclusion/similarFiles.ts`](../completions-sample-code/prompt/src/snippetInclusion/similarFiles.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SimilarFilesOptions {
|
||||||
|
snippetLength: number; // 代码片段长度(行数)
|
||||||
|
threshold: number; // 相似度阈值
|
||||||
|
maxTopSnippets: number; // 最大返回片段数
|
||||||
|
maxCharPerFile: number; // 每文件最大字符数
|
||||||
|
maxNumberOfFiles: number; // 最大文件数
|
||||||
|
maxSnippetsPerFile: number; // 每文件最大片段数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
export const defaultSimilarFilesOptions: SimilarFilesOptions = {
|
||||||
|
snippetLength: 60,
|
||||||
|
threshold: 0.0,
|
||||||
|
maxTopSnippets: 4,
|
||||||
|
maxCharPerFile: 10000,
|
||||||
|
maxNumberOfFiles: 20,
|
||||||
|
maxSnippetsPerFile: 1,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Jaccard 相似度匹配
|
||||||
|
|
||||||
|
来源: [`prompt/src/snippetInclusion/selectRelevance.ts`](../completions-sample-code/prompt/src/snippetInclusion/selectRelevance.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用 Jaccard 相似度计算代码片段相关性
|
||||||
|
abstract class WindowedMatcher {
|
||||||
|
protected abstract similarityScore(a: Set<string>, b: Set<string>): number;
|
||||||
|
|
||||||
|
// 分词器:将代码转换为 token 集合
|
||||||
|
class Tokenizer {
|
||||||
|
tokenize(a: string): Set<string> {
|
||||||
|
return new Set(splitIntoWords(a).filter(x => !this.stopsForLanguage.has(x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 代码片段格式化
|
||||||
|
|
||||||
|
来源: [`prompt/src/snippetInclusion/snippets.ts`](../completions-sample-code/prompt/src/snippetInclusion/snippets.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function announceSnippet(snippet: SnippetToAnnounce) {
|
||||||
|
const headline = snippet.relativePath
|
||||||
|
? `Compare ${pluralizedSemantics} ${semantics} from ${snippet.relativePath}:`
|
||||||
|
: `Compare ${pluralizedSemantics} ${semantics}:`;
|
||||||
|
return { headline, snippet: snippet.snippet };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. API 请求格式
|
||||||
|
|
||||||
|
### 6.1 请求结构
|
||||||
|
|
||||||
|
来源: [`lib/src/openai/fetch.ts`](../completions-sample-code/lib/src/openai/fetch.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CompletionRequest = {
|
||||||
|
prompt: string; // 前缀代码
|
||||||
|
suffix: string; // 后缀代码
|
||||||
|
stream: true; // 始终使用流式响应
|
||||||
|
max_tokens: number; // 最大生成 token 数
|
||||||
|
n: number; // 并行补全数量
|
||||||
|
temperature: number; // 温度参数
|
||||||
|
top_p: number; // nucleus 采样参数
|
||||||
|
stop: string[]; // 停止标记
|
||||||
|
logprobs?: number; // logprob 数量
|
||||||
|
extra: {
|
||||||
|
language: string; // 语言 ID
|
||||||
|
trim_by_indentation?: boolean;
|
||||||
|
force_indent?: number;
|
||||||
|
next_indent?: number;
|
||||||
|
prompt_tokens: number;
|
||||||
|
suffix_tokens: number;
|
||||||
|
context?: string[]; // 额外上下文
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 停止标记
|
||||||
|
|
||||||
|
来源: [`lib/src/openai/openai.ts`](../completions-sample-code/lib/src/openai/openai.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const stopsForLanguage: { [key: string]: string[] } = {
|
||||||
|
markdown: ['\n\n\n'],
|
||||||
|
python: ['\ndef ', '\nclass ', '\nif ', '\n\n#'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getStops(languageId?: string) {
|
||||||
|
return stopsForLanguage[languageId ?? ''] ?? ['\n\n\n', '\n```'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 温度参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function getTemperatureForSamples(numShots: number): number {
|
||||||
|
if (numShots <= 1) return 0.0;
|
||||||
|
else if (numShots < 10) return 0.2;
|
||||||
|
else if (numShots < 20) return 0.4;
|
||||||
|
else return 0.8;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Tokenization
|
||||||
|
|
||||||
|
来源: [`prompt/src/tokenization/tokenizer.ts`](../completions-sample-code/prompt/src/tokenization/tokenizer.ts)
|
||||||
|
|
||||||
|
### 7.1 支持的 Tokenizer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export enum TokenizerName {
|
||||||
|
cl100k = 'cl100k_base', // GPT-3.5/GPT-4
|
||||||
|
o200k = 'o200k_base', // GPT-4o
|
||||||
|
mock = 'mock', // 测试用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Tokenizer 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Tokenizer {
|
||||||
|
tokenLength(text: string): number;
|
||||||
|
tokenize(text: string): number[];
|
||||||
|
detokenize(tokens: number[]): string;
|
||||||
|
tokenizeStrings(text: string): string[];
|
||||||
|
takeLastTokens(text: string, n: number): { text: string; tokens: number[] };
|
||||||
|
takeFirstTokens(text: string, n: number): { text: string; tokens: number[] };
|
||||||
|
takeLastLinesTokens(text: string, n: number): string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Tree-sitter 代码解析
|
||||||
|
|
||||||
|
来源: [`prompt/src/parse.ts`](../completions-sample-code/prompt/src/parse.ts)
|
||||||
|
|
||||||
|
### 8.1 支持的语言
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export enum WASMLanguage {
|
||||||
|
Python = 'python',
|
||||||
|
JavaScript = 'javascript',
|
||||||
|
TypeScript = 'typescript',
|
||||||
|
TSX = 'tsx',
|
||||||
|
Go = 'go',
|
||||||
|
Ruby = 'ruby',
|
||||||
|
CSharp = 'c-sharp',
|
||||||
|
Java = 'java',
|
||||||
|
Php = 'php',
|
||||||
|
Cpp = 'cpp',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 用途
|
||||||
|
|
||||||
|
- 判断代码块是否为空块开始 (`isEmptyBlockStart`)
|
||||||
|
- 判断代码块是否完成 (`isBlockBodyFinished`)
|
||||||
|
- 获取语法节点起始位置 (`getNodeStart`)
|
||||||
|
|
||||||
|
## 9. 提示词构建流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant VSCode as VS Code
|
||||||
|
participant GT as GhostText
|
||||||
|
participant PF as PromptFactory
|
||||||
|
participant Components as 组件系统
|
||||||
|
participant Tokenizer as Tokenizer
|
||||||
|
participant API as OpenAI API
|
||||||
|
|
||||||
|
User->>VSCode: 输入代码
|
||||||
|
VSCode->>GT: 请求补全
|
||||||
|
GT->>PF: extractPrompt
|
||||||
|
PF->>Components: 构建组件树
|
||||||
|
|
||||||
|
Components->>Components: DocumentMarker
|
||||||
|
Components->>Components: Traits
|
||||||
|
Components->>Components: Diagnostics
|
||||||
|
Components->>Components: CodeSnippets
|
||||||
|
Components->>Components: SimilarFiles
|
||||||
|
Components->>Components: RecentEdits
|
||||||
|
Components->>Components: CurrentFile
|
||||||
|
|
||||||
|
Components->>Tokenizer: 计算 token 数量
|
||||||
|
Tokenizer-->>Components: 返回 token 数
|
||||||
|
|
||||||
|
Components->>Components: Elision 省略处理
|
||||||
|
Components-->>PF: Prompt 对象
|
||||||
|
PF-->>GT: PromptResponse
|
||||||
|
GT->>API: 发送请求
|
||||||
|
API-->>GT: 流式返回补全
|
||||||
|
GT-->>VSCode: 显示 Ghost Text
|
||||||
|
VSCode-->>User: 展示建议
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 关键设计模式
|
||||||
|
|
||||||
|
### 10.1 声明式组件
|
||||||
|
|
||||||
|
使用 JSX 语法声明提示词结构,支持:
|
||||||
|
- 组件组合
|
||||||
|
- 权重分配
|
||||||
|
- 状态管理
|
||||||
|
- 数据订阅
|
||||||
|
|
||||||
|
### 10.2 虚拟提示词树
|
||||||
|
|
||||||
|
在渲染前构建虚拟树结构,支持:
|
||||||
|
- 增量更新
|
||||||
|
- 高效 diff
|
||||||
|
- 条件渲染
|
||||||
|
|
||||||
|
### 10.3 Token 预算管理
|
||||||
|
|
||||||
|
- 每个组件有权重属性
|
||||||
|
- 根据 token 预算动态省略内容
|
||||||
|
- 优先保留高权重组件
|
||||||
|
|
||||||
|
### 10.4 后缀缓存
|
||||||
|
|
||||||
|
- 使用编辑距离判断后缀相似度
|
||||||
|
- 相似时复用缓存的后缀
|
||||||
|
- 减少 token 波动,提高缓存命中率
|
||||||
|
|
||||||
|
## 11. 实现参考
|
||||||
|
|
||||||
|
如果要在自己的项目中实现类似的提示词系统,需要关注以下核心模块:
|
||||||
|
|
||||||
|
1. **Tokenizer**: 使用 tiktoken 进行准确的 token 计数
|
||||||
|
2. **语言标记**: 为不同语言生成适当的标记
|
||||||
|
3. **上下文收集**: 收集相似文件、最近编辑等上下文
|
||||||
|
4. **Token 预算**: 动态分配 token 给不同组件
|
||||||
|
5. **FIM 格式**: 使用 Fill-In-the-Middle 格式发送请求
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
GitHub Copilot 的提示词系统是一个精心设计的工程系统,核心特点包括:
|
||||||
|
|
||||||
|
1. **模块化组件架构**: 使用声明式组件构建提示词
|
||||||
|
2. **智能上下文选择**: 通过 Jaccard 相似度选择相关代码片段
|
||||||
|
3. **Token 预算管理**: 动态分配 token 给不同优先级的内容
|
||||||
|
4. **多语言支持**: 支持 60+ 种编程语言
|
||||||
|
5. **Tree-sitter 解析**: 精确理解代码结构
|
||||||
|
6. **流式响应**: 实时返回补全结果
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# 虚拟文本 Markdown 渲染解决方案
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
当前虚拟文本(灰色字)无法正确渲染 Markdown 和换行符,根本原因是:
|
||||||
|
|
||||||
|
1. **纯文本插入**:`insertGhostText` 使用 `tr.insertText()` 直接插入纯文本
|
||||||
|
2. **绕过解析器**:文本未经过 Milkdown 的 Markdown 解析流程
|
||||||
|
3. **节点结构错误**:`\n` 字符被当作普通字符,而非创建新段落节点
|
||||||
|
|
||||||
|
## 解决方案架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph 当前流程
|
||||||
|
A1[LLM 返回 Markdown] --> B1[insertText 直接插入]
|
||||||
|
B1 --> C1[添加 copilot_ghost mark]
|
||||||
|
C1 --> D1[显示为灰色纯文本]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 新流程
|
||||||
|
A2[LLM 返回 Markdown] --> B2[调用 parserCtx 解析]
|
||||||
|
B2 --> C2[生成 ProseMirror 节点]
|
||||||
|
C2 --> D2[为所有节点添加 ghost 属性]
|
||||||
|
D2 --> E2[插入到文档]
|
||||||
|
E2 --> F2[显示为格式化灰色文本]
|
||||||
|
end
|
||||||
|
|
||||||
|
style D1 fill:#f99
|
||||||
|
style F2 fill:#9f9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术方案
|
||||||
|
|
||||||
|
### 方案一:使用 Milkdown Parser 解析(推荐)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 完整支持 Markdown 语法
|
||||||
|
- 与编辑器行为一致
|
||||||
|
- 自动处理换行
|
||||||
|
|
||||||
|
**实现步骤**:
|
||||||
|
|
||||||
|
1. 获取 `parserCtx` 从 Milkdown 上下文
|
||||||
|
2. 使用 parser 将 Markdown 解析为 ProseMirror Fragment
|
||||||
|
3. 遍历所有节点,添加 `copilot_ghost` mark
|
||||||
|
4. 使用 `tr.replaceWith()` 插入节点
|
||||||
|
|
||||||
|
### 方案二:使用 Decoration API(备选)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 不修改实际文档内容
|
||||||
|
- 更轻量级
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 实现复杂
|
||||||
|
- 可能与某些功能冲突
|
||||||
|
|
||||||
|
## 详细实现计划
|
||||||
|
|
||||||
|
### 步骤 1:修改 copilotPlugin.ts
|
||||||
|
|
||||||
|
需要修改以下部分:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 新增导入
|
||||||
|
import { parserCtx } from '@milkdown/kit/core'
|
||||||
|
|
||||||
|
// 修改 insertGhostText 函数
|
||||||
|
async function insertGhostText(view: EditorView, suggestion: string, from: number) {
|
||||||
|
if (!currentCtx || !suggestion) return
|
||||||
|
|
||||||
|
const schema = view.state.schema
|
||||||
|
const markType = schema.marks.copilot_ghost
|
||||||
|
|
||||||
|
if (!markType) return
|
||||||
|
|
||||||
|
// 使用 parser 解析 Markdown
|
||||||
|
const parser = currentCtx.get(parserCtx)
|
||||||
|
const doc = await parser(suggestion)
|
||||||
|
|
||||||
|
if (!doc) return
|
||||||
|
|
||||||
|
// 为所有文本节点添加 ghost mark
|
||||||
|
const ghostDoc = doc.descendants((node, pos) => {
|
||||||
|
if (node.isText) {
|
||||||
|
// 添加 mark
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 插入节点
|
||||||
|
const tr = view.state.tr
|
||||||
|
tr.replaceWith(from, from, ghostDoc.content)
|
||||||
|
tr.setMeta(COPILOT_PLUGIN_KEY, { from, to: from + doc.content.size, suggestion })
|
||||||
|
view.dispatch(tr)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:处理换行符
|
||||||
|
|
||||||
|
换行符处理策略:
|
||||||
|
|
||||||
|
| 换行类型 | 处理方式 |
|
||||||
|
|---------|---------|
|
||||||
|
| 单个 `\n` | 创建 `hard_break` 节点 |
|
||||||
|
| 双个 `\n\n` | 创建新段落节点 |
|
||||||
|
| 列表项换行 | 创建新列表项节点 |
|
||||||
|
|
||||||
|
### 步骤 3:样式处理
|
||||||
|
|
||||||
|
需要修改 CSS 以支持格式化的虚拟文本:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.copilot-ghost-text {
|
||||||
|
color: #999;
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 虚拟文本内的格式化元素 */
|
||||||
|
.copilot-ghost-text strong,
|
||||||
|
.copilot-ghost-text em,
|
||||||
|
.copilot-ghost-text code {
|
||||||
|
opacity: inherit;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4:状态管理
|
||||||
|
|
||||||
|
需要跟踪虚拟节点的范围,以便:
|
||||||
|
- Tab 键接受时正确移除 mark
|
||||||
|
- 用户输入时正确清除虚拟内容
|
||||||
|
- 导出时正确处理虚拟文本
|
||||||
|
|
||||||
|
## 文件修改清单
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `src/plugins/copilotPlugin.ts` | 重构 insertGhostText,添加解析逻辑 |
|
||||||
|
| `src/components/MilkdownEditor.vue` | 更新 CSS 样式 |
|
||||||
|
| `src/plugins/types.ts` | 可能需要更新类型定义 |
|
||||||
|
|
||||||
|
## 风险与注意事项
|
||||||
|
|
||||||
|
1. **性能考虑**:解析 Markdown 可能有延迟,需要考虑用户体验
|
||||||
|
2. **嵌套处理**:复杂的 Markdown 结构(如嵌套列表)需要特殊处理
|
||||||
|
3. **撤销/重做**:确保虚拟文本的接受/拒绝正确处理 undo stack
|
||||||
|
4. **光标位置**:插入多段落内容后光标位置需要正确设置
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- [ ] Markdown 语法正确渲染(粗体、斜体、代码等)
|
||||||
|
- [ ] 换行符正确转换为段落
|
||||||
|
- [ ] Tab 键接受功能正常
|
||||||
|
- [ ] Escape 键拒绝功能正常
|
||||||
|
- [ ] 导出时虚拟文本正确处理
|
||||||
|
- [ ] 性能无明显下降
|
||||||
@@ -315,10 +315,27 @@ onUnmounted(() => {
|
|||||||
color: #999;
|
color: #999;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.copilot-ghost-text.copilot-loading {
|
.copilot-ghost-text.copilot-loading {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copilot-ghost-text strong,
|
||||||
|
.copilot-ghost-text em,
|
||||||
|
.copilot-ghost-text code,
|
||||||
|
.copilot-ghost-text a {
|
||||||
|
color: inherit;
|
||||||
|
opacity: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copilot-ghost-text code {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copilot-ghost-text a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Plugin, PluginKey, Selection } from '@milkdown/prose/state'
|
import { Plugin, PluginKey, Selection } from '@milkdown/prose/state'
|
||||||
import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils'
|
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 { Ctx } from '@milkdown/kit/core'
|
||||||
import type { EditorView } from '@milkdown/prose/view'
|
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
|
if (!currentCtx || !suggestion) return
|
||||||
|
|
||||||
const schema = view.state.schema
|
const schema = view.state.schema
|
||||||
@@ -62,6 +131,47 @@ function insertGhostText(view: EditorView, suggestion: string, from: number) {
|
|||||||
return
|
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
|
const tr = view.state.tr
|
||||||
tr.insertText(suggestion, from)
|
tr.insertText(suggestion, from)
|
||||||
const endPos = from + suggestion.length
|
const endPos = from + suggestion.length
|
||||||
@@ -118,7 +228,17 @@ function acceptSuggestion(view: EditorView) {
|
|||||||
const state = COPILOT_PLUGIN_KEY.getState(view.state)
|
const state = COPILOT_PLUGIN_KEY.getState(view.state)
|
||||||
if (!state?.suggestion || state.from >= state.to) return false
|
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)
|
const endPos = Math.min(state.to, tr.doc.content.size)
|
||||||
tr.setSelection(Selection.near(tr.doc.resolve(endPos)))
|
tr.setSelection(Selection.near(tr.doc.resolve(endPos)))
|
||||||
tr.setMeta(COPILOT_PLUGIN_KEY, { ...initialState })
|
tr.setMeta(COPILOT_PLUGIN_KEY, { ...initialState })
|
||||||
|
|||||||
Reference in New Issue
Block a user