feat(gpt-runner-vscode): add completion support

This commit is contained in:
JinmingYang
2023-06-21 23:08:11 +08:00
parent 5e92d3ea8a
commit 6f07f83675
18 changed files with 226 additions and 45 deletions

2
.gitignore vendored
View File

@@ -32,3 +32,5 @@ docs/changelog
docs/_dogfooding/_swizzle_theme_tests
docs/i18n/**/*
.gpt-runner

View File

@@ -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",

View File

@@ -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[] = []

View File

@@ -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)

View 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
}
}

View File

@@ -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`)

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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"
}
}
}

View File

@@ -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()

View File

@@ -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' })
}

View File

@@ -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
`

View File

@@ -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
}

View 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()
})
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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')

View File

@@ -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