|
| 1 | +/** |
| 2 | + * Copyright (c) 2023 - present TinyEngine Authors. |
| 3 | + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license. |
| 6 | + * |
| 7 | + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, |
| 8 | + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR |
| 9 | + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. |
| 10 | + * |
| 11 | + */ |
| 12 | +import { ref } from 'vue' |
| 13 | +import { useCanvas, useResource } from '@opentiny/tiny-engine-meta-register' |
| 14 | + |
| 15 | +const keyWords = [ |
| 16 | + 'state', |
| 17 | + 'stores', |
| 18 | + 'props', |
| 19 | + 'emit', |
| 20 | + 'setState', |
| 21 | + 'route', |
| 22 | + 'i18n', |
| 23 | + 'getLocale', |
| 24 | + 'setLocale', |
| 25 | + 'history', |
| 26 | + 'utils', |
| 27 | + 'bridge', |
| 28 | + 'dataSourceMap' |
| 29 | +] |
| 30 | + |
| 31 | +const snippets = [ |
| 32 | + { |
| 33 | + lable: 'new function', |
| 34 | + type: 'Function', |
| 35 | + insertText: `function \${1:funName} (\${2}) { |
| 36 | + \${3} |
| 37 | +}`, |
| 38 | + detail: 'create new function' |
| 39 | + } |
| 40 | +] |
| 41 | + |
| 42 | +const TYPES = { |
| 43 | + KeyWord: 'KeyWord', |
| 44 | + Function: 'Function', |
| 45 | + Method: 'Method', |
| 46 | + Value: 'Value', |
| 47 | + Variable: 'Variable' |
| 48 | +} |
| 49 | + |
| 50 | +const getApiSuggestions = (monaco, range, wordContent) => |
| 51 | + keyWords |
| 52 | + .map((item) => ({ |
| 53 | + label: `this.${item}`, |
| 54 | + kind: monaco.languages.CompletionItemKind.Keyword, |
| 55 | + insertText: `this.${item}`, |
| 56 | + detail: `Lowcode API`, |
| 57 | + range |
| 58 | + })) |
| 59 | + .filter(({ insertText }) => insertText.indexOf(wordContent) === 0) |
| 60 | + |
| 61 | +const getSnippetsSuggestions = (monaco, range, wordContent) => |
| 62 | + snippets |
| 63 | + .map((item) => ({ |
| 64 | + label: item.lable, |
| 65 | + insertText: item.insertText, |
| 66 | + detail: item.detail, |
| 67 | + kind: monaco.languages.CompletionItemKind[item.type], |
| 68 | + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, |
| 69 | + range |
| 70 | + })) |
| 71 | + .filter(({ insertText }) => insertText.indexOf(wordContent) === 0) |
| 72 | + |
| 73 | +const getUserWords = () => { |
| 74 | + const { bridge = [], dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState |
| 75 | + |
| 76 | + return { |
| 77 | + state: { |
| 78 | + type: TYPES.Variable, |
| 79 | + getInsertText: (value) => `this.state.${value}`, |
| 80 | + data: Object.keys(useCanvas().getPageSchema().state || {}) |
| 81 | + }, |
| 82 | + stores: { |
| 83 | + type: TYPES.Variable, |
| 84 | + getInsertText: (value) => `this.stores.${value}`, |
| 85 | + data: globalState |
| 86 | + .filter((item) => item.id) |
| 87 | + .map((item) => [ |
| 88 | + item.id, |
| 89 | + ...[...Object.keys(item.state), ...Object.keys(item.getters)].map((name) => `${item.id}.${name}`) |
| 90 | + ]) |
| 91 | + .flat() |
| 92 | + }, |
| 93 | + storeFn: { |
| 94 | + type: TYPES.Method, |
| 95 | + getInsertText: (value) => `this.stores.${value}()`, |
| 96 | + data: globalState |
| 97 | + .filter((item) => item.id) |
| 98 | + .map((item) => Object.keys(item.actions).map((name) => `${item.id}.${name}`)) |
| 99 | + .flat() |
| 100 | + }, |
| 101 | + utils: { |
| 102 | + type: TYPES.Variable, |
| 103 | + getInsertText: (value) => `this.utils.${value}`, |
| 104 | + data: utils.map((item) => item.name) |
| 105 | + }, |
| 106 | + dataSource: { |
| 107 | + type: TYPES.Method, |
| 108 | + getInsertText: (value) => `this.dataSourceMap.${value}.load()`, |
| 109 | + data: dataSource.map((item) => item.name) |
| 110 | + }, |
| 111 | + bridge: { |
| 112 | + type: TYPES.Variable, |
| 113 | + getInsertText: (value) => `this.bridge.${value}`, |
| 114 | + data: bridge.map((item) => item.name) |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +const getUserSuggestions = (monaco, range, wordContent) => { |
| 120 | + const userWords = getUserWords() |
| 121 | + |
| 122 | + return Object.entries(userWords) |
| 123 | + .map(([_itemKey, itemContent]) => |
| 124 | + itemContent.data.map((item) => ({ |
| 125 | + kind: monaco.languages.CompletionItemKind[itemContent.type], |
| 126 | + label: itemContent.getInsertText(item), |
| 127 | + insertText: itemContent.getInsertText(item), |
| 128 | + detail: `Lowcode API`, |
| 129 | + range |
| 130 | + })) |
| 131 | + ) |
| 132 | + .flat() |
| 133 | + .filter(({ insertText }) => insertText.indexOf(wordContent) === 0) |
| 134 | +} |
| 135 | + |
| 136 | +const getCurrentChar = (model, position) => { |
| 137 | + const currentChar = model.getValueInRange({ |
| 138 | + startLineNumber: position.lineNumber, |
| 139 | + endLineNumber: position.lineNumber, |
| 140 | + startColumn: position.column - 1, |
| 141 | + endColumn: position.column |
| 142 | + }) |
| 143 | + |
| 144 | + return { word: currentChar, startColumn: position.column - 1, endColumn: position.column } |
| 145 | +} |
| 146 | + |
| 147 | +const getWords = (model, position) => { |
| 148 | + const words = [] |
| 149 | + |
| 150 | + const currentWord = model.getWordUntilPosition(position).word |
| 151 | + ? model.getWordAtPosition(position) |
| 152 | + : getCurrentChar(model, position) |
| 153 | + words.push(currentWord) |
| 154 | + |
| 155 | + const lastPosition = { ...position, column: currentWord.startColumn } |
| 156 | + while (lastPosition.column > 1) { |
| 157 | + const lastWord = model.getWordUntilPosition(lastPosition).word |
| 158 | + ? model.getWordUntilPosition(lastPosition) |
| 159 | + : getCurrentChar(model, lastPosition) |
| 160 | + if (!/[\w.]/.test(lastWord.word)) break |
| 161 | + words.push(lastWord) |
| 162 | + lastPosition.column = lastWord.startColumn |
| 163 | + } |
| 164 | + |
| 165 | + return words.reverse() |
| 166 | +} |
| 167 | + |
| 168 | +const getRange = (position, words) => ({ |
| 169 | + startLineNumber: position.lineNumber, |
| 170 | + endLineNumber: position.lineNumber, |
| 171 | + startColumn: words[0].startColumn, |
| 172 | + endColumn: words[words.length - 1].endColumn |
| 173 | +}) |
| 174 | + |
| 175 | +const generateBaseReference = () => { |
| 176 | + const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState |
| 177 | + const { state, methods } = useCanvas().getPageSchema() |
| 178 | + const currentSchema = useCanvas().getCurrentSchema() |
| 179 | + let referenceContext = `以下是一些通用的协议: |
| 180 | + \n常规属性如:{ width: '300px' } |
| 181 | + \n1.变量引用 |
| 182 | + \n{ width: { type: 'JSExpression', value: 'this.state.xxx' }} |
| 183 | + \n即当type为JSExpression,取其value并将value的值当做变量调用 |
| 184 | + \n2.方法引用 |
| 185 | + \n{ onClickNew: { type: 'JSFunction', value: 'function onClickNew() {}' }} |
| 186 | + \n即当type为JSFunction,取其value并将value的值函数调用 |
| 187 | + \n以下是一些依赖,调用均以this.开头:\n`; |
| 188 | + if (dataSource.length) { |
| 189 | + referenceContext += `数据源是定义的数据模型\nconst dataSource=${JSON.stringify(dataSource)}\n调用方式为: this.dataSource.xxx\n` |
| 190 | + } |
| 191 | + if (utils.length) { |
| 192 | + referenceContext += `工具类是通用的调用方法或npm依赖 |
| 193 | + \nconst utils=${JSON.stringify(utils)} |
| 194 | + \n调用方式为: this.utils.xxx |
| 195 | + \nutils有两种类型 |
| 196 | + \ntype为npm时,读取content内容,可构造如下引用,例如content中package(依赖包名)为@opentiny/vue,destructuring(解构)为true,exportName(导出组件名称)为Notify,实际引用方式是import { Notify } from '@opentiny/vue'; |
| 197 | + \ntype为function时,读取content内容,当content.type为JSFunction则将value视为JS方法并调用,其他可参考通用的协议\n` |
| 198 | + } |
| 199 | + if (globalState.length) { |
| 200 | + referenceContext += `全局变量是使用pinia创建的变量\nconst stores=${JSON.stringify(globalState)}\n调用方式为: this.stores.xxx\n` |
| 201 | + } |
| 202 | + if (Object.keys(state).length) { |
| 203 | + referenceContext += `js变量\nconst state=${JSON.stringify(state)}\n调用方式为: this.state.xxx\n` |
| 204 | + } |
| 205 | + if (Object.keys(methods).length) { |
| 206 | + referenceContext += `js方法\nconst methods=${JSON.stringify(methods)}\n调用方式为: this.xxx\n` |
| 207 | + } |
| 208 | + referenceContext += `以上依赖中没有的,则不能调用,如utils中没有axios,则axios不能使用\n` |
| 209 | + if (currentSchema) { |
| 210 | + referenceContext += `以下是当前选中的组件 |
| 211 | + \n${JSON.stringify(currentSchema)} |
| 212 | + \n请理解当前组件,componentName为组件名称,组件均为tinyVue组件,和基本html元素 |
| 213 | + \n该对象中的ref属性为vue组件的ref属性,如ref值为testForm,使用方式为this.$('testForm') |
| 214 | + \nprops表示组件的属性,是一个对象,对应vue组件的defineProps和defineEmits中的内容 |
| 215 | + \nprops中以on开头的表示其传递的是方法,如onClick,其值可以参考通用协议 |
| 216 | + \nprops中没有以on开头的则是普通属性,如onClick,其值中满足type为JSExpression和JSFunction的可以参考通用协议\n` |
| 217 | + } |
| 218 | + return referenceContext; |
| 219 | +} |
| 220 | + |
| 221 | +const fetchAiInlineCompletion = (codeBeforeCursor, codeAfterCursor) => { |
| 222 | + const referenceContext = generateBaseReference() |
| 223 | + return fetch('/designer/app-center/api/chat/completions', { |
| 224 | + method: 'POST', |
| 225 | + headers: { |
| 226 | + 'Content-Type': 'application/json', |
| 227 | + Authorization: 'Bearer sk-1234' |
| 228 | + }, |
| 229 | + body: JSON.stringify({ |
| 230 | + model: 'internvl3-14b', |
| 231 | + messages: [ |
| 232 | + { |
| 233 | + role: 'user', |
| 234 | + content: `你是一个JavaScript代码补全器,可以使用JS和ES的语法 |
| 235 | + \n${referenceContext} |
| 236 | + \n直接上下文如下: |
| 237 | + \n${codeBeforeCursor}<cursor>${codeAfterCursor} |
| 238 | + \n请从<cursor>(光标位置)处进行补全 |
| 239 | + \n返回代码不要包含上下文代码,不需要多余代码,注意如果是函数时,须以这种格式:function xxx() {} |
| 240 | + \n如果有多个示例,只选择其中一个,不需要返回思考过程和解释` |
| 241 | + } |
| 242 | + ], |
| 243 | + stream: false |
| 244 | + }) |
| 245 | + }) |
| 246 | +} |
| 247 | + |
| 248 | +const initInlineCompletion = (monacoInstance, editorModel) => { |
| 249 | + const controller = ref(new AbortController()) |
| 250 | + const signal = ref(controller.value.signal) |
| 251 | + const requestAllowed = ref(true) |
| 252 | + const timer = ref() |
| 253 | + const inlineCompletionProvider = { |
| 254 | + provideInlineCompletions(model, position, _context, _token) { |
| 255 | + if (editorModel && model.id !== editorModel.id) { |
| 256 | + return new Promise((resolve) => { |
| 257 | + resolve({items: []}) |
| 258 | + }) |
| 259 | + } |
| 260 | + |
| 261 | + if (timer.value) { |
| 262 | + clearTimeout(timer.value) |
| 263 | + } |
| 264 | + |
| 265 | + const words = getWords(model, position) |
| 266 | + const range = getRange(position, words) |
| 267 | + const wordContent = words.map((item) => item.word).join('') |
| 268 | + if (!wordContent || wordContent.lastIndexOf('}') === 0 || wordContent.length < 4) { |
| 269 | + return new Promise((resolve) => { |
| 270 | + resolve({items: []}) |
| 271 | + }) |
| 272 | + } |
| 273 | + if (!requestAllowed.value) { |
| 274 | + return new Promise((resolve) => { |
| 275 | + resolve({items: [{ |
| 276 | + insertText: '', |
| 277 | + range |
| 278 | + }]}) |
| 279 | + }) |
| 280 | + } |
| 281 | + return new Promise((resolve) => { |
| 282 | + // 延迟请求800ms |
| 283 | + timer.value = setTimeout(() => { |
| 284 | + // 节流操作,防止接口一直被请求 |
| 285 | + requestAllowed.value = false |
| 286 | + fetchAiInlineCompletion(wordContent) |
| 287 | + .then((response) => response.json()) |
| 288 | + .then((res) => { |
| 289 | + let insertText = res.choices[0].message.content |
| 290 | + .replace(/.*\n/, '') |
| 291 | + .replaceAll('```', '') |
| 292 | + .replace('javascript', '') |
| 293 | + .replace('typescript', '') |
| 294 | + .trim() |
| 295 | + const wordContentIndex = insertText.indexOf(wordContent) |
| 296 | + if (wordContentIndex === -1) { |
| 297 | + insertText = `${wordContent}${insertText}\n` |
| 298 | + } |
| 299 | + if (wordContentIndex > 0) { |
| 300 | + insertText = insertText.slice(wordContentIndex) |
| 301 | + } |
| 302 | + requestAllowed.value = true |
| 303 | + resolve({ |
| 304 | + items: [ |
| 305 | + { |
| 306 | + insertText, |
| 307 | + range |
| 308 | + } |
| 309 | + ] |
| 310 | + }) |
| 311 | + }) |
| 312 | + .catch(() => { |
| 313 | + requestAllowed.value = true |
| 314 | + }) |
| 315 | + }, 800) |
| 316 | + }) |
| 317 | + }, |
| 318 | + freeInlineCompletions() {} |
| 319 | + } |
| 320 | + return ['javascript', 'typescript'].map((lang) => |
| 321 | + monacoInstance.languages.registerInlineCompletionsProvider(lang, inlineCompletionProvider) |
| 322 | + ) |
| 323 | +} |
| 324 | + |
| 325 | +export const initCompletion = (monacoInstance, editorModel, conditionFn) => { |
| 326 | + const completionItemProvider = { |
| 327 | + provideCompletionItems(model, position, _context, _token) { |
| 328 | + if (editorModel && model.id !== editorModel.id) { |
| 329 | + return { |
| 330 | + suggestions: [] |
| 331 | + } |
| 332 | + } |
| 333 | + const words = getWords(model, position) |
| 334 | + const wordContent = words.map((item) => item.word).join('') |
| 335 | + const range = getRange(position, words) |
| 336 | + |
| 337 | + // 内置 API 提示 e.g. this.state/props/utils/... |
| 338 | + const apiSuggestions = getApiSuggestions(monacoInstance, range, wordContent) |
| 339 | + // 代码片段提示 e.g. create new function |
| 340 | + const snippetSuggestions = getSnippetsSuggestions(monacoInstance, range, wordContent) |
| 341 | + // 用户变量数据提示 e.g. this.dataSourceMap.xxx.load() |
| 342 | + const userSuggestions = getUserSuggestions(monacoInstance, range, wordContent) |
| 343 | + return { |
| 344 | + suggestions: [...apiSuggestions, ...snippetSuggestions, ...userSuggestions].filter((item) => |
| 345 | + conditionFn ? conditionFn(item) : true |
| 346 | + ) |
| 347 | + } |
| 348 | + }, |
| 349 | + triggerCharacters: ['.'] |
| 350 | + } |
| 351 | + |
| 352 | + return ['javascript', 'typescript'] |
| 353 | + .map((lang) => monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider)) |
| 354 | + .concat(initInlineCompletion(monacoInstance, editorModel)) |
| 355 | +} |
| 356 | + |
0 commit comments