Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev 2025.1.2 #205

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
<template>
<div class="p-32 space-y-4">
<ShadcnCodeEditor :auto-complete-config="{
endpoint: 'http://jsonplaceholder.typicode.com/posts',
endpoint: [
{
url: 'http://jsonplaceholder.typicode.com/posts',
transform: (data: any) => data.map(item => ({
label: item.title,
insertText: item.body,
detail: item.title
}))
},
{
url: 'https://jsonplaceholder.typicode.com/photos',
transform: (data: any) => data.map(item => ({
label: item.id,
insertText: item.url,
detail: item.id
}))
}
],
method: 'GET',
trigger: ['.', '@'],
transform: (data: any) => {
return data.map((item: any) => ({
label: item.title,
insertText: item.body,
detail: item.title
}))
},
transform: (allResults: any[]) => {
return allResults
}
// requestParams: (context) => ({
// word: context.word,
// line: context.position.lineNumber.toString()
Expand Down
150 changes: 95 additions & 55 deletions src/ui/code-editor/feature/auto-completion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as monaco from 'monaco-editor'
import { t } from '@/utils/locale'
import { CodeEditorAutoCompleteProps } from '../types.ts'
import { CodeEditorAutoCompleteEndpoint, CodeEditorAutoCompleteProps } from '../types.ts'
import { createApp, h } from 'vue'
import { debounce } from 'lodash'

Expand All @@ -13,6 +13,11 @@ export function registerApiCompletion(editor: monaco.editor.IStandaloneCodeEdito
throw new Error(t('codeEditor.validated.endpoint'))
}

// 转换 endpoint 配置为统一格式
const endpoints: CodeEditorAutoCompleteEndpoint[] = Array.isArray(config.endpoint)
? config.endpoint.map(ep => typeof ep === 'string' ? { url: ep } : ep)
: [typeof config.endpoint === 'string' ? { url: config.endpoint } : config.endpoint]

if (!config.transform) {
throw new Error(t('codeEditor.validated.transform'))
}
Expand Down Expand Up @@ -133,28 +138,98 @@ export function registerApiCompletion(editor: monaco.editor.IStandaloneCodeEdito
let currentWord = ''

// 创建防抖的请求和处理函数
const fetchEndpoint = async (
endpoint: CodeEditorAutoCompleteEndpoint,
context: any,
controller: AbortController
) => {
let url = endpoint.url

if (config.requestParams) {
const params = new URLSearchParams(config.requestParams(context))
url = `${ url }${ url.includes('?') ? '&' : '?' }${ params.toString() }`
}

const options: RequestInit = {
method: endpoint.method || config.method || 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers,
...endpoint.headers
},
signal: controller.signal
}

if (config.requestBody) {
options.body = JSON.stringify(config.requestBody(context))
}

// 检查缓存
const cacheKey = JSON.stringify({ url, body: options.body })
const cachedData = suggestionCache.get(url, cacheKey)
if (cachedData) {
return endpoint.transform ? endpoint.transform(cachedData) : cachedData
}

const response = await fetch(url, options)
const data = await response.json()
suggestionCache.set(url, cacheKey, data)
return endpoint.transform ? endpoint.transform(data) : data
}

const debouncedFetch = debounce(async (
url: string,
options: RequestInit,
context: any,
onSuccess: (data: any) => void,
onError: (error: any) => void
) => {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), config.timeout)

try {
// 检查缓存
const cachedData = suggestionCache.get(url, options.body)
if (cachedData) {
console.debug('读取缓冲数据')
onSuccess(cachedData)
return
}
let hasError = false
const results = await Promise.all(
endpoints.map(endpoint =>
fetchEndpoint(endpoint, context, controller)
.catch(error => {
hasError = true
console.error(`Error fetching from ${ endpoint.url }:`, error)

// 显示错误信息
loadingContainer.style.display = 'none'
if (error.name === 'AbortError') {
suggestionsList.innerHTML = `<li class="px-3 py-2 text-red-500 select-none">Request timeout after ${ config.timeout }ms</li>`
}
else {
suggestionsList.innerHTML = `<li class="px-3 py-2 text-red-500 select-none">${ error.message }</li>`
}
suggestionsList.style.display = 'block'

return []
})
)
)

clearTimeout(timeoutId)

const response = await fetch(url, options)
const data = await response.json()
// 存入缓存
suggestionCache.set(url, options.body, data)
onSuccess(data)
// 只有在没有错误的情况下才处理结果
if (!hasError) {
const flattenedResults = results.flat()
const transformedResults = (config as any).transform(flattenedResults)
onSuccess(transformedResults)
}
}
catch (error) {
catch (error: any) {
clearTimeout(timeoutId)

loadingContainer.style.display = 'none'
if (error.name === 'AbortError') {
suggestionsList.innerHTML = `<li class="px-3 py-2 text-red-500 select-none">Request timeout after ${ config.timeout }ms</li>`
}
else {
suggestionsList.innerHTML = `<li class="px-3 py-2 text-red-500 select-none">${ error.message }</li>`
}
suggestionsList.style.display = 'block'

onError(error)
}
}, config.debounceTime || 500)
Expand Down Expand Up @@ -238,33 +313,11 @@ export function registerApiCompletion(editor: monaco.editor.IStandaloneCodeEdito
word: word.word
}

