@monaco-ai-editor/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1074 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +295 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.js +1032 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/ai/aiCompletion.ts +310 -0
- package/src/bus/EditorBus.ts +102 -0
- package/src/bus/EventEmitter.ts +33 -0
- package/src/completion/sqlCompletion.ts +151 -0
- package/src/constants.ts +71 -0
- package/src/controllers/AiChatController.ts +198 -0
- package/src/controllers/EditorController.ts +231 -0
- package/src/index.ts +47 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/bus/EventEmitter.ts","../src/completion/sqlCompletion.ts","../src/ai/aiCompletion.ts","../src/controllers/EditorController.ts","../src/controllers/AiChatController.ts","../src/bus/EditorBus.ts"],"sourcesContent":["export const SQL_KEYWORDS = [\n 'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',\n 'CREATE', 'TABLE', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'INDEX', 'VIEW', 'DATABASE',\n 'SCHEMA', 'TRUNCATE', 'RENAME', 'AS', 'ON', 'JOIN', 'INNER', 'LEFT', 'RIGHT',\n 'FULL', 'OUTER', 'CROSS', 'NATURAL', 'USING', 'AND', 'OR', 'NOT', 'IN', 'EXISTS',\n 'BETWEEN', 'LIKE', 'IS', 'NULL', 'TRUE', 'FALSE', 'CASE', 'WHEN', 'THEN', 'ELSE',\n 'END', 'IF', 'UNION', 'ALL', 'DISTINCT', 'ORDER', 'BY', 'ASC', 'DESC', 'GROUP',\n 'HAVING', 'LIMIT', 'OFFSET', 'FETCH', 'NEXT', 'ROWS', 'ONLY', 'WITH', 'RECURSIVE',\n 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE', 'NOT NULL', 'DEFAULT',\n 'CHECK', 'CONSTRAINT', 'AUTO_INCREMENT', 'IDENTITY', 'SEQUENCE', 'GRANT',\n 'REVOKE', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'BEGIN', 'SAVEPOINT', 'EXPLAIN',\n];\n\nexport const SQL_FUNCTIONS = [\n 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COALESCE', 'NULLIF', 'IFNULL', 'NVL',\n 'CONCAT', 'LENGTH', 'SUBSTR', 'SUBSTRING', 'TRIM', 'LTRIM', 'RTRIM', 'UPPER',\n 'LOWER', 'REPLACE', 'INSTR', 'LPAD', 'RPAD', 'CHAR_LENGTH', 'POSITION',\n 'NOW', 'CURDATE', 'CURTIME', 'DATE', 'TIME', 'YEAR', 'MONTH', 'DAY',\n 'HOUR', 'MINUTE', 'SECOND', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'DATEADD',\n 'DATESUB', 'EXTRACT', 'TO_DATE', 'TO_CHAR', 'DATE_FORMAT', 'TIMESTAMPDIFF',\n 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MOD', 'POWER', 'SQRT', 'SIGN', 'RAND',\n 'CAST', 'CONVERT', 'ROW_NUMBER', 'RANK', 'DENSE_RANK', 'NTILE', 'LAG', 'LEAD',\n 'FIRST_VALUE', 'LAST_VALUE', 'OVER', 'PARTITION', 'LISTAGG', 'GROUP_CONCAT',\n 'STRING_AGG', 'JSON_OBJECT', 'JSON_ARRAY', 'JSON_EXTRACT', 'ISNULL',\n];\n\nexport const SQL_DATA_TYPES = [\n 'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'DECIMAL', 'NUMERIC',\n 'FLOAT', 'DOUBLE', 'REAL', 'CHAR', 'VARCHAR', 'TEXT', 'NCHAR', 'NVARCHAR',\n 'NTEXT', 'DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL',\n 'BLOB', 'CLOB', 'BINARY', 'VARBINARY', 'JSON', 'UUID', 'SERIAL',\n];\n\nexport const LANGUAGES = [\n 'javascript',\n 'typescript',\n 'python',\n 'json',\n 'css',\n 'html',\n 'markdown',\n 'rust',\n 'go',\n 'sql',\n] as const;\n\n/** Monaco 编辑器支持的 UI 本地化语言 */\nexport const LOCALES = {\n ZH_CN: 'zh-cn',\n EN: 'en',\n DE: 'de',\n ES: 'es',\n FR: 'fr',\n IT: 'it',\n JA: 'ja',\n KO: 'ko',\n RU: 'ru',\n} as const;\n\nexport type Locale = (typeof LOCALES)[keyof typeof LOCALES];\n\nexport const THEMES = [\n { value: 'vs-dark', label: 'Dark' },\n { value: 'light', label: 'Light' },\n { value: 'hc-black', label: 'High Contrast' },\n] as const;\n\nexport const AI_INLINE_LANGUAGES = [\n 'typescript', 'javascript', 'python', 'sql',\n 'java', 'cpp', 'csharp', 'go', 'rust',\n];\n","/**\n * 极简事件发射器,框架无关。\n * 用于 EditorController / AiChatController / EditorBus 之间的事件通信。\n */\nexport type Listener<T> = (payload: T) => void;\n\nexport class EventEmitter<EventMap> {\n private listeners: { [K in keyof EventMap]?: Set<Listener<EventMap[K]>> } = {};\n\n on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): () => void {\n if (!this.listeners[event]) this.listeners[event] = new Set();\n this.listeners[event]!.add(listener);\n return () => this.off(event, listener);\n }\n\n off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): void {\n this.listeners[event]?.delete(listener);\n }\n\n emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {\n this.listeners[event]?.forEach((listener) => {\n try {\n listener(payload);\n } catch (err) {\n console.error(`[EventEmitter] listener for \"${String(event)}\" threw:`, err);\n }\n });\n }\n\n removeAllListeners(): void {\n this.listeners = {};\n }\n}\n","import type * as MonacoType from 'monaco-editor';\nimport { SQL_KEYWORDS, SQL_FUNCTIONS, SQL_DATA_TYPES } from '../constants';\nimport type { SqlSchema } from '../types';\n\n/** 从 SQL 文档中动态解析表名、CTE 别名和变量 */\nfunction parseDynamicItems(sql: string): { tables: string[]; variables: string[] } {\n const tables: string[] = [];\n const variables: string[] = [];\n let m: RegExpExecArray | null;\n\n const tableRe = /\\b(?:FROM|JOIN|UPDATE|INTO|TABLE)\\s+([`\"']?\\w+[`\"']?)/gi;\n while ((m = tableRe.exec(sql)) !== null) {\n const name = m[1].replace(/[`\"']/g, '');\n if (!tables.includes(name)) tables.push(name);\n }\n\n const cteRe = /\\bWITH\\s+(\\w+)\\s+AS\\s*\\(/gi;\n while ((m = cteRe.exec(sql)) !== null) {\n if (!tables.includes(m[1])) tables.push(m[1]);\n }\n\n const declareRe = /\\bDECLARE\\s+(@?\\w+)/gi;\n while ((m = declareRe.exec(sql)) !== null) {\n if (!variables.includes(m[1])) variables.push(m[1]);\n }\n\n const setVarRe = /\\bSET\\s+(@\\w+)\\s*=/gi;\n while ((m = setVarRe.exec(sql)) !== null) {\n if (!variables.includes(m[1])) variables.push(m[1]);\n }\n\n const atVarRe = /@(\\w+)/g;\n while ((m = atVarRe.exec(sql)) !== null) {\n const v = `@${m[1]}`;\n if (!variables.includes(v)) variables.push(v);\n }\n\n return { tables, variables };\n}\n\n/**\n * 注册 SQL 内置补全提供者。\n * schemaRef 使用 ref 模式以支持动态更新。\n * excludeLabelsRef(可选)使用 ref 模式,提供需要从内置关键字/函数中排除的 label\n * (大写匹配)。用于让使用方的自定义词表覆盖内置同名项,避免重复提示。\n */\nexport function registerSqlCompletion(\n monaco: typeof MonacoType,\n schemaRef: { readonly current: SqlSchema },\n excludeLabelsRef?: { readonly current: Set<string> },\n): MonacoType.IDisposable {\n return monaco.languages.registerCompletionItemProvider('sql', {\n triggerCharacters: ['.'],\n provideCompletionItems(model, position, context) {\n const word = model.getWordUntilPosition(position);\n const range: MonacoType.IRange = {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: word.startColumn,\n endColumn: word.endColumn,\n };\n\n // 点号触发:提示该表的列名\n if (context.triggerCharacter === '.') {\n const lineText = model.getLineContent(position.lineNumber);\n const textBeforeDot = lineText.substring(0, position.column - 2);\n const tableMatch = textBeforeDot.match(/(\\w+)$/);\n if (tableMatch) {\n const typed = tableMatch[1].toUpperCase();\n const schema = schemaRef.current;\n const tableKey = Object.keys(schema).find((k) => k.toUpperCase() === typed);\n if (tableKey) {\n const colRange: MonacoType.IRange = {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: position.column,\n endColumn: position.column,\n };\n return {\n suggestions: schema[tableKey].map((col) => ({\n label: col,\n kind: monaco.languages.CompletionItemKind.Field,\n insertText: col,\n range: colRange,\n detail: `${tableKey} · 列`,\n documentation: `表 ${tableKey} 的字段`,\n })),\n };\n }\n }\n return { suggestions: [] };\n }\n\n // 普通触发:关键字 + 函数 + 类型 + 表名 + 变量\n const fullText = model.getValue();\n const { tables: dynamicTables, variables } = parseDynamicItems(fullText);\n const schema = schemaRef.current;\n const schemaTables = Object.keys(schema);\n const allTables = [...new Set([...schemaTables, ...dynamicTables])];\n\n // 需要排除的内置 label(大写匹配),让使用方自定义词表覆盖同名项\n const exclude = excludeLabelsRef?.current;\n const isExcluded = (label: string) => exclude?.has(label.toUpperCase()) ?? false;\n\n const keywords = SQL_KEYWORDS.filter((kw) => !isExcluded(kw)).map((kw) => ({\n label: kw,\n kind: monaco.languages.CompletionItemKind.Keyword,\n insertText: kw,\n range,\n detail: 'SQL 关键字',\n }));\n\n const functions = SQL_FUNCTIONS.filter((fn) => !isExcluded(fn)).map((fn) => ({\n label: fn,\n kind: monaco.languages.CompletionItemKind.Function,\n insertText: `${fn}($1)`,\n insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n range,\n detail: 'SQL 函数',\n }));\n\n const dataTypes = SQL_DATA_TYPES.map((dt) => ({\n label: dt,\n kind: monaco.languages.CompletionItemKind.TypeParameter,\n insertText: dt,\n range,\n detail: 'SQL 数据类型',\n }));\n\n const tableItems = allTables.map((t) => ({\n label: t,\n kind: monaco.languages.CompletionItemKind.Class,\n insertText: t,\n range,\n detail: schemaTables.includes(t) ? '表名 (Schema)' : '表名 (文档)',\n }));\n\n const variableItems = variables.map((v) => ({\n label: v,\n kind: monaco.languages.CompletionItemKind.Variable,\n insertText: v,\n range,\n detail: 'SQL 变量',\n }));\n\n return {\n suggestions: [...keywords, ...functions, ...dataTypes, ...tableItems, ...variableItems],\n };\n },\n });\n}\n","import type * as MonacoType from 'monaco-editor';\nimport type { ApiConfig, PendingCompletion } from '../types';\nimport { AI_INLINE_LANGUAGES } from '../constants';\n\n/** 去除模型返回中可能包含的代码围栏(```lang ... ```) */\nfunction stripCodeFence(text: string): string {\n return text.replace(/^```[a-z]*\\n?/, '').replace(/\\n?```$/, '');\n}\n\nconst SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] as const;\n\nfunction ensureSpinnerStyle() {\n const id = 'mae-ai-completion-spinner-style';\n if (typeof document === 'undefined' || document.getElementById(id)) return;\n const style = document.createElement('style');\n style.id = id;\n const frameCss = SPINNER_FRAMES.map((ch, i) => `.mae-ai-spinner-f${i}::after { content: \" ${ch}\"; }`).join('\\n');\n style.textContent = `\n@keyframes mae-ai-completion-glow {\n 0%, 100% { text-shadow: 0 0 3px rgba(96, 165, 250, 0.35); }\n 50% { text-shadow: 0 0 9px rgba(96, 165, 250, 0.95), 0 0 18px rgba(59, 130, 246, 0.35); }\n}\n${frameCss}\n[class^=\"mae-ai-spinner-f\"]::after {\n color: #60a5fa;\n font-family: \"Segoe UI Symbol\", \"Noto Sans Symbols\", \"Apple Symbols\", monospace;\n display: inline-block;\n margin-left: 2px;\n animation: mae-ai-completion-glow 1s ease-in-out infinite;\n}`;\n document.head.appendChild(style);\n}\n\nfunction createCursorSpinner(\n editor: MonacoType.editor.IStandaloneCodeEditor,\n line: number,\n col: number,\n): () => void {\n ensureSpinnerStyle();\n let frameIdx = 0;\n const makeDecor = () => [{\n range: { startLineNumber: line, startColumn: col, endLineNumber: line, endColumn: col },\n options: { afterContentClassName: `mae-ai-spinner-f${frameIdx}` },\n }];\n const collection = editor.createDecorationsCollection(makeDecor());\n const timer = setInterval(() => {\n frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;\n collection.set(makeDecor());\n }, 80);\n return () => {\n clearInterval(timer);\n collection.clear();\n };\n}\n\n/** 非流式调用 OpenAI 兼容 API(使用原生 fetch,无 axios 依赖) */\nexport async function fetchAiCompletionNonStream(\n config: ApiConfig,\n context: string,\n prefix: string,\n signal?: AbortSignal,\n): Promise<string> {\n const response = await fetch(`${config.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` },\n body: JSON.stringify({\n model: config.model,\n messages: [\n {\n role: 'system',\n content: '你是一个代码补全助手。基于用户提供的代码上下文和前缀,生成短小精悍的代码补全。只返回补全的代码片段,不要解释。',\n },\n {\n role: 'user',\n content: `代码上下文:\\n${context}\\n\\n前缀:${prefix}\\n\\n请继续补全这段代码,只返回补全部分。`,\n },\n ],\n max_tokens: 1024,\n stream: false,\n }),\n signal,\n });\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${await response.text().catch(() => '')}`);\n }\n const data = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n return stripCodeFence(data.choices?.[0]?.message?.content?.trim() ?? '');\n}\n\n/** 流式调用(fetch + ReadableStream) */\nexport async function fetchAiCompletionStream(\n config: ApiConfig,\n context: string,\n prefix: string,\n onChunk: (accumulated: string) => void,\n signal?: AbortSignal,\n): Promise<void> {\n const response = await fetch(`${config.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` },\n body: JSON.stringify({\n model: config.model,\n messages: [\n {\n role: 'system',\n content: '你是一个代码补全助手。基于用户提供的代码上下文和前缀,生成短小精悍的代码补全。只返回补全的代码片段,不要解释。',\n },\n {\n role: 'user',\n content: `代码上下文:\\n${context}\\n\\n前缀:${prefix}\\n\\n请继续补全这段代码,只返回补全部分。`,\n },\n ],\n max_tokens: 1024,\n stream: true,\n }),\n signal,\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${await response.text().catch(() => '')}`);\n }\n if (!response.body) throw new Error('Response body is null');\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let accumulated = '';\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data:')) continue;\n const data = trimmed.slice(5).trim();\n if (data === '[DONE]') return;\n try {\n const parsed = JSON.parse(data) as { choices?: Array<{ delta?: { content?: string } }> };\n const delta = parsed.choices?.[0]?.delta?.content ?? '';\n if (delta) {\n accumulated += delta;\n onChunk(accumulated);\n }\n } catch {\n // ignore malformed lines\n }\n }\n }\n}\n\n/** 注册 AI 内联幽灵文本提供者 */\nexport function registerAiInlineCompletion(\n monaco: typeof MonacoType,\n pendingRef: { current: PendingCompletion | null },\n): void {\n AI_INLINE_LANGUAGES.forEach((lang) => {\n monaco.languages.registerInlineCompletionsProvider(lang, {\n provideInlineCompletions: (_model, position) => {\n const pending = pendingRef.current;\n if (!pending) return { items: [] };\n if (pending.lineNumber !== position.lineNumber || pending.column !== position.column) {\n pendingRef.current = null;\n return { items: [] };\n }\n return {\n items: [\n {\n insertText: pending.insertText,\n range: {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: position.column,\n endColumn: position.column,\n },\n },\n ],\n };\n },\n disposeInlineCompletions: () => {},\n });\n });\n}\n\n/**\n * 注册 AI 补全命令(Ctrl+Alt+L 手动触发,流式更新幽灵文本)。\n * 框架无关:通过函数引用方式读取最新的 apiConfig。\n */\nexport function registerAiCompletionCommand(\n editor: MonacoType.editor.IStandaloneCodeEditor,\n getApiConfig: () => ApiConfig | null,\n pendingRef: { current: PendingCompletion | null },\n onLoadingChange?: (loading: boolean) => void,\n onError?: (message: string) => void,\n): MonacoType.IDisposable {\n let currentAbortController: AbortController | null = null;\n\n const isAbortError = (err: unknown): boolean =>\n err instanceof Error && (err.name === 'AbortError' || err.message?.includes('aborted'));\n\n const triggerAiCompletion = async () => {\n const apiConfig = getApiConfig();\n if (!apiConfig || !apiConfig.apiKey.trim()) {\n onError?.('未配置 API Key,请先配置 AI 服务再使用补全功能。');\n return;\n }\n\n const position = editor.getPosition();\n if (!position) return;\n const model = editor.getModel();\n if (!model) return;\n\n const lineText = model.getLineContent(position.lineNumber);\n const textBeforePosition = lineText.substring(0, position.column - 1);\n if (!textBeforePosition.trim()) return;\n\n const startLine = Math.max(0, position.lineNumber - 5);\n const contextLines: string[] = [];\n for (let i = startLine; i < position.lineNumber; i++) {\n contextLines.push(model.getLineContent(i + 1));\n }\n const contextCode = contextLines.join('\\n');\n\n currentAbortController?.abort();\n currentAbortController = new AbortController();\n pendingRef.current = null;\n\n const stopSpinner = createCursorSpinner(editor, position.lineNumber, position.column);\n onLoadingChange?.(true);\n\n const useStream = apiConfig.streamMode !== false;\n\n try {\n if (useStream) {\n let triggerTimer: ReturnType<typeof setTimeout> | null = null;\n const scheduleTrigger = () => {\n if (triggerTimer !== null) clearTimeout(triggerTimer);\n triggerTimer = setTimeout(() => {\n triggerTimer = null;\n if (pendingRef.current) {\n editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});\n }\n }, 80);\n };\n\n await fetchAiCompletionStream(\n apiConfig,\n contextCode,\n textBeforePosition,\n (accumulated) => {\n let insertText = stripCodeFence(accumulated);\n if (insertText.startsWith(textBeforePosition)) {\n insertText = insertText.slice(textBeforePosition.length);\n }\n if (!insertText.trim()) return;\n pendingRef.current = {\n lineNumber: position.lineNumber,\n column: position.column,\n insertText,\n };\n scheduleTrigger();\n },\n currentAbortController.signal,\n );\n\n if (triggerTimer !== null) clearTimeout(triggerTimer);\n if (pendingRef.current) {\n editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});\n }\n } else {\n const completion = await fetchAiCompletionNonStream(\n apiConfig,\n contextCode,\n textBeforePosition,\n currentAbortController.signal,\n );\n if (!completion) return;\n let insertText = completion;\n if (insertText.startsWith(textBeforePosition)) {\n insertText = insertText.slice(textBeforePosition.length);\n }\n if (!insertText.trim()) return;\n pendingRef.current = {\n lineNumber: position.lineNumber,\n column: position.column,\n insertText,\n };\n editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});\n }\n } catch (err) {\n if (isAbortError(err)) return;\n const msg = err instanceof Error ? err.message : String(err);\n onError?.(`AI 补全失败:${msg}`);\n console.error('[AI Completion] Error:', err);\n } finally {\n stopSpinner();\n onLoadingChange?.(false);\n }\n };\n\n return editor.onKeyDown((e) => {\n if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyL') {\n e.preventDefault();\n void triggerAiCompletion();\n }\n });\n}\n","import type * as MonacoType from 'monaco-editor';\nimport { EventEmitter } from '../bus/EventEmitter';\nimport type { ApiConfig, PendingCompletion, SqlSchema, CompletionProvider } from '../types';\nimport { registerSqlCompletion } from '../completion/sqlCompletion';\nimport { registerAiInlineCompletion } from '../ai/aiCompletion';\n\nexport interface EditorControllerEvents {\n change: { value: string };\n cursor: { line: number; column: number };\n ready: { editor: MonacoType.editor.IStandaloneCodeEditor };\n languageChange: { language: string };\n focus: void;\n blur: void;\n}\n\nexport interface EditorControllerOptions {\n container: HTMLElement;\n language?: string;\n theme?: string;\n value?: string;\n monaco: typeof MonacoType;\n /** Monaco loader monaco-editor 路径配置 */\n vsPath?: string;\n sqlSchema?: SqlSchema;\n apiConfig?: ApiConfig | null;\n completionProviders?: CompletionProvider[];\n /** Monaco editor options */\n editorOptions?: MonacoType.editor.IStandaloneEditorConstructionOptions;\n}\n\n/**\n * 框架无关的编辑器控制器。\n * 封装 Monaco 实例的生命周期、核心操作、补全注册。\n * 通过 EventEmitter 暴露编辑器事件,不依赖任何 UI 框架。\n */\nexport class EditorController extends EventEmitter<EditorControllerEvents> {\n private editor: MonacoType.editor.IStandaloneCodeEditor | null = null;\n private monaco: typeof MonacoType;\n private sqlProviderRegistered = false;\n private aiProviderRegistered = false;\n private pendingCompletionRef = { current: null as PendingCompletion | null };\n private customProviders: CompletionProvider[] = [];\n private disposables: MonacoType.IDisposable[] = [];\n\n completed = false;\n error: Error | null = null;\n\n constructor(private options: EditorControllerOptions) {\n super();\n this.monaco = options.monaco;\n this.customProviders = options.completionProviders ?? [];\n this.init();\n }\n\n private init(): void {\n const { language, theme, value, sqlSchema, apiConfig, editorOptions } = this.options;\n\n // 注册 SQL 补全(只注册一次)\n if (!this.sqlProviderRegistered) {\n const schemaRef = { current: sqlSchema ?? {} };\n registerSqlCompletion(this.monaco, schemaRef);\n this.sqlProviderRegistered = true;\n }\n\n // 注册 AI 幽灵文本提供者\n if (!this.aiProviderRegistered) {\n registerAiInlineCompletion(this.monaco, this.pendingCompletionRef);\n this.aiProviderRegistered = true;\n }\n\n // 注册自定义补全提供者\n this.registerCustomCompletionProviders();\n\n // 创建编辑器\n this.editor = this.monaco.editor.create(this.options.container, {\n value: value ?? '',\n language: language ?? 'typescript',\n theme: theme ?? 'vs-dark',\n fontSize: 14,\n minimap: { enabled: true },\n scrollBeyondLastLine: false,\n automaticLayout: true,\n lineNumbers: 'on',\n renderLineHighlight: 'all',\n smoothScrolling: true,\n cursorBlinking: 'smooth',\n bracketPairColorization: { enabled: true },\n tabSize: 2,\n wordWrap: 'on',\n folding: true,\n formatOnPaste: true,\n suggestOnTriggerCharacters: true,\n quickSuggestions: { other: true, comments: false, strings: false },\n acceptSuggestionOnCommitCharacter: true,\n acceptSuggestionOnEnter: 'on',\n inlineSuggest: { enabled: true },\n tabCompletion: 'on',\n ...editorOptions,\n });\n\n // 绑定事件\n this.bindEditorEvents();\n this.emit('ready', { editor: this.editor });\n }\n\n private bindEditorEvents(): void {\n const editor = this.editor!;\n\n const d1 = editor.onDidChangeModelContent(() => {\n this.emit('change', { value: editor.getValue() });\n });\n\n const d2 = editor.onDidChangeCursorPosition((e) => {\n this.emit('cursor', {\n line: e.position.lineNumber,\n column: e.position.column,\n });\n });\n\n const d3 = editor.onDidFocusEditorText(() => this.emit('focus', undefined));\n const d4 = editor.onDidBlurEditorText(() => this.emit('blur', undefined));\n\n this.disposables.push(d1, d2, d3, d4);\n }\n\n private registerCustomCompletionProviders(): void {\n if (this.customProviders.length === 0) return;\n\n for (const provider of this.customProviders) {\n const langs = provider.languages ?? ['*'];\n for (const lang of langs) {\n this.monaco.languages.registerCompletionItemProvider(lang, {\n triggerCharacters: provider.triggerCharacters,\n provideCompletionItems: async (model, position, context) => {\n const ctx = {\n monaco: this.monaco,\n model,\n position,\n lineText: model.getLineContent(position.lineNumber),\n currentWord: model.getWordUntilPosition(position).word,\n fullText: model.getValue(),\n triggerCharacter: context.triggerCharacter,\n };\n\n if (provider.shouldTrigger && !provider.shouldTrigger(ctx)) {\n return { suggestions: [] };\n }\n\n const suggestions = await provider.provide(ctx);\n return { suggestions };\n },\n });\n }\n }\n }\n\n // ─── 公开 API ──────────────────────────────────────────────────────────\n\n getEditor(): MonacoType.editor.IStandaloneCodeEditor | null {\n return this.editor;\n }\n\n getValue(): string {\n return this.editor?.getValue() ?? '';\n }\n\n setValue(value: string): void {\n this.editor?.setValue(value);\n }\n\n insertText(text: string, range?: MonacoType.IRange): void {\n const editor = this.editor;\n if (!editor) return;\n const targetRange = range ?? editor.getSelection();\n if (targetRange) {\n editor.executeEdits('controller-insert', [{ range: targetRange, text }]);\n editor.focus();\n }\n }\n\n format(): void {\n this.editor?.getAction('editor.action.formatDocument')?.run();\n }\n\n focus(): void {\n this.editor?.focus();\n }\n\n setLanguage(language: string): void {\n const model = this.editor?.getModel();\n if (model) {\n this.monaco.editor.setModelLanguage(model, language);\n this.emit('languageChange', { language });\n }\n }\n\n setTheme(theme: string): void {\n this.monaco.editor.setTheme(theme);\n }\n\n getCursorPosition(): { line: number; column: number } {\n const pos = this.editor?.getPosition();\n return pos\n ? { line: pos.lineNumber, column: pos.column }\n : { line: 1, column: 1 };\n }\n\n getSelection(): MonacoType.IRange | null {\n return this.editor?.getSelection() ?? null;\n }\n\n /**\n * 触发 AI 行内补全(Ctrl+Alt+L 快捷键)。\n * 需要提前设置 apiConfig(通过 setApiConfig 或构造函数传入)。\n */\n setApiConfig(config: ApiConfig | null): void {\n this.options.apiConfig = config;\n }\n\n getPendingCompletionRef(): { current: PendingCompletion | null } {\n return this.pendingCompletionRef;\n }\n\n dispose(): void {\n for (const d of this.disposables) d.dispose();\n this.disposables = [];\n this.editor?.dispose();\n this.editor = null;\n this.removeAllListeners();\n }\n}","import { EventEmitter } from '../bus/EventEmitter';\nimport type { ApiConfig, ChatMessage } from '../types';\n\nexport interface AiChatControllerEvents {\n messageAdded: ChatMessage;\n messageUpdated: ChatMessage;\n cleared: void;\n loadingChange: { loading: boolean };\n error: { message: string };\n}\n\nfunction generateUUID(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * AI 聊天控制器:管理消息列表、API 调用与流式响应。\n * 框架无关:通过事件通知 UI 层更新。\n */\nexport class AiChatController extends EventEmitter<AiChatControllerEvents> {\n private messages: ChatMessage[] = [];\n private abortController: AbortController | null = null;\n private loading = false;\n private apiConfig: ApiConfig;\n\n constructor(apiConfig: ApiConfig) {\n super();\n this.apiConfig = apiConfig;\n }\n\n setApiConfig(config: ApiConfig): void {\n this.apiConfig = config;\n }\n\n getApiConfig(): ApiConfig {\n return this.apiConfig;\n }\n\n getMessages(): readonly ChatMessage[] {\n return this.messages;\n }\n\n isLoading(): boolean {\n return this.loading;\n }\n\n /** 发送消息,可选携带编辑器上下文 */\n async send(\n userInput: string,\n options?: { editorContent?: string; language?: string },\n ): Promise<void> {\n const trimmed = userInput.trim();\n if (!trimmed || this.loading) return;\n\n if (!this.apiConfig.apiKey.trim()) {\n const userMsg: ChatMessage = { id: generateUUID(), role: 'user', content: trimmed };\n this.messages.push(userMsg);\n this.emit('messageAdded', userMsg);\n const assistantMsg: ChatMessage = {\n id: generateUUID(),\n role: 'assistant',\n content: '⚠️ 尚未配置 API Key,请配置 Base URL、API Key 和模型名称后再开始对话。',\n };\n this.messages.push(assistantMsg);\n this.emit('messageAdded', assistantMsg);\n return;\n }\n\n const userMsg: ChatMessage = { id: generateUUID(), role: 'user', content: trimmed };\n this.messages.push(userMsg);\n this.emit('messageAdded', userMsg);\n\n const assistantMsg: ChatMessage = {\n id: generateUUID(),\n role: 'assistant',\n content: '',\n isStreaming: true,\n };\n this.messages.push(assistantMsg);\n this.emit('messageAdded', assistantMsg);\n\n this.loading = true;\n this.emit('loadingChange', { loading: true });\n\n try {\n this.abortController = new AbortController();\n\n const systemPrompt = options?.language\n ? `You are an expert coding assistant. The user is working with ${options.language} code. Respond concisely in the same language as the user's message, defaulting to Chinese.`\n : \"You are an expert coding assistant. Respond concisely in the same language as the user's message, defaulting to Chinese.\";\n\n const historyForApi = this.messages\n .filter((m) => m.id !== assistantMsg.id)\n .map((m) => ({ role: m.role, content: m.content }));\n\n const response = await fetch(`${this.apiConfig.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiConfig.apiKey}`,\n },\n body: JSON.stringify({\n model: this.apiConfig.model,\n messages: [{ role: 'system', content: systemPrompt }, ...historyForApi],\n stream: true,\n }),\n signal: this.abortController.signal,\n });\n\n if (!response.ok) {\n let errText = '';\n try {\n errText = await response.text();\n } catch {\n /* ignore */\n }\n let hint = '';\n if (response.status === 401) hint = '(API Key 无效或已过期)';\n else if (response.status === 403) hint = '(无访问权限,请检查 API Key 和 Base URL)';\n else if (response.status === 429) hint = '(请求过于频繁,请稍后再试)';\n else if (response.status >= 500) hint = '(服务端错误)';\n throw new Error(`HTTP ${response.status}${hint}${errText ? `\\n${errText}` : ''}`);\n }\n\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let fullContent = '';\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (data === '[DONE]') break;\n try {\n const parsed = JSON.parse(data) as {\n choices?: Array<{ delta?: { content?: string } }>;\n };\n const delta = parsed.choices?.[0]?.delta?.content ?? '';\n if (delta) {\n fullContent += delta;\n assistantMsg.content = fullContent;\n this.emit('messageUpdated', { ...assistantMsg });\n }\n } catch {\n /* ignore */\n }\n }\n }\n\n assistantMsg.isStreaming = false;\n this.emit('messageUpdated', { ...assistantMsg });\n } catch (err: unknown) {\n const isAbort = err instanceof Error && err.name === 'AbortError';\n const errMsg = err instanceof Error ? err.message : String(err);\n const isNetworkErr = err instanceof TypeError && errMsg.toLowerCase().includes('fetch');\n const errorContent = isAbort\n ? '⏹ 请求已取消'\n : isNetworkErr\n ? `⚠️ 网络错误,请检查 Base URL 和网络。\\n\\n${errMsg}`\n : `⚠️ 请求失败:${errMsg}`;\n assistantMsg.content = errorContent;\n assistantMsg.isStreaming = false;\n this.emit('messageUpdated', { ...assistantMsg });\n if (!isAbort) this.emit('error', { message: errMsg });\n } finally {\n this.loading = false;\n this.emit('loadingChange', { loading: false });\n }\n }\n\n stop(): void {\n this.abortController?.abort();\n }\n\n clear(): void {\n this.messages = [];\n this.emit('cleared', undefined);\n }\n\n dispose(): void {\n this.abortController?.abort();\n this.removeAllListeners();\n }\n}\n","import { EventEmitter } from './EventEmitter';\nimport type { EditorController } from '../controllers/EditorController';\nimport type { AiChatController } from '../controllers/AiChatController';\n\nexport interface EditorBusEvents {\n /** 编辑器已注册 */\n 'editor:ready': EditorController;\n /** 编辑器即将卸载 */\n 'editor:dispose': void;\n /** 编辑器内容变化 */\n 'editor:change': { value: string };\n /** 编辑器光标变化 */\n 'editor:cursor': { line: number; column: number };\n /** 编辑器语言变化 */\n 'editor:languageChange': { language: string };\n\n /** 聊天面板已注册 */\n 'chat:ready': AiChatController;\n /** 聊天面板卸载 */\n 'chat:dispose': void;\n /** 聊天面板请求向编辑器插入代码 */\n 'chat:insertCode': { code: string };\n /** 聊天面板请求替换编辑器全部内容 */\n 'chat:replaceCode': { code: string };\n}\n\n/**\n * 编辑器与聊天面板的通信总线。\n * 在两个组件都引入时由协调器(Coordinator)创建并共享,实现自动联动。\n * 当只引入其中一个组件时,bus 可为 null,组件正常独立工作。\n */\nexport class EditorBus extends EventEmitter<EditorBusEvents> {\n private editor: EditorController | null = null;\n private chat: AiChatController | null = null;\n\n registerEditor(controller: EditorController): () => void {\n this.editor = controller;\n\n // 桥接:编辑器事件 → bus 事件\n const offChange = controller.on('change', (data) => this.emit('editor:change', data));\n const offCursor = controller.on('cursor', (data) => this.emit('editor:cursor', data));\n const offLang = controller.on('languageChange', (data) => this.emit('editor:languageChange', data));\n\n // 桥接:bus 上的 chat 命令 → 编辑器操作\n const offInsert = this.on('chat:insertCode', ({ code }) => controller.insertText(code));\n const offReplace = this.on('chat:replaceCode', ({ code }) => controller.setValue(code));\n\n this.emit('editor:ready', controller);\n\n return () => {\n offChange();\n offCursor();\n offLang();\n offInsert();\n offReplace();\n if (this.editor === controller) {\n this.editor = null;\n this.emit('editor:dispose', undefined);\n }\n };\n }\n\n registerChat(controller: AiChatController): () => void {\n this.chat = controller;\n this.emit('chat:ready', controller);\n return () => {\n if (this.chat === controller) {\n this.chat = null;\n this.emit('chat:dispose', undefined);\n }\n };\n }\n\n getEditor(): EditorController | null {\n return this.editor;\n }\n\n getChat(): AiChatController | null {\n return this.chat;\n }\n\n /** 让聊天面板向编辑器请求当前内容 */\n getEditorContent(): string {\n return this.editor?.getValue() ?? '';\n }\n\n /** 让聊天面板向编辑器请求当前语言 */\n getEditorLanguage(): string | undefined {\n const editor = this.editor?.getEditor();\n return editor?.getModel()?.getLanguageId();\n }\n\n /** 让聊天面板请求向编辑器插入代码 */\n insertToEditor(code: string): void {\n this.emit('chat:insertCode', { code });\n }\n\n /** 让聊天面板请求替换编辑器全部内容 */\n replaceEditor(code: string): void {\n this.emit('chat:replaceCode', { code });\n }\n}\n"],"mappings":";AAAO,IAAM,eAAe;AAAA,EAC1B;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAU;AAAA,EAAO;AAAA,EACxE;AAAA,EAAU;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAO;AAAA,EAAU;AAAA,EAAS;AAAA,EAAQ;AAAA,EACtE;AAAA,EAAU;AAAA,EAAY;AAAA,EAAU;AAAA,EAAM;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EACrE;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAS;AAAA,EAAW;AAAA,EAAS;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EACxE;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAC1E;AAAA,EAAO;AAAA,EAAM;AAAA,EAAS;AAAA,EAAO;AAAA,EAAY;AAAA,EAAS;AAAA,EAAM;AAAA,EAAO;AAAA,EAAQ;AAAA,EACvE;AAAA,EAAU;AAAA,EAAS;AAAA,EAAU;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EACtE;AAAA,EAAW;AAAA,EAAO;AAAA,EAAW;AAAA,EAAc;AAAA,EAAU;AAAA,EAAY;AAAA,EACjE;AAAA,EAAS;AAAA,EAAc;AAAA,EAAkB;AAAA,EAAY;AAAA,EAAY;AAAA,EACjE;AAAA,EAAU;AAAA,EAAU;AAAA,EAAY;AAAA,EAAe;AAAA,EAAS;AAAA,EAAa;AACvE;AAEO,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EAAS;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAY;AAAA,EAAU;AAAA,EAAU;AAAA,EACrE;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAAa;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAS;AAAA,EACrE;AAAA,EAAS;AAAA,EAAW;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAe;AAAA,EAC5D;AAAA,EAAO;AAAA,EAAW;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAC9D;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAU;AAAA,EAAY;AAAA,EAAY;AAAA,EAAY;AAAA,EAChE;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAe;AAAA,EAC3D;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAO;AAAA,EAAO;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EACjE;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAc;AAAA,EAAQ;AAAA,EAAc;AAAA,EAAS;AAAA,EAAO;AAAA,EACvE;AAAA,EAAe;AAAA,EAAc;AAAA,EAAQ;AAAA,EAAa;AAAA,EAAW;AAAA,EAC7D;AAAA,EAAc;AAAA,EAAe;AAAA,EAAc;AAAA,EAAgB;AAC7D;AAEO,IAAM,iBAAiB;AAAA,EAC5B;AAAA,EAAO;AAAA,EAAW;AAAA,EAAU;AAAA,EAAY;AAAA,EAAW;AAAA,EAAW;AAAA,EAC9D;AAAA,EAAS;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAS;AAAA,EAC/D;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAY;AAAA,EAAa;AAAA,EAAW;AAAA,EAC7D;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAa;AAAA,EAAQ;AAAA,EAAQ;AACzD;AAEO,IAAM,YAAY;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,UAAU;AAAA,EACrB,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAIO,IAAM,SAAS;AAAA,EACpB,EAAE,OAAO,WAAW,OAAO,OAAO;AAAA,EAClC,EAAE,OAAO,SAAS,OAAO,QAAQ;AAAA,EACjC,EAAE,OAAO,YAAY,OAAO,gBAAgB;AAC9C;AAEO,IAAM,sBAAsB;AAAA,EACjC;AAAA,EAAc;AAAA,EAAc;AAAA,EAAU;AAAA,EACtC;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAAA,EAAM;AACjC;;;AChEO,IAAM,eAAN,MAA6B;AAAA,EAA7B;AACL,SAAQ,YAAoE,CAAC;AAAA;AAAA,EAE7E,GAA6B,OAAU,UAA6C;AAClF,QAAI,CAAC,KAAK,UAAU,KAAK,EAAG,MAAK,UAAU,KAAK,IAAI,oBAAI,IAAI;AAC5D,SAAK,UAAU,KAAK,EAAG,IAAI,QAAQ;AACnC,WAAO,MAAM,KAAK,IAAI,OAAO,QAAQ;AAAA,EACvC;AAAA,EAEA,IAA8B,OAAU,UAAuC;AAC7E,SAAK,UAAU,KAAK,GAAG,OAAO,QAAQ;AAAA,EACxC;AAAA,EAEA,KAA+B,OAAU,SAA4B;AACnE,SAAK,UAAU,KAAK,GAAG,QAAQ,CAAC,aAAa;AAC3C,UAAI;AACF,iBAAS,OAAO;AAAA,MAClB,SAAS,KAAK;AACZ,gBAAQ,MAAM,gCAAgC,OAAO,KAAK,CAAC,YAAY,GAAG;AAAA,MAC5E;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,qBAA2B;AACzB,SAAK,YAAY,CAAC;AAAA,EACpB;AACF;;;AC3BA,SAAS,kBAAkB,KAAwD;AACjF,QAAM,SAAmB,CAAC;AAC1B,QAAM,YAAsB,CAAC;AAC7B,MAAI;AAEJ,QAAM,UAAU;AAChB,UAAQ,IAAI,QAAQ,KAAK,GAAG,OAAO,MAAM;AACvC,UAAM,OAAO,EAAE,CAAC,EAAE,QAAQ,UAAU,EAAE;AACtC,QAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,IAAI;AAAA,EAC9C;AAEA,QAAM,QAAQ;AACd,UAAQ,IAAI,MAAM,KAAK,GAAG,OAAO,MAAM;AACrC,QAAI,CAAC,OAAO,SAAS,EAAE,CAAC,CAAC,EAAG,QAAO,KAAK,EAAE,CAAC,CAAC;AAAA,EAC9C;AAEA,QAAM,YAAY;AAClB,UAAQ,IAAI,UAAU,KAAK,GAAG,OAAO,MAAM;AACzC,QAAI,CAAC,UAAU,SAAS,EAAE,CAAC,CAAC,EAAG,WAAU,KAAK,EAAE,CAAC,CAAC;AAAA,EACpD;AAEA,QAAM,WAAW;AACjB,UAAQ,IAAI,SAAS,KAAK,GAAG,OAAO,MAAM;AACxC,QAAI,CAAC,UAAU,SAAS,EAAE,CAAC,CAAC,EAAG,WAAU,KAAK,EAAE,CAAC,CAAC;AAAA,EACpD;AAEA,QAAM,UAAU;AAChB,UAAQ,IAAI,QAAQ,KAAK,GAAG,OAAO,MAAM;AACvC,UAAM,IAAI,IAAI,EAAE,CAAC,CAAC;AAClB,QAAI,CAAC,UAAU,SAAS,CAAC,EAAG,WAAU,KAAK,CAAC;AAAA,EAC9C;AAEA,SAAO,EAAE,QAAQ,UAAU;AAC7B;AAQO,SAAS,sBACd,QACA,WACA,kBACwB;AACxB,SAAO,OAAO,UAAU,+BAA+B,OAAO;AAAA,IAC5D,mBAAmB,CAAC,GAAG;AAAA,IACvB,uBAAuB,OAAO,UAAU,SAAS;AAC/C,YAAM,OAAO,MAAM,qBAAqB,QAAQ;AAChD,YAAM,QAA2B;AAAA,QAC/B,iBAAiB,SAAS;AAAA,QAC1B,eAAe,SAAS;AAAA,QACxB,aAAa,KAAK;AAAA,QAClB,WAAW,KAAK;AAAA,MAClB;AAGA,UAAI,QAAQ,qBAAqB,KAAK;AACpC,cAAM,WAAW,MAAM,eAAe,SAAS,UAAU;AACzD,cAAM,gBAAgB,SAAS,UAAU,GAAG,SAAS,SAAS,CAAC;AAC/D,cAAM,aAAa,cAAc,MAAM,QAAQ;AAC/C,YAAI,YAAY;AACd,gBAAM,QAAQ,WAAW,CAAC,EAAE,YAAY;AACxC,gBAAMA,UAAS,UAAU;AACzB,gBAAM,WAAW,OAAO,KAAKA,OAAM,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,KAAK;AAC1E,cAAI,UAAU;AACZ,kBAAM,WAA8B;AAAA,cAClC,iBAAiB,SAAS;AAAA,cAC1B,eAAe,SAAS;AAAA,cACxB,aAAa,SAAS;AAAA,cACtB,WAAW,SAAS;AAAA,YACtB;AACA,mBAAO;AAAA,cACL,aAAaA,QAAO,QAAQ,EAAE,IAAI,CAAC,SAAS;AAAA,gBAC1C,OAAO;AAAA,gBACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,gBAC1C,YAAY;AAAA,gBACZ,OAAO;AAAA,gBACP,QAAQ,GAAG,QAAQ;AAAA,gBACnB,eAAe,UAAK,QAAQ;AAAA,cAC9B,EAAE;AAAA,YACJ;AAAA,UACF;AAAA,QACF;AACA,eAAO,EAAE,aAAa,CAAC,EAAE;AAAA,MAC3B;AAGA,YAAM,WAAW,MAAM,SAAS;AAChC,YAAM,EAAE,QAAQ,eAAe,UAAU,IAAI,kBAAkB,QAAQ;AACvE,YAAM,SAAS,UAAU;AACzB,YAAM,eAAe,OAAO,KAAK,MAAM;AACvC,YAAM,YAAY,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,cAAc,GAAG,aAAa,CAAC,CAAC;AAGlE,YAAM,UAAU,kBAAkB;AAClC,YAAM,aAAa,CAAC,UAAkB,SAAS,IAAI,MAAM,YAAY,CAAC,KAAK;AAE3E,YAAM,WAAW,aAAa,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ;AAAA,QACzE,OAAO;AAAA,QACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,QAC1C,YAAY;AAAA,QACZ;AAAA,QACA,QAAQ;AAAA,MACV,EAAE;AAEF,YAAM,YAAY,cAAc,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ;AAAA,QAC3E,OAAO;AAAA,QACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,QAC1C,YAAY,GAAG,EAAE;AAAA,QACjB,iBAAiB,OAAO,UAAU,6BAA6B;AAAA,QAC/D;AAAA,QACA,QAAQ;AAAA,MACV,EAAE;AAEF,YAAM,YAAY,eAAe,IAAI,CAAC,QAAQ;AAAA,QAC5C,OAAO;AAAA,QACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,QAC1C,YAAY;AAAA,QACZ;AAAA,QACA,QAAQ;AAAA,MACV,EAAE;AAEF,YAAM,aAAa,UAAU,IAAI,CAAC,OAAO;AAAA,QACvC,OAAO;AAAA,QACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,QAC1C,YAAY;AAAA,QACZ;AAAA,QACA,QAAQ,aAAa,SAAS,CAAC,IAAI,0BAAgB;AAAA,MACrD,EAAE;AAEF,YAAM,gBAAgB,UAAU,IAAI,CAAC,OAAO;AAAA,QAC1C,OAAO;AAAA,QACP,MAAM,OAAO,UAAU,mBAAmB;AAAA,QAC1C,YAAY;AAAA,QACZ;AAAA,QACA,QAAQ;AAAA,MACV,EAAE;AAEF,aAAO;AAAA,QACL,aAAa,CAAC,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,aAAa;AAAA,MACxF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACjJA,SAAS,eAAe,MAAsB;AAC5C,SAAO,KAAK,QAAQ,iBAAiB,EAAE,EAAE,QAAQ,WAAW,EAAE;AAChE;AAEA,IAAM,iBAAiB,CAAC,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,QAAG;AAE9D,SAAS,qBAAqB;AAC5B,QAAM,KAAK;AACX,MAAI,OAAO,aAAa,eAAe,SAAS,eAAe,EAAE,EAAG;AACpE,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,KAAK;AACX,QAAM,WAAW,eAAe,IAAI,CAAC,IAAI,MAAM,oBAAoB,CAAC,wBAAwB,EAAE,MAAM,EAAE,KAAK,IAAI;AAC/G,QAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpB,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQR,WAAS,KAAK,YAAY,KAAK;AACjC;AAEA,SAAS,oBACP,QACA,MACA,KACY;AACZ,qBAAmB;AACnB,MAAI,WAAW;AACf,QAAM,YAAY,MAAM,CAAC;AAAA,IACvB,OAAO,EAAE,iBAAiB,MAAM,aAAa,KAAK,eAAe,MAAM,WAAW,IAAI;AAAA,IACtF,SAAS,EAAE,uBAAuB,mBAAmB,QAAQ,GAAG;AAAA,EAClE,CAAC;AACD,QAAM,aAAa,OAAO,4BAA4B,UAAU,CAAC;AACjE,QAAM,QAAQ,YAAY,MAAM;AAC9B,gBAAY,WAAW,KAAK,eAAe;AAC3C,eAAW,IAAI,UAAU,CAAC;AAAA,EAC5B,GAAG,EAAE;AACL,SAAO,MAAM;AACX,kBAAc,KAAK;AACnB,eAAW,MAAM;AAAA,EACnB;AACF;AAGA,eAAsB,2BACpB,QACA,SACA,QACA,QACiB;AACjB,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,qBAAqB;AAAA,IACjE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oBAAoB,eAAe,UAAU,OAAO,MAAM,GAAG;AAAA,IACxF,MAAM,KAAK,UAAU;AAAA,MACnB,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,EAAW,OAAO;AAAA;AAAA,oBAAU,MAAM;AAAA;AAAA;AAAA,QAC7C;AAAA,MACF;AAAA,MACA,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV,CAAC;AAAA,IACD;AAAA,EACF,CAAC;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,QAAQ,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE,CAAC,EAAE;AAAA,EACrF;AACA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO,eAAe,KAAK,UAAU,CAAC,GAAG,SAAS,SAAS,KAAK,KAAK,EAAE;AACzE;AAGA,eAAsB,wBACpB,QACA,SACA,QACA,SACA,QACe;AACf,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,qBAAqB;AAAA,IACjE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oBAAoB,eAAe,UAAU,OAAO,MAAM,GAAG;AAAA,IACxF,MAAM,KAAK,UAAU;AAAA,MACnB,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,EAAW,OAAO;AAAA;AAAA,oBAAU,MAAM;AAAA;AAAA;AAAA,QAC7C;AAAA,MACF;AAAA,MACA,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV,CAAC;AAAA,IACD;AAAA,EACF,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,QAAQ,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE,CAAC,EAAE;AAAA,EACrF;AACA,MAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAE3D,QAAM,SAAS,SAAS,KAAK,UAAU;AACvC,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AACb,MAAI,cAAc;AAGlB,SAAO,MAAM;AACX,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AACV,cAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,UAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,aAAS,MAAM,IAAI,KAAK;AACxB,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAQ,WAAW,OAAO,EAAG;AAClC,YAAM,OAAO,QAAQ,MAAM,CAAC,EAAE,KAAK;AACnC,UAAI,SAAS,SAAU;AACvB,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,cAAM,QAAQ,OAAO,UAAU,CAAC,GAAG,OAAO,WAAW;AACrD,YAAI,OAAO;AACT,yBAAe;AACf,kBAAQ,WAAW;AAAA,QACrB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,2BACd,QACA,YACM;AACN,sBAAoB,QAAQ,CAAC,SAAS;AACpC,WAAO,UAAU,kCAAkC,MAAM;AAAA,MACvD,0BAA0B,CAAC,QAAQ,aAAa;AAC9C,cAAM,UAAU,WAAW;AAC3B,YAAI,CAAC,QAAS,QAAO,EAAE,OAAO,CAAC,EAAE;AACjC,YAAI,QAAQ,eAAe,SAAS,cAAc,QAAQ,WAAW,SAAS,QAAQ;AACpF,qBAAW,UAAU;AACrB,iBAAO,EAAE,OAAO,CAAC,EAAE;AAAA,QACrB;AACA,eAAO;AAAA,UACL,OAAO;AAAA,YACL;AAAA,cACE,YAAY,QAAQ;AAAA,cACpB,OAAO;AAAA,gBACL,iBAAiB,SAAS;AAAA,gBAC1B,eAAe,SAAS;AAAA,gBACxB,aAAa,SAAS;AAAA,gBACtB,WAAW,SAAS;AAAA,cACtB;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,0BAA0B,MAAM;AAAA,MAAC;AAAA,IACnC,CAAC;AAAA,EACH,CAAC;AACH;AAMO,SAAS,4BACd,QACA,cACA,YACA,iBACA,SACwB;AACxB,MAAI,yBAAiD;AAErD,QAAM,eAAe,CAAC,QACpB,eAAe,UAAU,IAAI,SAAS,gBAAgB,IAAI,SAAS,SAAS,SAAS;AAEvF,QAAM,sBAAsB,YAAY;AACtC,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,aAAa,CAAC,UAAU,OAAO,KAAK,GAAG;AAC1C,gBAAU,0HAAgC;AAC1C;AAAA,IACF;AAEA,UAAM,WAAW,OAAO,YAAY;AACpC,QAAI,CAAC,SAAU;AACf,UAAM,QAAQ,OAAO,SAAS;AAC9B,QAAI,CAAC,MAAO;AAEZ,UAAM,WAAW,MAAM,eAAe,SAAS,UAAU;AACzD,UAAM,qBAAqB,SAAS,UAAU,GAAG,SAAS,SAAS,CAAC;AACpE,QAAI,CAAC,mBAAmB,KAAK,EAAG;AAEhC,UAAM,YAAY,KAAK,IAAI,GAAG,SAAS,aAAa,CAAC;AACrD,UAAM,eAAyB,CAAC;AAChC,aAAS,IAAI,WAAW,IAAI,SAAS,YAAY,KAAK;AACpD,mBAAa,KAAK,MAAM,eAAe,IAAI,CAAC,CAAC;AAAA,IAC/C;AACA,UAAM,cAAc,aAAa,KAAK,IAAI;AAE1C,4BAAwB,MAAM;AAC9B,6BAAyB,IAAI,gBAAgB;AAC7C,eAAW,UAAU;AAErB,UAAM,cAAc,oBAAoB,QAAQ,SAAS,YAAY,SAAS,MAAM;AACpF,sBAAkB,IAAI;AAEtB,UAAM,YAAY,UAAU,eAAe;AAE3C,QAAI;AACF,UAAI,WAAW;AACb,YAAI,eAAqD;AACzD,cAAM,kBAAkB,MAAM;AAC5B,cAAI,iBAAiB,KAAM,cAAa,YAAY;AACpD,yBAAe,WAAW,MAAM;AAC9B,2BAAe;AACf,gBAAI,WAAW,SAAS;AACtB,qBAAO,QAAQ,MAAM,uCAAuC,CAAC,CAAC;AAAA,YAChE;AAAA,UACF,GAAG,EAAE;AAAA,QACP;AAEA,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA,CAAC,gBAAgB;AACf,gBAAI,aAAa,eAAe,WAAW;AAC3C,gBAAI,WAAW,WAAW,kBAAkB,GAAG;AAC7C,2BAAa,WAAW,MAAM,mBAAmB,MAAM;AAAA,YACzD;AACA,gBAAI,CAAC,WAAW,KAAK,EAAG;AACxB,uBAAW,UAAU;AAAA,cACnB,YAAY,SAAS;AAAA,cACrB,QAAQ,SAAS;AAAA,cACjB;AAAA,YACF;AACA,4BAAgB;AAAA,UAClB;AAAA,UACA,uBAAuB;AAAA,QACzB;AAEA,YAAI,iBAAiB,KAAM,cAAa,YAAY;AACpD,YAAI,WAAW,SAAS;AACtB,iBAAO,QAAQ,MAAM,uCAAuC,CAAC,CAAC;AAAA,QAChE;AAAA,MACF,OAAO;AACL,cAAM,aAAa,MAAM;AAAA,UACvB;AAAA,UACA;AAAA,UACA;AAAA,UACA,uBAAuB;AAAA,QACzB;AACA,YAAI,CAAC,WAAY;AACjB,YAAI,aAAa;AACjB,YAAI,WAAW,WAAW,kBAAkB,GAAG;AAC7C,uBAAa,WAAW,MAAM,mBAAmB,MAAM;AAAA,QACzD;AACA,YAAI,CAAC,WAAW,KAAK,EAAG;AACxB,mBAAW,UAAU;AAAA,UACnB,YAAY,SAAS;AAAA,UACrB,QAAQ,SAAS;AAAA,UACjB;AAAA,QACF;AACA,eAAO,QAAQ,MAAM,uCAAuC,CAAC,CAAC;AAAA,MAChE;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,aAAa,GAAG,EAAG;AACvB,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,gBAAU,oCAAW,GAAG,EAAE;AAC1B,cAAQ,MAAM,0BAA0B,GAAG;AAAA,IAC7C,UAAE;AACA,kBAAY;AACZ,wBAAkB,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,SAAO,OAAO,UAAU,CAAC,MAAM;AAC7B,SAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,QAAQ;AAC7D,QAAE,eAAe;AACjB,WAAK,oBAAoB;AAAA,IAC3B;AAAA,EACF,CAAC;AACH;;;AClRO,IAAM,mBAAN,cAA+B,aAAqC;AAAA,EAYzE,YAAoB,SAAkC;AACpD,UAAM;AADY;AAXpB,SAAQ,SAAyD;AAEjE,SAAQ,wBAAwB;AAChC,SAAQ,uBAAuB;AAC/B,SAAQ,uBAAuB,EAAE,SAAS,KAAiC;AAC3E,SAAQ,kBAAwC,CAAC;AACjD,SAAQ,cAAwC,CAAC;AAEjD,qBAAY;AACZ,iBAAsB;AAIpB,SAAK,SAAS,QAAQ;AACtB,SAAK,kBAAkB,QAAQ,uBAAuB,CAAC;AACvD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEQ,OAAa;AACnB,UAAM,EAAE,UAAU,OAAO,OAAO,WAAW,WAAW,cAAc,IAAI,KAAK;AAG7E,QAAI,CAAC,KAAK,uBAAuB;AAC/B,YAAM,YAAY,EAAE,SAAS,aAAa,CAAC,EAAE;AAC7C,4BAAsB,KAAK,QAAQ,SAAS;AAC5C,WAAK,wBAAwB;AAAA,IAC/B;AAGA,QAAI,CAAC,KAAK,sBAAsB;AAC9B,iCAA2B,KAAK,QAAQ,KAAK,oBAAoB;AACjE,WAAK,uBAAuB;AAAA,IAC9B;AAGA,SAAK,kCAAkC;AAGvC,SAAK,SAAS,KAAK,OAAO,OAAO,OAAO,KAAK,QAAQ,WAAW;AAAA,MAC9D,OAAO,SAAS;AAAA,MAChB,UAAU,YAAY;AAAA,MACtB,OAAO,SAAS;AAAA,MAChB,UAAU;AAAA,MACV,SAAS,EAAE,SAAS,KAAK;AAAA,MACzB,sBAAsB;AAAA,MACtB,iBAAiB;AAAA,MACjB,aAAa;AAAA,MACb,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,MACjB,gBAAgB;AAAA,MAChB,yBAAyB,EAAE,SAAS,KAAK;AAAA,MACzC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,SAAS;AAAA,MACT,eAAe;AAAA,MACf,4BAA4B;AAAA,MAC5B,kBAAkB,EAAE,OAAO,MAAM,UAAU,OAAO,SAAS,MAAM;AAAA,MACjE,mCAAmC;AAAA,MACnC,yBAAyB;AAAA,MACzB,eAAe,EAAE,SAAS,KAAK;AAAA,MAC/B,eAAe;AAAA,MACf,GAAG;AAAA,IACL,CAAC;AAGD,SAAK,iBAAiB;AACtB,SAAK,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,EAC5C;AAAA,EAEQ,mBAAyB;AAC/B,UAAM,SAAS,KAAK;AAEpB,UAAM,KAAK,OAAO,wBAAwB,MAAM;AAC9C,WAAK,KAAK,UAAU,EAAE,OAAO,OAAO,SAAS,EAAE,CAAC;AAAA,IAClD,CAAC;AAED,UAAM,KAAK,OAAO,0BAA0B,CAAC,MAAM;AACjD,WAAK,KAAK,UAAU;AAAA,QAClB,MAAM,EAAE,SAAS;AAAA,QACjB,QAAQ,EAAE,SAAS;AAAA,MACrB,CAAC;AAAA,IACH,CAAC;AAED,UAAM,KAAK,OAAO,qBAAqB,MAAM,KAAK,KAAK,SAAS,MAAS,CAAC;AAC1E,UAAM,KAAK,OAAO,oBAAoB,MAAM,KAAK,KAAK,QAAQ,MAAS,CAAC;AAExE,SAAK,YAAY,KAAK,IAAI,IAAI,IAAI,EAAE;AAAA,EACtC;AAAA,EAEQ,oCAA0C;AAChD,QAAI,KAAK,gBAAgB,WAAW,EAAG;AAEvC,eAAW,YAAY,KAAK,iBAAiB;AAC3C,YAAM,QAAQ,SAAS,aAAa,CAAC,GAAG;AACxC,iBAAW,QAAQ,OAAO;AACxB,aAAK,OAAO,UAAU,+BAA+B,MAAM;AAAA,UACzD,mBAAmB,SAAS;AAAA,UAC5B,wBAAwB,OAAO,OAAO,UAAU,YAAY;AAC1D,kBAAM,MAAM;AAAA,cACV,QAAQ,KAAK;AAAA,cACb;AAAA,cACA;AAAA,cACA,UAAU,MAAM,eAAe,SAAS,UAAU;AAAA,cAClD,aAAa,MAAM,qBAAqB,QAAQ,EAAE;AAAA,cAClD,UAAU,MAAM,SAAS;AAAA,cACzB,kBAAkB,QAAQ;AAAA,YAC5B;AAEA,gBAAI,SAAS,iBAAiB,CAAC,SAAS,cAAc,GAAG,GAAG;AAC1D,qBAAO,EAAE,aAAa,CAAC,EAAE;AAAA,YAC3B;AAEA,kBAAM,cAAc,MAAM,SAAS,QAAQ,GAAG;AAC9C,mBAAO,EAAE,YAAY;AAAA,UACvB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIA,YAA4D;AAC1D,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAmB;AACjB,WAAO,KAAK,QAAQ,SAAS,KAAK;AAAA,EACpC;AAAA,EAEA,SAAS,OAAqB;AAC5B,SAAK,QAAQ,SAAS,KAAK;AAAA,EAC7B;AAAA,EAEA,WAAW,MAAc,OAAiC;AACxD,UAAM,SAAS,KAAK;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,cAAc,SAAS,OAAO,aAAa;AACjD,QAAI,aAAa;AACf,aAAO,aAAa,qBAAqB,CAAC,EAAE,OAAO,aAAa,KAAK,CAAC,CAAC;AACvE,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AAAA,EAEA,SAAe;AACb,SAAK,QAAQ,UAAU,8BAA8B,GAAG,IAAI;AAAA,EAC9D;AAAA,EAEA,QAAc;AACZ,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA,EAEA,YAAY,UAAwB;AAClC,UAAM,QAAQ,KAAK,QAAQ,SAAS;AACpC,QAAI,OAAO;AACT,WAAK,OAAO,OAAO,iBAAiB,OAAO,QAAQ;AACnD,WAAK,KAAK,kBAAkB,EAAE,SAAS,CAAC;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,SAAS,OAAqB;AAC5B,SAAK,OAAO,OAAO,SAAS,KAAK;AAAA,EACnC;AAAA,EAEA,oBAAsD;AACpD,UAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,WAAO,MACH,EAAE,MAAM,IAAI,YAAY,QAAQ,IAAI,OAAO,IAC3C,EAAE,MAAM,GAAG,QAAQ,EAAE;AAAA,EAC3B;AAAA,EAEA,eAAyC;AACvC,WAAO,KAAK,QAAQ,aAAa,KAAK;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,QAAgC;AAC3C,SAAK,QAAQ,YAAY;AAAA,EAC3B;AAAA,EAEA,0BAAiE;AAC/D,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,eAAW,KAAK,KAAK,YAAa,GAAE,QAAQ;AAC5C,SAAK,cAAc,CAAC;AACpB,SAAK,QAAQ,QAAQ;AACrB,SAAK,SAAS;AACd,SAAK,mBAAmB;AAAA,EAC1B;AACF;;;AC3NA,SAAS,eAAuB;AAC9B,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,UAAM,IAAI,MAAM,MAAM,IAAK,IAAI,IAAO;AACtC,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB,CAAC;AACH;AAMO,IAAM,mBAAN,cAA+B,aAAqC;AAAA,EAMzE,YAAY,WAAsB;AAChC,UAAM;AANR,SAAQ,WAA0B,CAAC;AACnC,SAAQ,kBAA0C;AAClD,SAAQ,UAAU;AAKhB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,aAAa,QAAyB;AACpC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,eAA0B;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,cAAsC;AACpC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,KACJ,WACA,SACe;AACf,UAAM,UAAU,UAAU,KAAK;AAC/B,QAAI,CAAC,WAAW,KAAK,QAAS;AAE9B,QAAI,CAAC,KAAK,UAAU,OAAO,KAAK,GAAG;AACjC,YAAMC,WAAuB,EAAE,IAAI,aAAa,GAAG,MAAM,QAAQ,SAAS,QAAQ;AAClF,WAAK,SAAS,KAAKA,QAAO;AAC1B,WAAK,KAAK,gBAAgBA,QAAO;AACjC,YAAMC,gBAA4B;AAAA,QAChC,IAAI,aAAa;AAAA,QACjB,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AACA,WAAK,SAAS,KAAKA,aAAY;AAC/B,WAAK,KAAK,gBAAgBA,aAAY;AACtC;AAAA,IACF;AAEA,UAAM,UAAuB,EAAE,IAAI,aAAa,GAAG,MAAM,QAAQ,SAAS,QAAQ;AAClF,SAAK,SAAS,KAAK,OAAO;AAC1B,SAAK,KAAK,gBAAgB,OAAO;AAEjC,UAAM,eAA4B;AAAA,MAChC,IAAI,aAAa;AAAA,MACjB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AACA,SAAK,SAAS,KAAK,YAAY;AAC/B,SAAK,KAAK,gBAAgB,YAAY;AAEtC,SAAK,UAAU;AACf,SAAK,KAAK,iBAAiB,EAAE,SAAS,KAAK,CAAC;AAE5C,QAAI;AACF,WAAK,kBAAkB,IAAI,gBAAgB;AAE3C,YAAM,eAAe,SAAS,WAC1B,gEAAgE,QAAQ,QAAQ,gGAChF;AAEJ,YAAM,gBAAgB,KAAK,SACxB,OAAO,CAAC,MAAM,EAAE,OAAO,aAAa,EAAE,EACtC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,QAAQ,EAAE;AAEpD,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,UAAU,OAAO,qBAAqB;AAAA,QACzE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,UAAU,MAAM;AAAA,QAChD;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB,OAAO,KAAK,UAAU;AAAA,UACtB,UAAU,CAAC,EAAE,MAAM,UAAU,SAAS,aAAa,GAAG,GAAG,aAAa;AAAA,UACtE,QAAQ;AAAA,QACV,CAAC;AAAA,QACD,QAAQ,KAAK,gBAAgB;AAAA,MAC/B,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,YAAI,UAAU;AACd,YAAI;AACF,oBAAU,MAAM,SAAS,KAAK;AAAA,QAChC,QAAQ;AAAA,QAER;AACA,YAAI,OAAO;AACX,YAAI,SAAS,WAAW,IAAK,QAAO;AAAA,iBAC3B,SAAS,WAAW,IAAK,QAAO;AAAA,iBAChC,SAAS,WAAW,IAAK,QAAO;AAAA,iBAChC,SAAS,UAAU,IAAK,QAAO;AACxC,cAAM,IAAI,MAAM,QAAQ,SAAS,MAAM,GAAG,IAAI,GAAG,UAAU;AAAA,EAAK,OAAO,KAAK,EAAE,EAAE;AAAA,MAClF;AAEA,YAAM,SAAS,SAAS,KAAM,UAAU;AACxC,YAAM,UAAU,IAAI,YAAY;AAChC,UAAI,SAAS;AACb,UAAI,cAAc;AAGlB,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,KAAM;AACV,kBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,cAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,iBAAS,MAAM,IAAI,KAAK;AACxB,mBAAW,QAAQ,OAAO;AACxB,cAAI,CAAC,KAAK,WAAW,QAAQ,EAAG;AAChC,gBAAM,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAChC,cAAI,SAAS,SAAU;AACvB,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,IAAI;AAG9B,kBAAM,QAAQ,OAAO,UAAU,CAAC,GAAG,OAAO,WAAW;AACrD,gBAAI,OAAO;AACT,6BAAe;AACf,2BAAa,UAAU;AACvB,mBAAK,KAAK,kBAAkB,EAAE,GAAG,aAAa,CAAC;AAAA,YACjD;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAEA,mBAAa,cAAc;AAC3B,WAAK,KAAK,kBAAkB,EAAE,GAAG,aAAa,CAAC;AAAA,IACjD,SAAS,KAAc;AACrB,YAAM,UAAU,eAAe,SAAS,IAAI,SAAS;AACrD,YAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,YAAM,eAAe,eAAe,aAAa,OAAO,YAAY,EAAE,SAAS,OAAO;AACtF,YAAM,eAAe,UACjB,0CACA,eACE;AAAA;AAAA,EAAgC,MAAM,KACtC,8CAAW,MAAM;AACvB,mBAAa,UAAU;AACvB,mBAAa,cAAc;AAC3B,WAAK,KAAK,kBAAkB,EAAE,GAAG,aAAa,CAAC;AAC/C,UAAI,CAAC,QAAS,MAAK,KAAK,SAAS,EAAE,SAAS,OAAO,CAAC;AAAA,IACtD,UAAE;AACA,WAAK,UAAU;AACf,WAAK,KAAK,iBAAiB,EAAE,SAAS,MAAM,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,OAAa;AACX,SAAK,iBAAiB,MAAM;AAAA,EAC9B;AAAA,EAEA,QAAc;AACZ,SAAK,WAAW,CAAC;AACjB,SAAK,KAAK,WAAW,MAAS;AAAA,EAChC;AAAA,EAEA,UAAgB;AACd,SAAK,iBAAiB,MAAM;AAC5B,SAAK,mBAAmB;AAAA,EAC1B;AACF;;;ACtKO,IAAM,YAAN,cAAwB,aAA8B;AAAA,EAAtD;AAAA;AACL,SAAQ,SAAkC;AAC1C,SAAQ,OAAgC;AAAA;AAAA,EAExC,eAAe,YAA0C;AACvD,SAAK,SAAS;AAGd,UAAM,YAAY,WAAW,GAAG,UAAU,CAAC,SAAS,KAAK,KAAK,iBAAiB,IAAI,CAAC;AACpF,UAAM,YAAY,WAAW,GAAG,UAAU,CAAC,SAAS,KAAK,KAAK,iBAAiB,IAAI,CAAC;AACpF,UAAM,UAAU,WAAW,GAAG,kBAAkB,CAAC,SAAS,KAAK,KAAK,yBAAyB,IAAI,CAAC;AAGlG,UAAM,YAAY,KAAK,GAAG,mBAAmB,CAAC,EAAE,KAAK,MAAM,WAAW,WAAW,IAAI,CAAC;AACtF,UAAM,aAAa,KAAK,GAAG,oBAAoB,CAAC,EAAE,KAAK,MAAM,WAAW,SAAS,IAAI,CAAC;AAEtF,SAAK,KAAK,gBAAgB,UAAU;AAEpC,WAAO,MAAM;AACX,gBAAU;AACV,gBAAU;AACV,cAAQ;AACR,gBAAU;AACV,iBAAW;AACX,UAAI,KAAK,WAAW,YAAY;AAC9B,aAAK,SAAS;AACd,aAAK,KAAK,kBAAkB,MAAS;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,aAAa,YAA0C;AACrD,SAAK,OAAO;AACZ,SAAK,KAAK,cAAc,UAAU;AAClC,WAAO,MAAM;AACX,UAAI,KAAK,SAAS,YAAY;AAC5B,aAAK,OAAO;AACZ,aAAK,KAAK,gBAAgB,MAAS;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,YAAqC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAmC;AACjC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,mBAA2B;AACzB,WAAO,KAAK,QAAQ,SAAS,KAAK;AAAA,EACpC;AAAA;AAAA,EAGA,oBAAwC;AACtC,UAAM,SAAS,KAAK,QAAQ,UAAU;AACtC,WAAO,QAAQ,SAAS,GAAG,cAAc;AAAA,EAC3C;AAAA;AAAA,EAGA,eAAe,MAAoB;AACjC,SAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;AAAA,EACvC;AAAA;AAAA,EAGA,cAAc,MAAoB;AAChC,SAAK,KAAK,oBAAoB,EAAE,KAAK,CAAC;AAAA,EACxC;AACF;","names":["schema","userMsg","assistantMsg"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monaco-ai-editor/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic core for Monaco AI editor (controllers, event bus, completion providers).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"monaco-editor": ">=0.40.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"monaco-editor": "^0.55.1",
|
|
28
|
+
"tsup": "^8.3.0",
|
|
29
|
+
"typescript": "^5.6.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type * as MonacoType from 'monaco-editor';
|
|
2
|
+
import type { ApiConfig, PendingCompletion } from '../types';
|
|
3
|
+
import { AI_INLINE_LANGUAGES } from '../constants';
|
|
4
|
+
|
|
5
|
+
/** 去除模型返回中可能包含的代码围栏(```lang ... ```) */
|
|
6
|
+
function stripCodeFence(text: string): string {
|
|
7
|
+
return text.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] as const;
|
|
11
|
+
|
|
12
|
+
function ensureSpinnerStyle() {
|
|
13
|
+
const id = 'mae-ai-completion-spinner-style';
|
|
14
|
+
if (typeof document === 'undefined' || document.getElementById(id)) return;
|
|
15
|
+
const style = document.createElement('style');
|
|
16
|
+
style.id = id;
|
|
17
|
+
const frameCss = SPINNER_FRAMES.map((ch, i) => `.mae-ai-spinner-f${i}::after { content: " ${ch}"; }`).join('\n');
|
|
18
|
+
style.textContent = `
|
|
19
|
+
@keyframes mae-ai-completion-glow {
|
|
20
|
+
0%, 100% { text-shadow: 0 0 3px rgba(96, 165, 250, 0.35); }
|
|
21
|
+
50% { text-shadow: 0 0 9px rgba(96, 165, 250, 0.95), 0 0 18px rgba(59, 130, 246, 0.35); }
|
|
22
|
+
}
|
|
23
|
+
${frameCss}
|
|
24
|
+
[class^="mae-ai-spinner-f"]::after {
|
|
25
|
+
color: #60a5fa;
|
|
26
|
+
font-family: "Segoe UI Symbol", "Noto Sans Symbols", "Apple Symbols", monospace;
|
|
27
|
+
display: inline-block;
|
|
28
|
+
margin-left: 2px;
|
|
29
|
+
animation: mae-ai-completion-glow 1s ease-in-out infinite;
|
|
30
|
+
}`;
|
|
31
|
+
document.head.appendChild(style);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCursorSpinner(
|
|
35
|
+
editor: MonacoType.editor.IStandaloneCodeEditor,
|
|
36
|
+
line: number,
|
|
37
|
+
col: number,
|
|
38
|
+
): () => void {
|
|
39
|
+
ensureSpinnerStyle();
|
|
40
|
+
let frameIdx = 0;
|
|
41
|
+
const makeDecor = () => [{
|
|
42
|
+
range: { startLineNumber: line, startColumn: col, endLineNumber: line, endColumn: col },
|
|
43
|
+
options: { afterContentClassName: `mae-ai-spinner-f${frameIdx}` },
|
|
44
|
+
}];
|
|
45
|
+
const collection = editor.createDecorationsCollection(makeDecor());
|
|
46
|
+
const timer = setInterval(() => {
|
|
47
|
+
frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
|
|
48
|
+
collection.set(makeDecor());
|
|
49
|
+
}, 80);
|
|
50
|
+
return () => {
|
|
51
|
+
clearInterval(timer);
|
|
52
|
+
collection.clear();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 非流式调用 OpenAI 兼容 API(使用原生 fetch,无 axios 依赖) */
|
|
57
|
+
export async function fetchAiCompletionNonStream(
|
|
58
|
+
config: ApiConfig,
|
|
59
|
+
context: string,
|
|
60
|
+
prefix: string,
|
|
61
|
+
signal?: AbortSignal,
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
model: config.model,
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: 'system',
|
|
71
|
+
content: '你是一个代码补全助手。基于用户提供的代码上下文和前缀,生成短小精悍的代码补全。只返回补全的代码片段,不要解释。',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
role: 'user',
|
|
75
|
+
content: `代码上下文:\n${context}\n\n前缀:${prefix}\n\n请继续补全这段代码,只返回补全部分。`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
max_tokens: 1024,
|
|
79
|
+
stream: false,
|
|
80
|
+
}),
|
|
81
|
+
signal,
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`HTTP ${response.status}: ${await response.text().catch(() => '')}`);
|
|
85
|
+
}
|
|
86
|
+
const data = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };
|
|
87
|
+
return stripCodeFence(data.choices?.[0]?.message?.content?.trim() ?? '');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 流式调用(fetch + ReadableStream) */
|
|
91
|
+
export async function fetchAiCompletionStream(
|
|
92
|
+
config: ApiConfig,
|
|
93
|
+
context: string,
|
|
94
|
+
prefix: string,
|
|
95
|
+
onChunk: (accumulated: string) => void,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
model: config.model,
|
|
103
|
+
messages: [
|
|
104
|
+
{
|
|
105
|
+
role: 'system',
|
|
106
|
+
content: '你是一个代码补全助手。基于用户提供的代码上下文和前缀,生成短小精悍的代码补全。只返回补全的代码片段,不要解释。',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
role: 'user',
|
|
110
|
+
content: `代码上下文:\n${context}\n\n前缀:${prefix}\n\n请继续补全这段代码,只返回补全部分。`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
max_tokens: 1024,
|
|
114
|
+
stream: true,
|
|
115
|
+
}),
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`HTTP ${response.status}: ${await response.text().catch(() => '')}`);
|
|
121
|
+
}
|
|
122
|
+
if (!response.body) throw new Error('Response body is null');
|
|
123
|
+
|
|
124
|
+
const reader = response.body.getReader();
|
|
125
|
+
const decoder = new TextDecoder();
|
|
126
|
+
let buffer = '';
|
|
127
|
+
let accumulated = '';
|
|
128
|
+
|
|
129
|
+
// eslint-disable-next-line no-constant-condition
|
|
130
|
+
while (true) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done) break;
|
|
133
|
+
buffer += decoder.decode(value, { stream: true });
|
|
134
|
+
const lines = buffer.split('\n');
|
|
135
|
+
buffer = lines.pop() ?? '';
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
139
|
+
const data = trimmed.slice(5).trim();
|
|
140
|
+
if (data === '[DONE]') return;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(data) as { choices?: Array<{ delta?: { content?: string } }> };
|
|
143
|
+
const delta = parsed.choices?.[0]?.delta?.content ?? '';
|
|
144
|
+
if (delta) {
|
|
145
|
+
accumulated += delta;
|
|
146
|
+
onChunk(accumulated);
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore malformed lines
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 注册 AI 内联幽灵文本提供者 */
|
|
156
|
+
export function registerAiInlineCompletion(
|
|
157
|
+
monaco: typeof MonacoType,
|
|
158
|
+
pendingRef: { current: PendingCompletion | null },
|
|
159
|
+
): void {
|
|
160
|
+
AI_INLINE_LANGUAGES.forEach((lang) => {
|
|
161
|
+
monaco.languages.registerInlineCompletionsProvider(lang, {
|
|
162
|
+
provideInlineCompletions: (_model, position) => {
|
|
163
|
+
const pending = pendingRef.current;
|
|
164
|
+
if (!pending) return { items: [] };
|
|
165
|
+
if (pending.lineNumber !== position.lineNumber || pending.column !== position.column) {
|
|
166
|
+
pendingRef.current = null;
|
|
167
|
+
return { items: [] };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
items: [
|
|
171
|
+
{
|
|
172
|
+
insertText: pending.insertText,
|
|
173
|
+
range: {
|
|
174
|
+
startLineNumber: position.lineNumber,
|
|
175
|
+
endLineNumber: position.lineNumber,
|
|
176
|
+
startColumn: position.column,
|
|
177
|
+
endColumn: position.column,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
disposeInlineCompletions: () => {},
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 注册 AI 补全命令(Ctrl+Alt+L 手动触发,流式更新幽灵文本)。
|
|
190
|
+
* 框架无关:通过函数引用方式读取最新的 apiConfig。
|
|
191
|
+
*/
|
|
192
|
+
export function registerAiCompletionCommand(
|
|
193
|
+
editor: MonacoType.editor.IStandaloneCodeEditor,
|
|
194
|
+
getApiConfig: () => ApiConfig | null,
|
|
195
|
+
pendingRef: { current: PendingCompletion | null },
|
|
196
|
+
onLoadingChange?: (loading: boolean) => void,
|
|
197
|
+
onError?: (message: string) => void,
|
|
198
|
+
): MonacoType.IDisposable {
|
|
199
|
+
let currentAbortController: AbortController | null = null;
|
|
200
|
+
|
|
201
|
+
const isAbortError = (err: unknown): boolean =>
|
|
202
|
+
err instanceof Error && (err.name === 'AbortError' || err.message?.includes('aborted'));
|
|
203
|
+
|
|
204
|
+
const triggerAiCompletion = async () => {
|
|
205
|
+
const apiConfig = getApiConfig();
|
|
206
|
+
if (!apiConfig || !apiConfig.apiKey.trim()) {
|
|
207
|
+
onError?.('未配置 API Key,请先配置 AI 服务再使用补全功能。');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const position = editor.getPosition();
|
|
212
|
+
if (!position) return;
|
|
213
|
+
const model = editor.getModel();
|
|
214
|
+
if (!model) return;
|
|
215
|
+
|
|
216
|
+
const lineText = model.getLineContent(position.lineNumber);
|
|
217
|
+
const textBeforePosition = lineText.substring(0, position.column - 1);
|
|
218
|
+
if (!textBeforePosition.trim()) return;
|
|
219
|
+
|
|
220
|
+
const startLine = Math.max(0, position.lineNumber - 5);
|
|
221
|
+
const contextLines: string[] = [];
|
|
222
|
+
for (let i = startLine; i < position.lineNumber; i++) {
|
|
223
|
+
contextLines.push(model.getLineContent(i + 1));
|
|
224
|
+
}
|
|
225
|
+
const contextCode = contextLines.join('\n');
|
|
226
|
+
|
|
227
|
+
currentAbortController?.abort();
|
|
228
|
+
currentAbortController = new AbortController();
|
|
229
|
+
pendingRef.current = null;
|
|
230
|
+
|
|
231
|
+
const stopSpinner = createCursorSpinner(editor, position.lineNumber, position.column);
|
|
232
|
+
onLoadingChange?.(true);
|
|
233
|
+
|
|
234
|
+
const useStream = apiConfig.streamMode !== false;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
if (useStream) {
|
|
238
|
+
let triggerTimer: ReturnType<typeof setTimeout> | null = null;
|
|
239
|
+
const scheduleTrigger = () => {
|
|
240
|
+
if (triggerTimer !== null) clearTimeout(triggerTimer);
|
|
241
|
+
triggerTimer = setTimeout(() => {
|
|
242
|
+
triggerTimer = null;
|
|
243
|
+
if (pendingRef.current) {
|
|
244
|
+
editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});
|
|
245
|
+
}
|
|
246
|
+
}, 80);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await fetchAiCompletionStream(
|
|
250
|
+
apiConfig,
|
|
251
|
+
contextCode,
|
|
252
|
+
textBeforePosition,
|
|
253
|
+
(accumulated) => {
|
|
254
|
+
let insertText = stripCodeFence(accumulated);
|
|
255
|
+
if (insertText.startsWith(textBeforePosition)) {
|
|
256
|
+
insertText = insertText.slice(textBeforePosition.length);
|
|
257
|
+
}
|
|
258
|
+
if (!insertText.trim()) return;
|
|
259
|
+
pendingRef.current = {
|
|
260
|
+
lineNumber: position.lineNumber,
|
|
261
|
+
column: position.column,
|
|
262
|
+
insertText,
|
|
263
|
+
};
|
|
264
|
+
scheduleTrigger();
|
|
265
|
+
},
|
|
266
|
+
currentAbortController.signal,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (triggerTimer !== null) clearTimeout(triggerTimer);
|
|
270
|
+
if (pendingRef.current) {
|
|
271
|
+
editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
const completion = await fetchAiCompletionNonStream(
|
|
275
|
+
apiConfig,
|
|
276
|
+
contextCode,
|
|
277
|
+
textBeforePosition,
|
|
278
|
+
currentAbortController.signal,
|
|
279
|
+
);
|
|
280
|
+
if (!completion) return;
|
|
281
|
+
let insertText = completion;
|
|
282
|
+
if (insertText.startsWith(textBeforePosition)) {
|
|
283
|
+
insertText = insertText.slice(textBeforePosition.length);
|
|
284
|
+
}
|
|
285
|
+
if (!insertText.trim()) return;
|
|
286
|
+
pendingRef.current = {
|
|
287
|
+
lineNumber: position.lineNumber,
|
|
288
|
+
column: position.column,
|
|
289
|
+
insertText,
|
|
290
|
+
};
|
|
291
|
+
editor.trigger('ai', 'editor.action.inlineSuggest.trigger', {});
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (isAbortError(err)) return;
|
|
295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
296
|
+
onError?.(`AI 补全失败:${msg}`);
|
|
297
|
+
console.error('[AI Completion] Error:', err);
|
|
298
|
+
} finally {
|
|
299
|
+
stopSpinner();
|
|
300
|
+
onLoadingChange?.(false);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return editor.onKeyDown((e) => {
|
|
305
|
+
if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === 'KeyL') {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
void triggerAiCompletion();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { EventEmitter } from './EventEmitter';
|
|
2
|
+
import type { EditorController } from '../controllers/EditorController';
|
|
3
|
+
import type { AiChatController } from '../controllers/AiChatController';
|
|
4
|
+
|
|
5
|
+
export interface EditorBusEvents {
|
|
6
|
+
/** 编辑器已注册 */
|
|
7
|
+
'editor:ready': EditorController;
|
|
8
|
+
/** 编辑器即将卸载 */
|
|
9
|
+
'editor:dispose': void;
|
|
10
|
+
/** 编辑器内容变化 */
|
|
11
|
+
'editor:change': { value: string };
|
|
12
|
+
/** 编辑器光标变化 */
|
|
13
|
+
'editor:cursor': { line: number; column: number };
|
|
14
|
+
/** 编辑器语言变化 */
|
|
15
|
+
'editor:languageChange': { language: string };
|
|
16
|
+
|
|
17
|
+
/** 聊天面板已注册 */
|
|
18
|
+
'chat:ready': AiChatController;
|
|
19
|
+
/** 聊天面板卸载 */
|
|
20
|
+
'chat:dispose': void;
|
|
21
|
+
/** 聊天面板请求向编辑器插入代码 */
|
|
22
|
+
'chat:insertCode': { code: string };
|
|
23
|
+
/** 聊天面板请求替换编辑器全部内容 */
|
|
24
|
+
'chat:replaceCode': { code: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 编辑器与聊天面板的通信总线。
|
|
29
|
+
* 在两个组件都引入时由协调器(Coordinator)创建并共享,实现自动联动。
|
|
30
|
+
* 当只引入其中一个组件时,bus 可为 null,组件正常独立工作。
|
|
31
|
+
*/
|
|
32
|
+
export class EditorBus extends EventEmitter<EditorBusEvents> {
|
|
33
|
+
private editor: EditorController | null = null;
|
|
34
|
+
private chat: AiChatController | null = null;
|
|
35
|
+
|
|
36
|
+
registerEditor(controller: EditorController): () => void {
|
|
37
|
+
this.editor = controller;
|
|
38
|
+
|
|
39
|
+
// 桥接:编辑器事件 → bus 事件
|
|
40
|
+
const offChange = controller.on('change', (data) => this.emit('editor:change', data));
|
|
41
|
+
const offCursor = controller.on('cursor', (data) => this.emit('editor:cursor', data));
|
|
42
|
+
const offLang = controller.on('languageChange', (data) => this.emit('editor:languageChange', data));
|
|
43
|
+
|
|
44
|
+
// 桥接:bus 上的 chat 命令 → 编辑器操作
|
|
45
|
+
const offInsert = this.on('chat:insertCode', ({ code }) => controller.insertText(code));
|
|
46
|
+
const offReplace = this.on('chat:replaceCode', ({ code }) => controller.setValue(code));
|
|
47
|
+
|
|
48
|
+
this.emit('editor:ready', controller);
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
offChange();
|
|
52
|
+
offCursor();
|
|
53
|
+
offLang();
|
|
54
|
+
offInsert();
|
|
55
|
+
offReplace();
|
|
56
|
+
if (this.editor === controller) {
|
|
57
|
+
this.editor = null;
|
|
58
|
+
this.emit('editor:dispose', undefined);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registerChat(controller: AiChatController): () => void {
|
|
64
|
+
this.chat = controller;
|
|
65
|
+
this.emit('chat:ready', controller);
|
|
66
|
+
return () => {
|
|
67
|
+
if (this.chat === controller) {
|
|
68
|
+
this.chat = null;
|
|
69
|
+
this.emit('chat:dispose', undefined);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getEditor(): EditorController | null {
|
|
75
|
+
return this.editor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getChat(): AiChatController | null {
|
|
79
|
+
return this.chat;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 让聊天面板向编辑器请求当前内容 */
|
|
83
|
+
getEditorContent(): string {
|
|
84
|
+
return this.editor?.getValue() ?? '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 让聊天面板向编辑器请求当前语言 */
|
|
88
|
+
getEditorLanguage(): string | undefined {
|
|
89
|
+
const editor = this.editor?.getEditor();
|
|
90
|
+
return editor?.getModel()?.getLanguageId();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 让聊天面板请求向编辑器插入代码 */
|
|
94
|
+
insertToEditor(code: string): void {
|
|
95
|
+
this.emit('chat:insertCode', { code });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 让聊天面板请求替换编辑器全部内容 */
|
|
99
|
+
replaceEditor(code: string): void {
|
|
100
|
+
this.emit('chat:replaceCode', { code });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简事件发射器,框架无关。
|
|
3
|
+
* 用于 EditorController / AiChatController / EditorBus 之间的事件通信。
|
|
4
|
+
*/
|
|
5
|
+
export type Listener<T> = (payload: T) => void;
|
|
6
|
+
|
|
7
|
+
export class EventEmitter<EventMap> {
|
|
8
|
+
private listeners: { [K in keyof EventMap]?: Set<Listener<EventMap[K]>> } = {};
|
|
9
|
+
|
|
10
|
+
on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): () => void {
|
|
11
|
+
if (!this.listeners[event]) this.listeners[event] = new Set();
|
|
12
|
+
this.listeners[event]!.add(listener);
|
|
13
|
+
return () => this.off(event, listener);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): void {
|
|
17
|
+
this.listeners[event]?.delete(listener);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
|
|
21
|
+
this.listeners[event]?.forEach((listener) => {
|
|
22
|
+
try {
|
|
23
|
+
listener(payload);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(`[EventEmitter] listener for "${String(event)}" threw:`, err);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
removeAllListeners(): void {
|
|
31
|
+
this.listeners = {};
|
|
32
|
+
}
|
|
33
|
+
}
|