feat(gpt-runner-vscode): add completion support
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,3 +32,5 @@ docs/changelog
|
||||
docs/_dogfooding/_swizzle_theme_tests
|
||||
|
||||
docs/i18n/**/*
|
||||
|
||||
.gpt-runner
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -10,6 +10,13 @@
|
||||
// "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"typescript.inlayHints.parameterTypes.enabled": true,
|
||||
// "typescript.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"[markdown]": {
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": false,
|
||||
"strings": true
|
||||
},
|
||||
},
|
||||
"cSpell.words": [
|
||||
"activeid",
|
||||
"cachedir",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { FileInfoTreeItem } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { travelTreeDeepFirst } from '@nicepkg/gpt-runner-shared/common'
|
||||
import type { Ignore } from 'ignore'
|
||||
import ignore from 'ignore'
|
||||
import type { TravelFilesByFilterPatternParams } from '@nicepkg/gpt-runner-shared/node'
|
||||
import { FileUtils, PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import { countFileTokens } from './count-tokens'
|
||||
import { getIgnoreFunction } from './gitignore'
|
||||
|
||||
interface CreateFileTreeParams {
|
||||
rootPath: string
|
||||
@@ -111,13 +110,7 @@ export type GetCommonFileTreeReturns = CreateFileTreeReturns
|
||||
export async function getCommonFileTree(params: GetCommonFileTreeParams): Promise<GetCommonFileTreeReturns> {
|
||||
const { rootPath, respectGitIgnore = true, isValidPath, ...othersParams } = params
|
||||
|
||||
const ig: Ignore | null = await (async () => {
|
||||
const gitignorePath = PathUtils.join(rootPath, '.gitignore')
|
||||
const gitignoreContent = await FileUtils.readFile({ filePath: gitignorePath })
|
||||
const ig = ignore().add(gitignoreContent)
|
||||
|
||||
return ig
|
||||
})()
|
||||
const isGitIgnore = await getIgnoreFunction({ rootPath })
|
||||
|
||||
const isGitignorePaths = (filePath: string): boolean => {
|
||||
if (!respectGitIgnore)
|
||||
@@ -126,8 +119,7 @@ export async function getCommonFileTree(params: GetCommonFileTreeParams): Promis
|
||||
if (filePath && filePath.match(/\/\.git\//))
|
||||
return true
|
||||
|
||||
const relativePath = PathUtils.relative(rootPath, filePath)
|
||||
return ig?.ignores(relativePath) ?? false
|
||||
return isGitIgnore(filePath)
|
||||
}
|
||||
|
||||
const filePaths: string[] = []
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { GptFileInfo, GptFileInfoTree, GptFileInfoTreeItem, UserConfig } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { GptFileTreeItemType, userConfigWithDefault } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { FileUtils, PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import type { Ignore } from 'ignore'
|
||||
import ignore from 'ignore'
|
||||
import { parseGptFile } from './parser'
|
||||
import { getIgnoreFunction } from './gitignore'
|
||||
|
||||
export interface GetGptFilesInfoParams {
|
||||
userConfig: UserConfig
|
||||
@@ -27,13 +26,7 @@ export async function getGptFilesInfo(params: GetGptFilesInfoParams): Promise<Ge
|
||||
respectGitIgnore = true,
|
||||
} = resolvedUserConfig
|
||||
|
||||
const ig: Ignore | null = await (async () => {
|
||||
const gitignorePath = PathUtils.join(rootPath, '.gitignore')
|
||||
const gitignoreContent = await FileUtils.readFile({ filePath: gitignorePath })
|
||||
const ig = ignore().add(gitignoreContent)
|
||||
|
||||
return ig
|
||||
})()
|
||||
const isGitIgnore = await getIgnoreFunction({ rootPath })
|
||||
|
||||
const isGitignorePaths = (filePath: string): boolean => {
|
||||
if (!respectGitIgnore)
|
||||
@@ -42,9 +35,7 @@ export async function getGptFilesInfo(params: GetGptFilesInfoParams): Promise<Ge
|
||||
if (filePath && filePath.match(/\/\.git\//))
|
||||
return true
|
||||
|
||||
const relativePath = PathUtils.relative(rootPath, filePath)
|
||||
|
||||
return ig?.ignores(relativePath) ?? false
|
||||
return isGitIgnore(filePath)
|
||||
}
|
||||
|
||||
const fullRootPath = PathUtils.resolve(rootPath)
|
||||
|
||||
24
packages/gpt-runner-core/src/core/gitignore.ts
Normal file
24
packages/gpt-runner-core/src/core/gitignore.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { GPT_RUNNER_OFFICIAL_FOLDER } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { FileUtils, PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import ignore from 'ignore'
|
||||
|
||||
export interface GetIgnoreInstanceParams {
|
||||
rootPath: string
|
||||
}
|
||||
|
||||
export async function getIgnoreFunction(params: GetIgnoreInstanceParams) {
|
||||
const { rootPath } = params
|
||||
|
||||
const gitignorePath = PathUtils.join(rootPath, '.gitignore')
|
||||
const gitignoreContent = await FileUtils.readFile({ filePath: gitignorePath })
|
||||
const ig = ignore().add(gitignoreContent)
|
||||
|
||||
return (filePath: string): boolean => {
|
||||
const relativePath = PathUtils.relative(rootPath, filePath)
|
||||
|
||||
if (relativePath.includes(GPT_RUNNER_OFFICIAL_FOLDER))
|
||||
return false
|
||||
|
||||
return ig?.ignores(relativePath) ?? false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileUtils, PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import { DEFAULT_INIT_FOLDER } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { GPT_RUNNER_OFFICIAL_FOLDER } from '@nicepkg/gpt-runner-shared'
|
||||
import { copilotMdFile } from './copilot.gpt'
|
||||
|
||||
export const gptFilesForInit = {
|
||||
@@ -14,11 +14,11 @@ export interface InitGptFilesParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* write some .gpt.md files to the <rootPath>/gpt-presets folder
|
||||
* write some .gpt.md files to the <rootPath>/.gpt-runner folder
|
||||
*/
|
||||
export async function initGptFiles(params: InitGptFilesParams) {
|
||||
const { rootPath, gptFilesNames } = params
|
||||
const generateTargetFolder = PathUtils.join(rootPath, DEFAULT_INIT_FOLDER)
|
||||
const generateTargetFolder = PathUtils.join(rootPath, GPT_RUNNER_OFFICIAL_FOLDER)
|
||||
|
||||
for (const gptFileName of gptFilesNames) {
|
||||
const filePath = PathUtils.join(generateTargetFolder, `${gptFileName}.gpt.md`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const DEFAULT_INIT_FOLDER = 'gpt-presets'
|
||||
export const MIN_NODE_VERSION = '16.18.0'
|
||||
export const SECRET_KEY_PLACEHOLDER = '********'
|
||||
export const STREAM_DONE_FLAG = '[DONE]'
|
||||
export const GPT_RUNNER_OFFICIAL_FOLDER = '.gpt-runner'
|
||||
|
||||
export const DEFAULT_EXCLUDE_FILES = [
|
||||
'**/node_modules',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { PathUtils } from './path-utils'
|
||||
import { FileUtils } from './file-utils'
|
||||
|
||||
@@ -54,7 +53,7 @@ export class FileManager {
|
||||
if (await fs.stat(fullPath).then(stat => stat.isDirectory())) {
|
||||
const subDirContentMap = await FileManager.readDir({ directory: fullPath, exclude })
|
||||
Object.entries(subDirContentMap).forEach(([relativePath, content]) => {
|
||||
relativePathContentMap[path.join(file, relativePath)] = content
|
||||
relativePathContentMap[PathUtils.join(file, relativePath)] = content
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:vsix": "pnpm esno ./scripts/build.ts",
|
||||
"build": "pnpm esno ./scripts/build.ts",
|
||||
"build:vsix": "pnpm esno ./scripts/build.ts -- --vsix",
|
||||
"dev": "pnpm esno ./scripts/dev.ts",
|
||||
"publish": "esno ./scripts/publish.ts"
|
||||
},
|
||||
@@ -98,4 +98,4 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ const dirname = PathUtils.getCurrentDirName(import.meta.url, () => __dirname)
|
||||
const root = PathUtils.join(dirname, '..')
|
||||
const dist = PathUtils.join(root, 'dist')
|
||||
|
||||
async function buildVsix() {
|
||||
// is build vsix
|
||||
const isBuildVsix = process.argv.includes('--vsix')
|
||||
|
||||
async function build() {
|
||||
// remove <root>/dist
|
||||
await fs.remove(dist)
|
||||
|
||||
@@ -16,17 +19,26 @@ async function buildVsix() {
|
||||
pkg.name = 'gpt-runner'
|
||||
|
||||
await fs.writeJSON(pkgPath, pkg, { spaces: 2 })
|
||||
await execa('pnpm', ['run', 'build'], { cwd: root, stdio: 'inherit' })
|
||||
await execa('tsup', { cwd: root, stdio: 'inherit' })
|
||||
|
||||
try {
|
||||
// copy from <root>/node_modules/@nicepkg/gpt-runner-web/dist to <root>/dist/web
|
||||
const webDistPath = PathUtils.join(root, 'dist/web')
|
||||
|
||||
await fs.copy(
|
||||
PathUtils.join(root, 'node_modules/@nicepkg/gpt-runner-web/dist'),
|
||||
webDistPath,
|
||||
)
|
||||
|
||||
// copy from <root>/node_modules/@nicepkg/gpt-runner-shared/dist/json-schema to <root>/dist/json-schema
|
||||
const jsonSchemaDistPath = PathUtils.join(root, 'dist/json-schema')
|
||||
await fs.copy(
|
||||
PathUtils.join(root, 'node_modules/@nicepkg/gpt-runner-shared/dist/json-schema'),
|
||||
jsonSchemaDistPath,
|
||||
)
|
||||
|
||||
if (!isBuildVsix)
|
||||
return
|
||||
|
||||
console.log('\nBuild Vsix...\n')
|
||||
await execa('vsce', ['package', '-o', 'dist/gpt-runner.vsix', '--no-dependencies'], { cwd: root, stdio: 'inherit' })
|
||||
}
|
||||
@@ -35,4 +47,4 @@ async function buildVsix() {
|
||||
}
|
||||
}
|
||||
|
||||
buildVsix()
|
||||
build()
|
||||
|
||||
@@ -20,6 +20,16 @@ async function dev() {
|
||||
)
|
||||
}
|
||||
|
||||
// make symlink from <root>/node_modules/@nicepkg/gpt-runner-shared/dist/json-schema to <root>/dist/json-schema
|
||||
const jsonSchemaDistPath = PathUtils.join(root, 'dist/json-schema')
|
||||
const jsonSchemaDistPathExists = await fs.pathExists(jsonSchemaDistPath)
|
||||
if (!jsonSchemaDistPathExists) {
|
||||
await fs.ensureSymlink(
|
||||
PathUtils.join(root, 'node_modules/@nicepkg/gpt-runner-shared/dist/json-schema'),
|
||||
jsonSchemaDistPath,
|
||||
)
|
||||
}
|
||||
|
||||
await execa('tsup', ['--watch', 'src'], { cwd: root, stdio: 'inherit' })
|
||||
}
|
||||
|
||||
|
||||
@@ -11,3 +11,23 @@ export enum Commands {
|
||||
}
|
||||
|
||||
export const URI_SCHEME = 'gpt-runner'
|
||||
|
||||
export const GPT_MD_COMPLETION_ITEM_SNIPPET = `\`\`\`json
|
||||
{
|
||||
"title": "common",
|
||||
"model": {
|
||||
"type": "openai",
|
||||
"modelName": "gpt-3.5-turbo-16k",
|
||||
"temperature": 0.7
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
# System Prompt
|
||||
|
||||
input your system prompt here \${1}
|
||||
|
||||
# User Prompt
|
||||
|
||||
input your user prompt here
|
||||
`
|
||||
|
||||
@@ -11,6 +11,7 @@ import { registerInsertCodes } from './register/insert-codes'
|
||||
import { registerDiffCodes } from './register/diff-codes'
|
||||
import { registerOpenInBrowser } from './register/open-in-browser'
|
||||
import { registerStatusBar } from './register/status-bar'
|
||||
import { registerCompletion } from './register/completion'
|
||||
|
||||
async function registerRoot(ext: ExtensionContext, status: StatusBarItem, cwd: string) {
|
||||
const contextLoader = new ContextLoader(cwd)
|
||||
@@ -27,6 +28,7 @@ async function registerRoot(ext: ExtensionContext, status: StatusBarItem, cwd: s
|
||||
await registerStatusBar(cwd, contextLoader, ext)
|
||||
await registerInsertCodes(cwd, contextLoader, ext)
|
||||
await registerDiffCodes(cwd, contextLoader, ext)
|
||||
await registerCompletion(cwd, contextLoader, ext)
|
||||
|
||||
return contextLoader
|
||||
}
|
||||
|
||||
118
packages/gpt-runner-vscode/src/register/completion.ts
Normal file
118
packages/gpt-runner-vscode/src/register/completion.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import * as vscode from 'vscode'
|
||||
import type { ExtensionContext } from 'vscode'
|
||||
import { FileUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import type { ContextLoader } from '../contextLoader'
|
||||
import { GPT_MD_COMPLETION_ITEM_SNIPPET } from '../constant'
|
||||
|
||||
// Helper function to create completion items from JSON schema properties
|
||||
function createCompletionItemsFromSchema(
|
||||
schema: any,
|
||||
parentProperty: string | null = null,
|
||||
): vscode.CompletionItem[] {
|
||||
const completionItems: vscode.CompletionItem[] = []
|
||||
|
||||
if (schema.properties) {
|
||||
for (const propName in schema.properties) {
|
||||
const propertySchema = schema.properties[propName]
|
||||
const itemLabel = parentProperty ? `${parentProperty}.${propName}` : propName
|
||||
const completionItem = new vscode.CompletionItem(
|
||||
`"${itemLabel}"`,
|
||||
vscode.CompletionItemKind.Property,
|
||||
)
|
||||
|
||||
const valueTypeInsertWrapperMap: Record<string, string> = {
|
||||
string: '"${1}"',
|
||||
number: '${1}',
|
||||
boolean: '${1:true}',
|
||||
array: '[${1}]',
|
||||
object: '{${1}}',
|
||||
}
|
||||
|
||||
completionItem.insertText = new vscode.SnippetString(
|
||||
`${propName}": ${valueTypeInsertWrapperMap[propertySchema.type] || '${1}'}`,
|
||||
)
|
||||
|
||||
completionItem.documentation = new vscode.MarkdownString(
|
||||
propertySchema.description || '',
|
||||
)
|
||||
completionItems.push(completionItem)
|
||||
|
||||
if (propertySchema.type === 'object') {
|
||||
const childCompletionItems = createCompletionItemsFromSchema(
|
||||
propertySchema,
|
||||
itemLabel,
|
||||
)
|
||||
completionItems.push(...childCompletionItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return completionItems
|
||||
}
|
||||
|
||||
export async function registerCompletion(
|
||||
cwd: string,
|
||||
contextLoader: ContextLoader,
|
||||
ext: ExtensionContext,
|
||||
) {
|
||||
const disposables: vscode.Disposable[] = []
|
||||
|
||||
const dispose = () => {
|
||||
disposables.forEach(d => d.dispose())
|
||||
}
|
||||
|
||||
const registerProvider = () => {
|
||||
dispose()
|
||||
|
||||
console.log('aaa registerCompletion')
|
||||
const disposable = vscode.languages.registerCompletionItemProvider(
|
||||
{ scheme: 'file', pattern: '**/*.gpt.md' },
|
||||
{
|
||||
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const completions: vscode.CompletionItem[] = []
|
||||
const tips = 'Quick Init GPT File'
|
||||
|
||||
const snippetCompletion = new vscode.CompletionItem('gptr')
|
||||
snippetCompletion.insertText = new vscode.SnippetString(GPT_MD_COMPLETION_ITEM_SNIPPET)
|
||||
snippetCompletion.documentation = new vscode.MarkdownString(tips)
|
||||
completions.push(snippetCompletion)
|
||||
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
||||
if (position.line === 0 && linePrefix.includes('```j')) {
|
||||
const completionItem = new vscode.CompletionItem('json', vscode.CompletionItemKind.Snippet)
|
||||
completionItem.insertText = new vscode.SnippetString(GPT_MD_COMPLETION_ITEM_SNIPPET.replace(/^\`\`\`/, ''))
|
||||
completionItem.documentation = new vscode.MarkdownString(tips)
|
||||
completions.push(completionItem)
|
||||
}
|
||||
|
||||
// json schema
|
||||
const { extensionUri } = ext
|
||||
const jsonSchemaPath = vscode.Uri.joinPath(extensionUri, './dist/json-schema/single-file-config.json').fsPath
|
||||
const jsonSchemaContent = await FileUtils.readFile({ filePath: jsonSchemaPath })
|
||||
const jsonSchema = JSON.parse(jsonSchemaContent)
|
||||
|
||||
// add suggestions for properties by json schema
|
||||
const schemaCompletionItems = createCompletionItemsFromSchema(jsonSchema)
|
||||
completions.push(...schemaCompletionItems)
|
||||
|
||||
return completions
|
||||
},
|
||||
})
|
||||
|
||||
disposables.push(disposable)
|
||||
|
||||
return disposable
|
||||
}
|
||||
|
||||
ext.subscriptions.push(
|
||||
registerProvider(),
|
||||
)
|
||||
|
||||
contextLoader.emitter.on('contextReload', () => {
|
||||
registerProvider()
|
||||
})
|
||||
contextLoader.emitter.on('contextUnload', () => {
|
||||
dispose()
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import type { ExtensionContext } from 'vscode'
|
||||
import * as vscode from 'vscode'
|
||||
import * as uuid from 'uuid'
|
||||
import { toUnixPath } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import type { ContextLoader } from '../contextLoader'
|
||||
import { Commands, EXT_DISPLAY_NAME, EXT_NAME } from '../constant'
|
||||
import { createHash, getServerBaseUrl } from '../utils'
|
||||
@@ -78,7 +78,7 @@ class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
localResourceRoots: [baseUri],
|
||||
}
|
||||
|
||||
const indexHtml = fs.readFileSync(path.join(baseUri.fsPath, 'index.html'), 'utf8')
|
||||
const indexHtml = fs.readFileSync(PathUtils.join(baseUri.fsPath, 'index.html'), 'utf8')
|
||||
const nonce = createHash()
|
||||
|
||||
const indexHtmlWithBaseUri = indexHtml.replace(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { type MaybePromise, sleep } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { GPT_RUNNER_OFFICIAL_FOLDER, type MaybePromise, sleep } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { IconButton } from '../../../../components/icon-button'
|
||||
import { initGptFiles } from '../../../../networks/gpt-files'
|
||||
import { getGlobalConfig } from '../../../../helpers/global-config'
|
||||
@@ -45,7 +45,7 @@ export const InitGptFiles: FC<InitGptFilesProps> = (props) => {
|
||||
</Title>
|
||||
<Title>
|
||||
Do you need to create a
|
||||
<StyledVSCodeTag>./gpt-presets/copilot.gpt.md</StyledVSCodeTag>
|
||||
<StyledVSCodeTag>./{GPT_RUNNER_OFFICIAL_FOLDER}/copilot.gpt.md</StyledVSCodeTag>
|
||||
file?
|
||||
</Title>
|
||||
<IconButton
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import './src/proxy'
|
||||
import path from 'node:path'
|
||||
import http from 'node:http'
|
||||
import type { Express } from 'express'
|
||||
import express from 'express'
|
||||
@@ -11,7 +10,7 @@ import { errorHandlerMiddleware } from './src/middleware'
|
||||
|
||||
const dirname = PathUtils.getCurrentDirName(import.meta.url, () => __dirname)
|
||||
|
||||
const resolvePath = (...paths: string[]) => path.resolve(dirname, ...paths)
|
||||
const resolvePath = (...paths: string[]) => PathUtils.resolve(dirname, ...paths)
|
||||
|
||||
export const DEFAULT_CLIENT_DIST_PATH = resolvePath('../dist/browser')
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
```json
|
||||
{
|
||||
"title": "common/"
|
||||
"title": "common",
|
||||
"model": {
|
||||
"type": "openai",
|
||||
"modelName": "gpt-3.5-turbo-16k",
|
||||
"temperature": 0.7
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# System Prompt
|
||||
|
||||
this is a system prompt
|
||||
input your system prompt here
|
||||
|
||||
# User Prompt
|
||||
|
||||
this is a user prompt
|
||||
input your user prompt here
|
||||
|
||||
Reference in New Issue
Block a user