Skip to content

Commit 7b38576

Browse files
feat: JS plugin add code completions(opentiny#1647)
1 parent 1109de0 commit 7b38576

File tree

4 files changed

+202
-7
lines changed

4 files changed

+202
-7
lines changed

packages/builtinComponent/src/components/BaseForm.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ const deleteApi = (evidence = { id: modelData.value?.id }) => {
185185
186186
const initFormData = () => {
187187
modelData.value = Object.fromEntries(
188-
(formModel.value.parameters || []).map((item) => {
188+
(formModel.value?.parameters || []).map((item) => {
189189
return [
190190
item.prop,
191191
item?.isModel
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
你是一个JavaScript代码补全器,可以使用JS和ES的语法
2+
3+
以下是一些通用的协议:
4+
常规属性如:{ width: '300px' }
5+
一. 变量引用
6+
{ width: { type: 'JSExpression', value: 'this.state.xxx' }
7+
即当type为JSExpression,取其value并将value的值当做变量调用
8+
二. 方法引用
9+
{ onClickNew: { type: 'JSFunction', value: 'function onClickNew() {}' }
10+
即当type为JSFunction,取其value并将value的值函数调用
11+
以下是一些依赖,调用均以this.开头:
12+
1. 数据源
13+
数据源是定义的数据模型
14+
const dataSource=$dataSource$
15+
调用方式为: this.dataSource.xxx
16+
2. 工具类
17+
工具类是通用的调用方法或npm依赖
18+
const utils=$utils$
19+
调用方式为: this.utils.xxx
20+
utils有两种类型
21+
type为npm时,读取content内容,可构造如下引用,例如content中package(依赖包名)为@opentiny/vue,destructuring(解构)为true,exportName(导出组件名称)为Notify,实际引用方式是import { Notify } from '@opentiny/vue';
22+
type为function时,读取content内容,当content.type为JSFunction则将value视为JS方法并调用,其他可参考通用的协议
23+
3. 全局变量
24+
全局变量是使用pinia创建的变量
25+
const stores=$globalState$
26+
调用方式为: this.stores.xxx
27+
4. JS变量
28+
js变量
29+
const state=$state$
30+
调用方式为: this.state.xxx
31+
5. JS方法
32+
js方法
33+
const methods=$methods$
34+
调用方式为: this.xxx
35+
36+
以上依赖中没有的,则不能调用,如utils中没有axios,则axios不能使用
37+
38+
以下是当前选中的组件
39+
$currentSchema$
40+
请理解当前组件,componentName为组件名称,组件包括tinyVue组件、ElementPlus组件,和基本html元素
41+
对象中的ref属性即vue组件的ref属性,如ref值为testForm,使用方式为this.$('testForm')
42+
props表示组件的属性,是一个对象,对应vue组件的defineProps和defineEmits中的内容
43+
props中以on开头的表示其传递的是方法,如onClick,其值可以参考通用协议
44+
props中没有以on开头的则是普通属性,如tinyInput组件中的placeholder
45+
props的属性中值为对象,且包含type和value属性,type为JSExpression和JSFunction时,value的值则参考通用协议取用
46+
47+
直接上下文如下:
48+
$codeBeforeCursor$<cursor>$codeAfterCursor$
49+
请从<cursor>(光标位置)后进行补全
50+
注意如果是函数时,须以function关键字开头,不使用箭头函数
51+
请只返回代码,且只返回一个示例,不需要思考过程和解释

packages/common/js/completion.js

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@
99
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
1010
*
1111
*/
12-
13-
import { useCanvas, useResource } from '@opentiny/tiny-engine-meta-register'
12+
import { ref } from 'vue'
13+
import {
14+
useCanvas,
15+
useResource,
16+
useRobot,
17+
getMergeMeta,
18+
getMetaApi,
19+
META_SERVICE
20+
} from '@opentiny/tiny-engine-meta-register'
21+
import completion from './completion-files/context.md?raw'
1422

1523
const keyWords = [
1624
'state',
@@ -172,6 +180,135 @@ const getRange = (position, words) => ({
172180
endColumn: words[words.length - 1].endColumn
173181
})
174182

183+
const generateBaseReference = () => {
184+
const { dataSource = [], utils = [], globalState = [] } = useResource().appSchemaState
185+
const { state, methods } = useCanvas().getPageSchema()
186+
const currentSchema = useCanvas().getCurrentSchema()
187+
let referenceContext = completion
188+
referenceContext = referenceContext.replace('$dataSource$', JSON.stringify(dataSource))
189+
referenceContext = referenceContext.replace('$utils$', JSON.stringify(utils))
190+
referenceContext = referenceContext.replace('$globalState$', JSON.stringify(globalState))
191+
referenceContext = referenceContext.replace('$state$', JSON.stringify(state))
192+
referenceContext = referenceContext.replace('$methods$', JSON.stringify(methods))
193+
referenceContext = referenceContext.replace('$currentSchema$', JSON.stringify(currentSchema))
194+
return referenceContext
195+
}
196+
197+
const fetchAiInlineCompletion = (codeBeforeCursor, codeAfterCursor) => {
198+
const { completeModel, apiKey, baseUrl } = useRobot().robotSettingState?.selectedModel || {}
199+
if (!completeModel || !apiKey || !baseUrl) {
200+
return
201+
}
202+
const referenceContext = generateBaseReference()
203+
return getMetaApi(META_SERVICE.Http).post(
204+
'/app-center/api/chat/completions',
205+
{
206+
model: completeModel,
207+
messages: [
208+
{
209+
role: 'user',
210+
content: referenceContext
211+
.replace('$codeBeforeCursor$', codeBeforeCursor)
212+
.replace('$codeAfterCursor$', codeAfterCursor)
213+
}
214+
],
215+
baseUrl,
216+
stream: false
217+
},
218+
{
219+
headers: {
220+
'Content-Type': 'application/json',
221+
Authorization: `Bearer ${apiKey}`
222+
}
223+
}
224+
)
225+
}
226+
227+
const initInlineCompletion = (monacoInstance, editorModel) => {
228+
const requestAllowed = ref(true)
229+
const timer = ref()
230+
const inlineCompletionProvider = {
231+
provideInlineCompletions(model, position, _context, _token) {
232+
if (editorModel && model.id !== editorModel.id) {
233+
return new Promise((resolve) => {
234+
resolve({ items: [] })
235+
})
236+
}
237+
238+
if (timer.value) {
239+
clearTimeout(timer.value)
240+
}
241+
242+
const words = getWords(model, position)
243+
const range = getRange(position, words)
244+
const wordContent = words.map((item) => item.word).join('')
245+
if (!wordContent || wordContent.lastIndexOf('}') === 0 || wordContent.length < 4) {
246+
return new Promise((resolve) => {
247+
resolve({ items: [] })
248+
})
249+
}
250+
if (!requestAllowed.value) {
251+
return new Promise((resolve) => {
252+
resolve({
253+
items: [
254+
{
255+
insertText: '',
256+
range
257+
}
258+
]
259+
})
260+
})
261+
}
262+
const codeBeforeCursor = model.getValueInRange({
263+
startLineNumber: 1,
264+
startColumn: 1,
265+
endLineNumber: position.lineNumber,
266+
endColumn: position.column
267+
})
268+
const codeAfterCursor = model.getValueInRange({
269+
startLineNumber: position.lineNumber,
270+
startColumn: position.column,
271+
endLineNumber: model.getLineCount(),
272+
endColumn: model.getLineMaxColumn(model.getLineCount())
273+
})
274+
return new Promise((resolve) => {
275+
// 延迟请求800ms
276+
timer.value = setTimeout(() => {
277+
// 节流操作,防止接口一直被请求
278+
requestAllowed.value = false
279+
fetchAiInlineCompletion(codeBeforeCursor, codeAfterCursor)
280+
.then((res) => {
281+
let insertText = res.choices[0].message.content.trim()
282+
const wordContentIndex = insertText.indexOf(wordContent)
283+
if (wordContentIndex === -1) {
284+
insertText = `${wordContent}${insertText}\n`
285+
}
286+
if (wordContentIndex > 0) {
287+
insertText = insertText.slice(wordContentIndex)
288+
}
289+
requestAllowed.value = true
290+
resolve({
291+
items: [
292+
{
293+
insertText,
294+
range
295+
}
296+
]
297+
})
298+
})
299+
.catch(() => {
300+
requestAllowed.value = true
301+
})
302+
}, 800)
303+
})
304+
},
305+
freeInlineCompletions() {}
306+
}
307+
return ['javascript', 'typescript'].map((lang) =>
308+
monacoInstance.languages.registerInlineCompletionsProvider(lang, inlineCompletionProvider)
309+
)
310+
}
311+
175312
export const initCompletion = (monacoInstance, editorModel, conditionFn) => {
176313
const completionItemProvider = {
177314
provideCompletionItems(model, position, _context, _token) {
@@ -198,8 +335,12 @@ export const initCompletion = (monacoInstance, editorModel, conditionFn) => {
198335
},
199336
triggerCharacters: ['.']
200337
}
201-
202-
return ['javascript', 'typescript'].map((lang) =>
203-
monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider)
204-
)
338+
const completions = ['javascript', 'typescript'].map((lang) => {
339+
return monacoInstance.languages.registerCompletionItemProvider(lang, completionItemProvider)
340+
})
341+
const { enableAICompletion } = getMergeMeta('engine.plugins.pagecontroller')?.options || {}
342+
if (enableAICompletion) {
343+
return completions.concat(initInlineCompletion(monacoInstance, editorModel))
344+
}
345+
return completions
205346
}

packages/plugins/script/meta.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ export default {
55
icon: 'plugin-icon-js',
66
width: 600,
77
widthResizable: true,
8+
options: {
9+
enableAICompletion: true
10+
},
811
confirm: 'close' // 当点击插件栏切换或关闭前是否需要确认, 会调用插件中confirm值指定的方法,e.g. 此处指向 close方法,会调用插件的close方法执行确认逻辑
912
}

0 commit comments

Comments
 (0)