import type * as monacoTypes from 'monaco-editor'; import { YamlCompletionItem, YamlCompletionType, YamlEditorSchema } from '../types'; export type Monaco = typeof monacoTypes; function getMonacoCompletionItemKind(type: YamlCompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { switch (type) { case 'KEYWORD': return monaco.languages.CompletionItemKind.Keyword; case 'VALUE': return monaco.languages.CompletionItemKind.Value; case 'KEY': return monaco.languages.CompletionItemKind.Property; case 'BOOLEAN': return monaco.languages.CompletionItemKind.Constant; case 'NUMBER': return monaco.languages.CompletionItemKind.Value; case 'STRING': return monaco.languages.CompletionItemKind.Text; default: return monaco.languages.CompletionItemKind.Text; } } export function getYamlCompletionProvider(monaco: Monaco, schemas: YamlEditorSchema[] = []): monacoTypes.languages.CompletionItemProvider { // 更准确的位置分析 const analyzePosition = (model: monacoTypes.editor.ITextModel, position: monacoTypes.Position) => { const line = model.getLineContent(position.lineNumber); const column = position.column; const beforeCursor = line.substring(0, column - 1); const trimmedLine = line.trim(); // 检查是否在值位置 const colonIndex = beforeCursor.indexOf(':'); const isAfterColon = colonIndex !== -1; // 如果在冒号后面,检查是否是值位置 let isInValuePosition = false; let currentKey = null; if (isAfterColon) { let keyPart = beforeCursor.substring(0, colonIndex).trim(); // 如果键名以 - 开头(数组项),去掉 - 和后面的空格 if (keyPart.startsWith('- ')) { keyPart = keyPart.substring(2).trim(); } else if (keyPart === '-') { // 如果只有一个 -,说明键名在下一个词 keyPart = ''; } currentKey = keyPart; // 检查冒号后面是否有空格或者光标紧跟在冒号后面 const afterColon = beforeCursor.substring(colonIndex + 1); isInValuePosition = afterColon.length === 0 || /^\s+$/.test(afterColon); } // 检查是否已经有完整的值 const hasCompleteValue = trimmedLine.includes(':') && !trimmedLine.endsWith(':') && trimmedLine.split(':')[1].trim().length > 0; return { isInKeyPosition: !isAfterColon || trimmedLine.endsWith(':'), isInValuePosition: isInValuePosition && !hasCompleteValue, isAtEndOfCompletedValue: hasCompleteValue, currentKey, line: trimmedLine, beforeCursor, afterCursor: line.substring(column - 1), }; }; // 从 schemas 中提取某个键的枚举值 const getEnumValuesFromSchemas = (keyName: string, path: string[] = []): YamlCompletionItem[] => { const completions: YamlCompletionItem[] = []; for (const schemaItem of schemas) { const enumValues = extractEnumValues(schemaItem.schema, keyName, path); completions.push(...enumValues); } return completions; }; // 递归提取 schema 中的枚举值 const extractEnumValues = (schema: any, targetKey: string, currentPath: string[] = []): YamlCompletionItem[] => { const completions: YamlCompletionItem[] = []; if (!schema || !schema.properties) { return completions; } // 如果路径为空,直接在当前层级查找 if (currentPath.length === 0) { const prop = schema.properties[targetKey]; if (prop && prop.enum) { return createEnumCompletions(prop); } return completions; } // 按路径导航到目标位置 let currentSchema = schema; for (const pathSegment of currentPath) { if (currentSchema.properties && currentSchema.properties[pathSegment]) { currentSchema = currentSchema.properties[pathSegment]; // 如果是数组类型,使用 items 的 schema if (currentSchema.type === 'array' && currentSchema.items) { currentSchema = currentSchema.items; } } else { // 路径不存在,返回空 return completions; } } // 在目标位置查找枚举值 if (currentSchema.properties && currentSchema.properties[targetKey]) { const prop = currentSchema.properties[targetKey]; if (prop.enum) { return createEnumCompletions(prop); } } return completions; }; // 创建枚举值补全项,支持 enumDescriptions 描述 const createEnumCompletions = (prop: any): YamlCompletionItem[] => { const completions: YamlCompletionItem[] = []; if (!prop.enum) { return completions; } prop.enum.forEach((enumValue: string, index: number) => { let description = prop.description || `Enum value: ${enumValue}`; // 如果有 enumDescriptions 数组,使用对应索引的描述 if (prop.enumDescriptions && prop.enumDescriptions[index]) { description = prop.enumDescriptions[index]; } completions.push({ label: enumValue, insertText: enumValue, detail: description, documentation: description, type: 'VALUE', }); }); return completions; }; // 从 schemas 中提取可用的键名 const getKeyCompletionsFromSchemas = (path: string[] = []): YamlCompletionItem[] => { const completions: YamlCompletionItem[] = []; const addedKeys = new Set(); // 避免重复 for (const schemaItem of schemas) { const keyCompletions = extractKeyCompletions(schemaItem.schema, path); for (const completion of keyCompletions) { if (!addedKeys.has(completion.label)) { addedKeys.add(completion.label); completions.push(completion); } } } return completions; }; // 递归提取 schema 中的键名 const extractKeyCompletions = (schema: any, targetPath: string[] = []): YamlCompletionItem[] => { const completions: YamlCompletionItem[] = []; let currentSchema = schema; // 导航到目标路径 for (const pathSegment of targetPath) { if (currentSchema.properties && currentSchema.properties[pathSegment]) { currentSchema = currentSchema.properties[pathSegment]; if (currentSchema.type === 'array' && currentSchema.items) { currentSchema = currentSchema.items; } } else { return completions; // 路径不存在 } } // 获取当前层级的属性 if (currentSchema.properties) { for (const [key, property] of Object.entries(currentSchema.properties)) { const prop = property as any; let insertText = `${key}: `; let shouldTriggerOnInsert = false; // 根据属性类型调整插入文本和是否触发补全 if (prop.type === 'object') { insertText = `${key}:\n `; shouldTriggerOnInsert = true; // 对象类型需要触发键补全 } else if (prop.type === 'array') { insertText = `${key}:\n - `; shouldTriggerOnInsert = true; // 数组类型需要触发补全 } else if (prop.enum && prop.enum.length > 0) { // 有枚举值的字段需要触发值补全 shouldTriggerOnInsert = true; } else if (prop.type === 'boolean') { // 布尔类型需要触发 true/false 补全 shouldTriggerOnInsert = true; } else if (prop.type === 'number') { // 数字类型需要触发数字补全 shouldTriggerOnInsert = true; } // 对于普通字符串字段,不触发补全 completions.push({ label: key, insertText, detail: prop.description || `Property: ${key}`, documentation: prop.description, type: 'KEY', triggerOnInsert: shouldTriggerOnInsert, }); } } return completions; }; const getFieldType = (keyName: string, path: string[] = []): string | null => { for (const schemaItem of schemas) { const fieldType = extractFieldType(schemaItem.schema, keyName, path); if (fieldType) { return fieldType; } } return null; }; const extractFieldType = (schema: any, targetKey: string, currentPath: string[] = []): string | null => { if (!schema || !schema.properties) { return null; } // 如果路径为空,直接在当前层级查找 if (currentPath.length === 0) { const prop = schema.properties[targetKey]; return prop ? prop.type : null; } // 按路径导航到目标位置 let currentSchema = schema; for (const pathSegment of currentPath) { if (currentSchema.properties && currentSchema.properties[pathSegment]) { currentSchema = currentSchema.properties[pathSegment]; // 如果是数组类型,使用 items 的 schema if (currentSchema.type === 'array' && currentSchema.items) { currentSchema = currentSchema.items; } } else { // 路径不存在,返回空 return null; } } // 在目标位置查找字段类型 if (currentSchema.properties && currentSchema.properties[targetKey]) { const prop = currentSchema.properties[targetKey]; return prop.type; } return null; }; const getCompletions = (model: monacoTypes.editor.ITextModel, position: monacoTypes.Position): YamlCompletionItem[] => { const wordInfo = model.getWordAtPosition(position); const word = wordInfo ? wordInfo.word : ''; // 使用新的位置分析 const positionInfo = analyzePosition(model, position); const currentPath = getCurrentPath(model, position); const completions: YamlCompletionItem[] = []; // 如果已经有完整的值,不提供任何补全 if (positionInfo.isAtEndOfCompletedValue) { return []; } // 如果在值位置,提供值的补全 if (positionInfo.isInValuePosition && positionInfo.currentKey) { // 查找枚举值补全 const enumCompletions = getEnumValuesFromSchemas(positionInfo.currentKey, currentPath); if (enumCompletions.length > 0) { // 如果有枚举值,只返回枚举值 completions.push(...enumCompletions); } else { // 如果没有枚举值,检查字段类型 const fieldType = getFieldType(positionInfo.currentKey, currentPath); if (fieldType === 'boolean') { // 只为布尔类型提供 true/false completions.push( { label: 'true', insertText: 'true', detail: 'Boolean true value', type: 'BOOLEAN', }, { label: 'false', insertText: 'false', detail: 'Boolean false value', type: 'BOOLEAN', }, ); } else if (fieldType === 'number') { // 为数字类型提供基本示例 completions.push({ label: '0', insertText: '0', detail: 'Number value', type: 'NUMBER', }); } // 对于 string 类型或其他类型,不提供任何补全 } } // 如果在键位置且不在值位置,提供键的补全 else if (positionInfo.isInKeyPosition && !positionInfo.isInValuePosition) { // 从 schemas 中获取键补全 const schemaKeyCompletions = getKeyCompletionsFromSchemas(currentPath); completions.push(...schemaKeyCompletions); } // 过滤匹配的项目 const filtered = completions.filter((item) => !word || item.label.toLowerCase().includes(word.toLowerCase())); return filtered; }; const getCurrentPath = (model: monacoTypes.editor.ITextModel, position: monacoTypes.Position): string[] => { const path: string[] = []; const lines = model.getLinesContent(); const currentLineNumber = position.lineNumber - 1; // Monaco uses 1-based line numbers if (currentLineNumber >= lines.length) { return path; } const currentLine = lines[currentLineNumber]; const currentIndent = getIndentLevel(currentLine); // 从当前行开始向上查找父级键 let targetIndent = currentIndent; for (let i = currentLineNumber; i >= 0; i--) { const line = lines[i]; const lineIndent = getIndentLevel(line); const trimmed = line.trim(); // 跳过空行和注释 if (!trimmed || trimmed.startsWith('#')) { continue; } // 如果缩进小于目标缩进 if (lineIndent < targetIndent) { // 检查是否是数组项(以 - 开头) if (trimmed.startsWith('-')) { // 数组项不添加到路径中,只更新缩进继续查找 targetIndent = lineIndent; continue; } // 如果包含冒号,这是一个父级键 if (trimmed.includes(':')) { const key = trimmed.split(':')[0].trim(); if (key) { path.unshift(key); targetIndent = lineIndent; // 更新目标缩进,继续向上查找 } } } } return path; }; const getIndentLevel = (line: string): number => { return line.length - line.trimLeft().length; }; const provideCompletionItems = ( model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, ): monacoTypes.languages.ProviderResult => { const word = model.getWordAtPosition(position); const range = word != null ? monaco.Range.lift({ startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }) : monaco.Range.fromPositions(position); const items = getCompletions(model, position); const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({ kind: getMonacoCompletionItemKind(item.type, monaco), label: item.label, insertText: item.insertText, detail: item.detail, documentation: item.documentation, sortText: index.toString().padStart(3, '0'), range, command: item.triggerOnInsert ? { id: 'editor.action.triggerSuggest', title: '', } : undefined, })); return { suggestions }; }; return { triggerCharacters: [':', ' ', '-', '\n', '\t'], provideCompletionItems, }; }