let url = config.endpoint
if (config.requestParams) {
const params = new URLSearchParams(config.requestParams(context))
url = `${ url }${ url.includes('?') ? '&' : '?' }${ params.toString() }`
}

const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), config.timeout)

const options: RequestInit = {
method: config.method || 'POST',
headers: { 'Content-Type': 'application/json', ...config.headers },
signal: controller.signal
}

if (config.requestBody) {
options.body = JSON.stringify(config.requestBody(context))
}

await new Promise((resolve, reject) => {
debouncedFetch(
url,
options,
(data) => {
clearTimeout(timeoutId)
const suggestions = config.transform ? config.transform(data) : data
const limitedSuggestions = suggestions?.slice(0, config.maxSuggestions)
context,
(suggestions) => {
const limitedSuggestions = suggestions.slice(0, config.maxSuggestions)

// 更新建议列表
currentTooltipCleanups.forEach(cleanup => cleanup())
Expand Down Expand Up @@ -305,22 +358,9 @@ export function registerApiCompletion(editor: monaco.editor.IStandaloneCodeEdito

loadingContainer.style.display = 'none'
suggestionsList.style.display = 'block'
resolve(data)
resolve(suggestions)
},
(error) => {
clearTimeout(timeoutId)
if (error.name === 'AbortError') {
console.error('Request timeout:', config.timeout + 'ms')
loadingContainer.style.display = 'none'
suggestionsList.innerHTML = `<li class="suggestion-item px-3 py-2 text-red-500 select-none">Request timeout after ${ config.timeout }ms</li>`
suggestionsList.style.display = 'block'
}
else {
loadingContainer.style.display = 'none'
suggestionsList.innerHTML = `<li class="suggestion-item px-3 py-2 text-red-500 select-none">${ error.message }</li>`
suggestionsList.style.display = 'block'
currentTooltipCleanups.forEach(cleanup => cleanup())
}
reject(error)
}
)
Expand Down
85 changes: 81 additions & 4 deletions src/ui/code-editor/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,98 @@
import * as monaco from 'monaco-editor'

/*
// 示例1:简单的多端点配置
const autoCompleteConfig: CodeEditorAutoCompleteProps = {
endpoint: [
'api/v1/metadata/mysql/databases',
'api/v1/metadata/mysql/tables'
],
transform: (data: any[]) => {
return data.map(item => ({
label: item.name,
kind: monaco.languages.CompletionItemKind.Value,
insertText: item.name,
detail: item.type,
documentation: item.description
}))
}
}
*/

/*
// 示例2:带有独立转换函数的多端点配置
const autoCompleteConfig: CodeEditorAutoCompleteProps = {
endpoint: [
{
url: 'api/v1/metadata/mysql/databases',
transform: (data: any) => data.map(db => ({
label: db.name,
kind: monaco.languages.CompletionItemKind.Folder,
insertText: db.name
}))
},
{
url: 'api/v1/metadata/mysql/tables',
headers: { 'X-Custom-Header': 'value' },
transform: (data: any) => data.map(table => ({
label: table.name,
kind: monaco.languages.CompletionItemKind.Value,
insertText: table.name
}))
}
],
transform: (allResults: any[]) => {
return allResults
}
}
*/

/*
// 示例3:混合使用字符串和配置对象
const autoCompleteConfig: CodeEditorAutoCompleteProps = {
endpoint: [
'api/v1/metadata/mysql/databases',
{
url: 'api/v1/metadata/mysql/tables',
method: 'GET',
headers: { 'X-Custom-Header': 'value' }
}
],
transform: (allResults: any[]) => {
return allResults.map(item => ({
label: item.name,
kind: monaco.languages.CompletionItemKind.Value,
insertText: item.name
}))
},
maxSuggestions: 50,
timeout: 10000
}
*/
export interface CodeEditorAutoCompleteParams
{
modelValue: string,
position: monaco.Position,
word: string
}

export interface CodeEditorAutoCompleteEndpoint
{
url: string
headers?: any
method?: string
transform?: (data: any) => monaco.languages.CompletionItem[]
}

export interface CodeEditorAutoCompleteProps
{
endpoint: string
endpoint: string | CodeEditorAutoCompleteEndpoint | (string | CodeEditorAutoCompleteEndpoint)[] // 支持单个或多个端点
trigger?: string[]
headers?: any
method?: string
transform?: (data: any) => monaco.languages.CompletionItem[]
requestBody?: (context: CodeEditorAutoCompleteParams) => any;
requestParams?: (context: CodeEditorAutoCompleteParams) => URLSearchParams;
transform?: (data: any | any[]) => monaco.languages.CompletionItem[]
requestBody?: (context: CodeEditorAutoCompleteParams) => any
requestParams?: (context: CodeEditorAutoCompleteParams) => URLSearchParams
maxSuggestions?: number
timeout?: number
debounceTime?: number // 访问服务的间隔时间(毫秒)
Expand Down
Loading