diff --git a/packages/ai-code/src/module/completion-item/completion-item.ts b/packages/ai-code/src/module/completion-item/completion-item.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7f27bc70a5de1dbc1b1a84b88bbf8e06acb9cd9 --- /dev/null +++ b/packages/ai-code/src/module/completion-item/completion-item.ts @@ -0,0 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as IMonaco from 'monaco-editor'; +import { IConfig, Monaco } from '../interface'; +import { TypeScriptVariable } from './variable'; +/** + * 完成项补全 + * + * @export + * @class CompletionItem + */ +export class CompletionItem { + /** + * Creates an instance of CompletionItem. + * @param {Monaco} monaco + * @param {IMonaco.editor.IStandaloneCodeEditor} editor + * @param {IConfig} config + * @memberof CompletionItem + */ + constructor( + private monaco: Monaco, + private editor: IMonaco.editor.IStandaloneCodeEditor, + private config: IConfig, + ) { + this.registerByLanguage(); + } + + /** + * 根据语言类型注册 + * + * @private + * @return {*} {Promise} + * @memberof CompletionItem + */ + private async registerByLanguage(): Promise { + switch (this.config.language) { + case 'typescript': + this.registerTypeScript(); + break; + default: + break; + } + } + + /** + * 注册TypeScript完成项补全提示 + * - 通过额外库添加 + * @private + * @return {*} {Promise} + * @memberof CompletionItem + */ + private async registerTypeScript(): Promise { + let disposable: IMonaco.IDisposable | undefined; + + // 聚焦时注册当前全局变量 + this.editor.onDidFocusEditorText(() => { + disposable = + this.monaco.languages.typescript.typescriptDefaults.addExtraLib( + this.getTypeScriptVariable(), + `ibiz-editor.d.ts`, + ); + }); + + // 失去焦点移除当前全局变量 + this.editor.onDidBlurEditorText(() => { + disposable?.dispose(); + }); + const libName = 'ibiz.d.ts'; + const libs = + this.monaco.languages.typescript.typescriptDefaults.getExtraLibs(); + // 全局只注册一次 + if (libs[libName] || (window as any).isTypeScriptLibInitialized) return; + (window as any).isTypeScriptLibInitialized = true; + try { + const response = await fetch('/assets/test/index.d.ts'); + if (response.status === 200) { + const content = await response.text(); + if (content) { + this.monaco.languages.typescript.typescriptDefaults.addExtraLib( + `${content}`, + libName, + ); + } + } + } catch (error) { + ibiz.log.error(error); + } + } + + /** + * 获取TypeScript当前全局变量 + * + * @private + * @return {*} {string} + * @memberof CompletionItem + */ + private getTypeScriptVariable(): string { + const variables = Array.from(TypeScriptVariable.values()); + // 获取全局对象接口 + const interfaces: string[] = []; + variables.forEach(v => { + interfaces.push(...v.interface); + }); + // 计算全局变量所需接口 + const interfaceText = `import { ${Array.from(new Set(interfaces)).join( + ',', + )} } from './ibiz'`; + + // 计算当前全局变量 + const variableText = `declare global {${Array.from( + TypeScriptVariable.values(), + ) + .map(v => v.declare) + .join()}}`; + return `${interfaceText}\n${variableText}`; + } +} diff --git a/packages/ai-code/src/module/completion-item/variable/index.ts b/packages/ai-code/src/module/completion-item/variable/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a901dcc39393f518813c79982e02cb7854f7765 --- /dev/null +++ b/packages/ai-code/src/module/completion-item/variable/index.ts @@ -0,0 +1 @@ +export { TypeScriptVariable } from './type-script'; diff --git a/packages/ai-code/src/module/completion-item/variable/type-script.ts b/packages/ai-code/src/module/completion-item/variable/type-script.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb6cff5dde2f8c119c0cb39b2d7cecff26d832db --- /dev/null +++ b/packages/ai-code/src/module/completion-item/variable/type-script.ts @@ -0,0 +1,269 @@ +/** + * TypeScript 变量映射 + */ +export const TypeScriptVariable: Map< + string, + { declare: string; interface: string[] } +> = new Map([ + [ + 'ibiz', + { + declare: ` + /** + * ibiz全局对象 + * + * @type {(IIBizSys & IBizSys)} + */ + const ibiz: IIBizSys & IBizSys; + `, + interface: ['IIBizSys', ' IBizSys'], + }, + ], + [ + 'view', + { + declare: ` + /** + * 当前视图控制器 + * + * @type {IViewController} + */ + const view: IViewController; + `, + interface: ['IViewController'], + }, + ], + [ + 'ctrl', + { + declare: ` + /** + * 当前部件控制器 + * + * @type {IControlController} + */ + const ctrl: IControlController; + `, + interface: ['IControlController'], + }, + ], + [ + 'context', + { + declare: ` + /** + * 当前上下文 + * + * @type {IIBizContext} + */ + const context: IIBizContext; + `, + interface: ['IIBizContext'], + }, + ], + [ + 'params', + { + declare: ` + /** + * 当前视图参数 + * + * @type {IParams} + */ + const params: IParams; + `, + interface: ['IParams'], + }, + ], + [ + 'env', + { + declare: ` + /** + * ibiz环境变量 + * + * @type {IEnvironment} + */ + const env: IEnvironment; + `, + interface: ['IEnvironment'], + }, + ], + [ + 'app', + { + declare: ` + /** + * 应用控制器 + * + * @type {IHubController} + */ + const app: IHubController; + `, + interface: ['IHubController'], + }, + ], + [ + 'data', + { + declare: ` + /** + * 当前行为数据 + * + * @description 具体数据类型需根据当前逻辑自行判断 + * @type {(IData | IData[])} + */ + const data: IData | IData[]; + `, + interface: ['IData'], + }, + ], + [ + 'appSession', + { + declare: ` + /** + * 全局共享数据对象 + * + * @type {IData} + */ + const appSession: IData; + `, + interface: ['IData'], + }, + ], + [ + 'parent', + { + declare: ` + /** + * 父级视图控制器 + * + * @type {IViewController} + */ + const parent: IViewController; + `, + interface: ['IViewController'], + }, + ], + [ + 'topView', + { + declare: ` + /** + * 顶级视图控制器 + * + * @type {IViewController} + */ + const topView: IViewController; + `, + interface: ['IViewController'], + }, + ], + [ + 'parentView', + { + declare: ` + /** + * 父级视图控制器 + * + * @type {IViewController} + */ + const parentView: IViewController; + `, + interface: ['IViewController'], + }, + ], + [ + 'topViewSession', + { + declare: ` + /** + * 顶级视图共享数据对象 + * + * @type {IData} + */ + const topViewSession: IData; + `, + interface: ['IData'], + }, + ], + [ + 'viewSession', + { + declare: ` + /** + * 当前视图共享数据对象 + * + * @type {IData} + */ + const viewSession: IData; + `, + interface: ['IData'], + }, + ], + [ + 'uiLogic', + { + declare: ` + /** + * 界面逻辑参数对象 + * + * @description 属性为当前逻辑所有参数 + * @type {IData} + */ + const uiLogic: IData; + `, + interface: ['IData'], + }, + ], + [ + 'util', + { + declare: ` + /** + * 应用工具 + * + * @type {{ + * message: IMessageUtil; + * modal: IModalUtil; + * confirm: IConfirmUtil; + * openView: IOpenViewUtil; + * }} + */ + const util: { + /** + * 消息通知服务 + * + * @type {IMessageUtil} + */ + message: IMessageUtil; + /** + * 简洁信息提示服务 + * + * @type {IModalUtil} + */ + modal: IModalUtil; + /** + * 确认操作弹窗服务 + * + * @type {IConfirmUtil} + */ + confirm: IConfirmUtil; + /** + * 打开视图方式 + * + * @type {IOpenViewUtil} + */ + openView: IOpenViewUtil; + }; + `, + interface: [ + 'IMessageUtil', + 'IModalUtil', + 'IConfirmUtil', + 'IOpenViewUtil', + ], + }, + ], +]); diff --git a/packages/ai-code/src/module/custom-language-theme/custom-language-theme.ts b/packages/ai-code/src/module/custom-language-theme/custom-language-theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a3e1e9ee12b389f9ded6693e6e95f05d1cf5094 --- /dev/null +++ b/packages/ai-code/src/module/custom-language-theme/custom-language-theme.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { IConfig, Monaco } from '../interface'; +import { TypeScriptLanguage } from './language'; +/** + * 自定义语言主题 + * + * @export + * @class CustomTheme + */ +export class CustomLanguageTheme { + /** + * 主题名称 + * + * @type {string} + * @memberof CustomTheme + */ + themeName: string = ''; + + /** + * Creates an instance of CompletionItem. + * @param {Monaco} monaco + * @param {IMonaco.editor.IStandaloneCodeEditor} editor + * @param {IConfig} config + * @memberof CompletionItem + */ + constructor( + private monaco: Monaco, + private config: IConfig, + ) { + this.registerByLanguage(); + } + + /** + * 根据语言类型注册 + * + * @private + * @memberof CustomTheme + */ + private registerByLanguage(): void { + switch (this.config.language) { + case 'typescript': + this.registerTypeScriptTheme(); + break; + default: + break; + } + } + + /** + * 注册并应用TS自定义主题 + * + * @private + * @memberof CustomTheme + */ + private registerTypeScriptTheme(): void { + this.themeName = 'ts-dark-plus'; + this.monaco.editor.defineTheme(this.themeName, { + base: 'vs-dark', + inherit: true, + rules: [ + // 对象名(蓝色) + { token: 'support.type.object.ts', foreground: '#4FC1FF' }, + // 方法名(浅黄) + { token: 'support.function.method.ts', foreground: '#DCDCAA' }, + // 属性名(天蓝) + { token: 'support.property.ts', foreground: '#9CDCFE' }, + // 点号和括号 + { token: 'delimiter.method.ts', foreground: '#D4D4D4' }, + ], + colors: { + 'editor.foreground': '#D4D4D4', + 'editor.background': '#1E1E1E', + }, + }); + this.monaco.languages.setMonarchTokensProvider('typescript', { + // 保留默认规则 + ...TypeScriptLanguage, + tokenizer: { + // 保留默认规则 + ...(TypeScriptLanguage.tokenizer as any), + root: [ + // 1:匹配对象方法 obj.method() + [ + /([a-zA-Z_$][\w$]*)(\.)([a-zA-Z_$][\w$]*)(\()/, + [ + 'support.type.object.ts', // 对象名 + 'delimiter.method.ts', // 点号 + 'support.function.method.ts', // 方法名 + 'delimiter.method.ts', // 括号 + ], + ], + // 2:匹配对象属性 obj.property + [ + /([a-zA-Z_$][\w$]*)(\.)([a-zA-Z_$][\w$]*)/, + [ + 'support.type.object.ts', // 对象名 + 'delimiter.method.ts', // 点号 + 'support.property.ts', // 属性名 + ], + ], + // 保留默认规则 + ...(TypeScriptLanguage.tokenizer.root as any), + ], + }, + }); + } +} diff --git a/packages/ai-code/src/module/custom-language-theme/language/index.ts b/packages/ai-code/src/module/custom-language-theme/language/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..38dc4a27d9f0412ee632bb952d1f5386c03e467b --- /dev/null +++ b/packages/ai-code/src/module/custom-language-theme/language/index.ts @@ -0,0 +1 @@ +export { TypeScriptLanguage } from './typescript'; diff --git a/packages/ai-code/src/module/custom-language-theme/language/typescript.ts b/packages/ai-code/src/module/custom-language-theme/language/typescript.ts new file mode 100644 index 0000000000000000000000000000000000000000..4648d815349308deaafaad4d41ed616e38e30cdb --- /dev/null +++ b/packages/ai-code/src/module/custom-language-theme/language/typescript.ts @@ -0,0 +1,302 @@ +/* eslint-disable no-useless-escape */ +// 语言规则 +export const TypeScriptLanguage = { + // Set defaultToken to invalid to see what you do not tokenize yet + defaultToken: 'invalid', + tokenPostfix: '.ts', + + keywords: [ + // Should match the keys of textToKeywordObj in + // https://github.com/microsoft/TypeScript/blob/master/src/compiler/scanner.ts + 'abstract', + 'any', + 'as', + 'asserts', + 'bigint', + 'boolean', + 'break', + 'case', + 'catch', + 'class', + 'continue', + 'const', + 'constructor', + 'debugger', + 'declare', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'from', + 'function', + 'get', + 'if', + 'implements', + 'import', + 'in', + 'infer', + 'instanceof', + 'interface', + 'is', + 'keyof', + 'let', + 'module', + 'namespace', + 'never', + 'new', + 'null', + 'number', + 'object', + 'out', + 'package', + 'private', + 'protected', + 'public', + 'override', + 'readonly', + 'require', + 'global', + 'return', + 'satisfies', + 'set', + 'static', + 'string', + 'super', + 'switch', + 'symbol', + 'this', + 'throw', + 'true', + 'try', + 'type', + 'typeof', + 'undefined', + 'unique', + 'unknown', + 'var', + 'void', + 'while', + 'with', + 'yield', + 'async', + 'await', + 'of', + ], + + operators: [ + '<=', + '>=', + '==', + '!=', + '===', + '!==', + '=>', + '+', + '-', + '**', + '*', + '/', + '%', + '++', + '--', + '<<', + '>', + '>>>', + '&', + '|', + '^', + '!', + '~', + '&&', + '||', + '??', + '?', + ':', + '=', + '+=', + '-=', + '*=', + '**=', + '/=', + '%=', + '<<=', + '>>=', + '>>>=', + '&=', + '|=', + '^=', + '@', + ], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [/!(?=([^=]|$))/, 'delimiter'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '', + }, + }, + ], + + // numbers + [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'], + [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'], + [/0[xX](@hexdigits)n?/, 'number.hex'], + [/0[oO]?(@octaldigits)n?/, 'number.octal'], + [/0[bB](@binarydigits)n?/, 'number.binary'], + [/(@digits)n?/, 'number'], + + // delimiter: after number because of .\d floats + [/[;,.]/, 'delimiter'], + + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/"/, 'string', '@string_double'], + [/'/, 'string', '@string_single'], + [/`/, 'string', '@string_backtick'], + ], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], + + jsdoc: [ + [/[^\/*]+/, 'comment.doc'], + [/\*\//, 'comment.doc', '@pop'], + [/[\/*]/, 'comment.doc'], + ], + + // We match regular expression quite precisely + regexp: [ + [ + /(\{)(\d+(?:,\d*)?)(\})/, + [ + 'regexp.escape.control', + 'regexp.escape.control', + 'regexp.escape.control', + ], + ], + [ + /(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, + [ + 'regexp.escape.control', + { token: 'regexp.escape.control', next: '@regexrange' }, + ], + ], + [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']], + [/[()]/, 'regexp.escape.control'], + [/@regexpctl/, 'regexp.escape.control'], + [/[^\\\/]/, 'regexp'], + [/@regexpesc/, 'regexp.escape'], + [/\\\./, 'regexp.invalid'], + [ + /(\/)([dgimsuy]*)/, + [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other'], + ], + ], + + regexrange: [ + [/-/, 'regexp.escape.control'], + [/\^/, 'regexp.invalid'], + [/@regexpesc/, 'regexp.escape'], + [/[^\]]/, 'regexp'], + [ + /\]/, + { + token: 'regexp.escape.control', + next: '@pop', + bracket: '@close', + }, + ], + ], + + string_double: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + + string_single: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'/, 'string', '@pop'], + ], + + string_backtick: [ + [/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }], + [/[^\\`$]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/`/, 'string', '@pop'], + ], + + bracketCounting: [ + [/\{/, 'delimiter.bracket', '@bracketCounting'], + [/\}/, 'delimiter.bracket', '@pop'], + { include: 'common' }, + ], + }, +}; diff --git a/packages/ai-code/src/module/index.ts b/packages/ai-code/src/module/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa565125b3d3b7ccad0d457007aba32cd037ec1c --- /dev/null +++ b/packages/ai-code/src/module/index.ts @@ -0,0 +1,52 @@ +/* eslint-disable no-shadow */ +import * as IMonaco from 'monaco-editor'; +import { InlineCompletions } from './inline-completions/inline-completions'; +import { IConfig, Monaco } from './interface'; +import { CompletionItem } from './completion-item/completion-item'; +import { CustomLanguageTheme } from './custom-language-theme/custom-language-theme'; + +/** + * 代码编辑器模块中心 + * + * @export + * @class CodeModuleCenter + */ +export class CodeModuleCenter { + /** + * 模块 + * + * @private + * @type {IData[]} + * @memberof CodeModuleCenter + */ + private modules: IData[] = []; + + /** + * 主题 + * + * @type {CustomLanguageTheme} + * @memberof CodeModuleCenter + */ + theme: CustomLanguageTheme; + + constructor( + private monaco: Monaco, + private config: IConfig, + ) { + // 主题必须在代码编辑器实例创建完成之前创建 + this.theme = new CustomLanguageTheme(monaco, config); + } + + /** + * 初始化代码编辑器模块 + * + * @param {IMonaco.editor.IStandaloneCodeEditor} editor 代码编辑器实例 + * @memberof CodeModuleCenter + */ + initModules(editor: IMonaco.editor.IStandaloneCodeEditor): void { + // 添加内联代码补全模块 + this.modules.push(new InlineCompletions(this.monaco, editor, this.config)); + // 添加完成项补全模块 + this.modules.push(new CompletionItem(this.monaco, editor, this.config)); + } +} diff --git a/packages/ai-code/src/module/inline-completions/inline-completions.ts b/packages/ai-code/src/module/inline-completions/inline-completions.ts new file mode 100644 index 0000000000000000000000000000000000000000..083a58060dd9962b62698be6d69130ab16c3370e --- /dev/null +++ b/packages/ai-code/src/module/inline-completions/inline-completions.ts @@ -0,0 +1,327 @@ +import * as IMonaco from 'monaco-editor'; +import { createUUID } from 'qx-util'; +import { getToken } from '@ibiz-template/core'; +import { IConfig, Monaco } from '../interface'; + +interface ICompletionContext { + /** + * 单词 + * + * @type {string} + * @memberof ICompletionContext + */ + word: string; + /** + * 语言ID + * + * @type {string} + * @memberof ICompletionContext + */ + languageId: string; + /** + * 时间戳 + * + * @type {number} + * @memberof ICompletionContext + */ + timestamp: number; + /** + * 位置 + * + * @type {IMonaco.Position} + * @memberof ICompletionContext + */ + position: IMonaco.Position; + /** + * 当前行 + * + * @type {string} + * @memberof ICompletionContext + */ + currentLine: string; + /** + * 当前行上方数据 + * + * @type {string} + * @memberof ICompletionContext + */ + aboveLines: string; + /** + * 当前行下方数据 + * + * @type {string} + * @memberof ICompletionContext + */ + belowLines: string; +} + +/** + * 内联代码补全 + * + * @export + * @class InlineCompletions + */ +export class InlineCompletions { + /** + * 是否显示补全提示 + * + * @private + * @type {boolean} + * @memberof InlineCompletions + */ + private visible: boolean = false; + + /** + * 唯一标识 + * + * @private + * @type {string} + * @memberof InlineCompletions + */ + private UUID: string = createUUID(); + + /** + * 防抖计时器 + * + * @private + * @type {(number | null)} + * @memberof InlineCompletions + */ + private debounceTimer: number | null = null; + + /** + * 请求终止控制器 + * + * @private + * @type {(AbortController | null)} + * @memberof InlineCompletions + */ + private abortController: AbortController | null = null; + + /** + * Creates an instance of InlineCompletions. + * @param {Monaco} monaco + * @param {IConfig} config + * @param {monaco.editor.IStandaloneCodeEditor} editor + * @memberof InlineCompletions + */ + constructor( + private monaco: Monaco, + private editor: IMonaco.editor.IStandaloneCodeEditor, + private config: IConfig, + ) { + this.registerInlineCompletions(); + } + + /** + * 注册内联代码补全 + * + * @private + * @memberof InlineCompletions + */ + private registerInlineCompletions(): void { + (this.editor as IData).__inlineCompletionsId = this.UUID; + const disposable = this.monaco.languages.registerInlineCompletionsProvider( + this.config.language, + { + provideInlineCompletions: async (model, position) => { + this.visible = false; + const lineContent = model.getLineContent(position.lineNumber); + // 排除空白行 + if (!this.validate(model) || !lineContent.trim()) + return { items: [] }; + + // 清除之前的计时器 + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + + // 使用防抖 + return new Promise(resolve => { + this.debounceTimer = window.setTimeout(async () => { + const completion = await this.getCompletion(model, position); + if (!completion) { + resolve({ items: [] }); + return; + } + this.visible = true; + resolve({ + items: [ + { + insertText: completion, + range: new this.monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column, + ), + }, + ], + }); + }, 500); + }); + }, + freeInlineCompletions(_completion) {}, + }, + ); + // Tab键处理 + this.editor.addCommand(this.monaco.KeyCode.Tab, async () => { + // 如果当前显示行内代码补全则自动补全 + if (this.visible) { + this.editor.trigger(null, 'editor.action.inlineSuggest.commit', {}); + this.visible = false; + } else { + // 否则执行默认tab + this.editor.trigger(null, 'tab', {}); + } + }); + // 编辑器销毁时清理资源 + this.editor.onDidDispose(() => { + disposable.dispose(); + }); + } + + /** + * 检查模型是否属于当前编辑器实例 + * + * @private + * @param {IMonaco.editor.ITextModel} model + * @return {*} {boolean} + * @memberof InlineCompletions + */ + private validate(model: IMonaco.editor.ITextModel): boolean { + const currentEditor = this.monaco.editor + .getEditors() + .find(e => e.getModel() === model); + + if ( + !currentEditor || + (currentEditor as IData).__inlineCompletionsId !== this.UUID + ) + return false; + return true; + } + + /** + * 获取指定位置前后行数据 + * + * @private + * @param {('Above' | 'Below')} type 获取数据类型(上方 | 下方) + * @param {IMonaco.editor.ITextModel} model 编辑器模型 + * @param {IMonaco.Position} position 光标位置 + * @param {number} count 行数量 + * @return {*} {string} + * @memberof InlineCompletions + */ + private getLinesValue( + type: 'Above' | 'Below', + model: IMonaco.editor.ITextModel, + position: IMonaco.Position, + count: number, + ): string { + const totalLines = model.getLineCount(); + const startLine = + type === 'Above' + ? Math.max(1, position.lineNumber - count) + : Math.min(totalLines, position.lineNumber + 1); + const endLine = + type === 'Above' + ? Math.max(1, position.lineNumber - 1) + : Math.min(totalLines, position.lineNumber + count); + // 没有前一行或者没有后一行时返回空 + if ( + type === 'Above' + ? position.lineNumber === 1 + : position.lineNumber === totalLines + ) + return ''; + return model.getValueInRange({ + startLineNumber: startLine, + startColumn: 1, + endLineNumber: endLine, + endColumn: model.getLineMaxColumn(endLine), + }); + } + + /** + * 计算上下文 + * + * @param {IMonaco.editor.ITextModel} model + * @param {IMonaco.Position} position + * @return {*} {(ICompletionContext | undefined)} + * @memberof InlineCompletions + */ + private calcContext( + model: IMonaco.editor.ITextModel, + position: IMonaco.Position, + ): ICompletionContext | undefined { + const currentLine = model.getLineContent(position.lineNumber); + const token = + model.getWordAtPosition(position) || model.getWordUntilPosition(position); + return { + position, + currentLine, + word: token.word, + timestamp: Date.now(), + aboveLines: this.getLinesValue('Above', model, position, 5), + belowLines: this.getLinesValue('Below', model, position, 3), + languageId: model.getLanguageId(), + }; + } + + /** + * 获取请求头 + * + * @return {*} {HeadersInit} + * @memberof InlineCompletions + */ + getRequestHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + srfsystemid: ibiz.env.dcSystem, + }; + const token = getToken(); + if (token) { + headers[`${ibiz.env.tokenHeader}Authorization`] = `${ + ibiz.env.tokenPrefix + }Bearer ${getToken()}`; + } + const { orgData } = ibiz; + if (orgData?.systemid) headers.srfsystemid = orgData.systemid; + if (orgData?.orgid) headers.srforgid = orgData.orgid; + return headers; + } + + /** + * 获取补全 + * + * @private + * @param {IMonaco.editor.ITextModel} model + * @param {IMonaco.Position} position + * @return {*} {Promise} + * @memberof InlineCompletions + */ + private async getCompletion( + _model: IMonaco.editor.ITextModel, + _position: IMonaco.Position, + ): Promise { + // const context = this.calcContext(model, position); + return undefined; + // if (this.abortController) this.abortController.abort(); + // this.abortController = new AbortController(); + // try { + // const response = await fetch('', { + // method: 'POST', + // body: JSON.stringify(context), + // headers: this.getRequestHeaders(), + // signal: this.abortController.signal, + // }); + // this.abortController = null; + // if (response.status !== 200) return; + // const completion = await response.text(); + // return completion; + // } catch (err: any) { + // if (err.name !== 'AbortError') ibiz.log.error(err); + // } + } +} diff --git a/packages/ai-code/src/module/interface.ts b/packages/ai-code/src/module/interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..e661d5f3037bd324bd7fa1195fe42eed69bdccd3 --- /dev/null +++ b/packages/ai-code/src/module/interface.ts @@ -0,0 +1,47 @@ +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; + +export type Monaco = typeof monacoEditor; + +/** + * 配置 + * + * @export + * @interface IConfig + */ +export interface IConfig { + /** + * 编辑器标识 + * + * @type {string} + * @memberof IConfig + */ + id: string; + /** + * 数据 + * + * @type {IData} + * @memberof IConfig + */ + data: IData; + /** + * 上下文 + * + * @type {IContext} + * @memberof IConfig + */ + context: IContext; + /** + * 视图 + * + * @type {IParams} + * @memberof IConfig + */ + params: IParams; + /** + * 语言 + * + * @type {string} + * @memberof IConfig + */ + language: string; +} diff --git a/packages/ai-code/src/monaco-editor/monaco-editor.tsx b/packages/ai-code/src/monaco-editor/monaco-editor.tsx index 9d5f693bcbbe5914d59f7d073a6661daaabe512d..114f1adfa8e25b09be39149055c94d3d292ac6a4 100644 --- a/packages/ai-code/src/monaco-editor/monaco-editor.tsx +++ b/packages/ai-code/src/monaco-editor/monaco-editor.tsx @@ -32,6 +32,7 @@ import { } from '@ibiz-template/core'; import { CodeEditorController } from '../code-editor.controller'; import { calcAiToolbarItemsByAc } from '../util'; +import { CodeModuleCenter } from '../module'; import './monaco-editor.scss'; export const IBizAICode = defineComponent({ @@ -80,6 +81,7 @@ export const IBizAICode = defineComponent({ } } + let codeModuleCenter: CodeModuleCenter | null; let editor: monaco.editor.IStandaloneCodeEditor | null; let monacoEditor: typeof monaco.editor; let codeLensProviderDisposable: monaco.IDisposable | null; @@ -472,10 +474,17 @@ export const IBizAICode = defineComponent({ isLoading.value = false; // 初始化编辑器 if (!editor) { + codeModuleCenter = new CodeModuleCenter(loaderMonaco, { + id: c.model.id!, + data: props.data, + context: c.context, + params: c.params, + language: props.language || props.controller.language, + }); monacoEditor = loaderMonaco.editor; editor = monacoEditor.create(codeEditBox.value, { language: props.language || props.controller.language, // 语言支持自行查阅demo - theme: getMonacoTheme(UIStore.theme), + theme: codeModuleCenter.theme.themeName, foldingStrategy: 'indentation', renderLineHighlight: 'all', // 行亮 selectOnLineNumbers: true, // 显示行号 @@ -489,11 +498,13 @@ export const IBizAICode = defineComponent({ value: ibiz.i18n.t('editor.code.readOnlyPrompt'), }, fontSize: 16, // 字体大小 + fixedOverflowWidgets: true, // 确保悬浮提示不会被裁剪 scrollBeyondLastLine: false, // 取消代码后面一大段空白 overviewRulerBorder: false, // 不要滚动条的边框 }); // 为当前编辑器实例添加自定义属性 (editor as any).__instanceId = UUID; + codeModuleCenter.initModules(editor); if (c.deACMode) { codeLensProviderDisposable = loaderMonaco.languages.registerCodeLensProvider(