From 6f07f836751a6ddeaa7a353646522f4cd4f0613c Mon Sep 17 00:00:00 2001 From: JinmingYang <2214962083@qq.com> Date: Wed, 21 Jun 2023 23:08:11 +0800 Subject: [PATCH] feat(gpt-runner-vscode): add completion support --- .gitignore | 2 + .vscode/settings.json | 7 ++ .../src/core/get-common-file-tree.ts | 14 +-- .../src/core/get-gpt-file-tree.ts | 15 +-- .../gpt-runner-core/src/core/gitignore.ts | 24 ++++ .../src/core/init-gpt-files/index.ts | 6 +- .../src/common/helpers/constants.ts | 2 +- .../src/node/helpers/file-manager.ts | 3 +- packages/gpt-runner-vscode/package.json | 6 +- packages/gpt-runner-vscode/scripts/build.ts | 20 ++- packages/gpt-runner-vscode/scripts/dev.ts | 10 ++ packages/gpt-runner-vscode/src/constant.ts | 20 +++ packages/gpt-runner-vscode/src/index.ts | 2 + .../src/register/completion.ts | 118 ++++++++++++++++++ .../gpt-runner-vscode/src/register/webview.ts | 4 +- .../chat/components/init-gpt-files/index.tsx | 4 +- packages/gpt-runner-web/server/index.ts | 3 +- playground/scripts/gpt/test.gpt.md | 11 +- 18 files changed, 226 insertions(+), 45 deletions(-) create mode 100644 packages/gpt-runner-core/src/core/gitignore.ts create mode 100644 packages/gpt-runner-vscode/src/register/completion.ts diff --git a/.gitignore b/.gitignore index 586963b..064c0e1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ docs/changelog docs/_dogfooding/_swizzle_theme_tests docs/i18n/**/* + +.gpt-runner diff --git a/.vscode/settings.json b/.vscode/settings.json index ebef121..343a274 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", diff --git a/packages/gpt-runner-core/src/core/get-common-file-tree.ts b/packages/gpt-runner-core/src/core/get-common-file-tree.ts index d68dd33..6fa516c 100644 --- a/packages/gpt-runner-core/src/core/get-common-file-tree.ts +++ b/packages/gpt-runner-core/src/core/get-common-file-tree.ts @@ -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 { 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[] = [] diff --git a/packages/gpt-runner-core/src/core/get-gpt-file-tree.ts b/packages/gpt-runner-core/src/core/get-gpt-file-tree.ts index 09a6fbc..04fc891 100644 --- a/packages/gpt-runner-core/src/core/get-gpt-file-tree.ts +++ b/packages/gpt-runner-core/src/core/get-gpt-file-tree.ts @@ -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 { - 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 { + const relativePath = PathUtils.relative(rootPath, filePath) + + if (relativePath.includes(GPT_RUNNER_OFFICIAL_FOLDER)) + return false + + return ig?.ignores(relativePath) ?? false + } +} diff --git a/packages/gpt-runner-core/src/core/init-gpt-files/index.ts b/packages/gpt-runner-core/src/core/init-gpt-files/index.ts index 95a531e..f4ccd23 100644 --- a/packages/gpt-runner-core/src/core/init-gpt-files/index.ts +++ b/packages/gpt-runner-core/src/core/init-gpt-files/index.ts @@ -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 /gpt-presets folder + * write some .gpt.md files to the /.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`) diff --git a/packages/gpt-runner-shared/src/common/helpers/constants.ts b/packages/gpt-runner-shared/src/common/helpers/constants.ts index b81f956..914cbc3 100644 --- a/packages/gpt-runner-shared/src/common/helpers/constants.ts +++ b/packages/gpt-runner-shared/src/common/helpers/constants.ts @@ -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', diff --git a/packages/gpt-runner-shared/src/node/helpers/file-manager.ts b/packages/gpt-runner-shared/src/node/helpers/file-manager.ts index 2d41b99..49e55be 100644 --- a/packages/gpt-runner-shared/src/node/helpers/file-manager.ts +++ b/packages/gpt-runner-shared/src/node/helpers/file-manager.ts @@ -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 { diff --git a/packages/gpt-runner-vscode/package.json b/packages/gpt-runner-vscode/package.json index 1ccfe43..cf767db 100644 --- a/packages/gpt-runner-vscode/package.json +++ b/packages/gpt-runner-vscode/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/gpt-runner-vscode/scripts/build.ts b/packages/gpt-runner-vscode/scripts/build.ts index 2fc18d0..484819c 100644 --- a/packages/gpt-runner-vscode/scripts/build.ts +++ b/packages/gpt-runner-vscode/scripts/build.ts @@ -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 /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 /node_modules/@nicepkg/gpt-runner-web/dist to /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 /node_modules/@nicepkg/gpt-runner-shared/dist/json-schema to /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() diff --git a/packages/gpt-runner-vscode/scripts/dev.ts b/packages/gpt-runner-vscode/scripts/dev.ts index ae7f101..3403642 100644 --- a/packages/gpt-runner-vscode/scripts/dev.ts +++ b/packages/gpt-runner-vscode/scripts/dev.ts @@ -20,6 +20,16 @@ async function dev() { ) } + // make symlink from /node_modules/@nicepkg/gpt-runner-shared/dist/json-schema to /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' }) } diff --git a/packages/gpt-runner-vscode/src/constant.ts b/packages/gpt-runner-vscode/src/constant.ts index 7ec3cc7..77dc06d 100644 --- a/packages/gpt-runner-vscode/src/constant.ts +++ b/packages/gpt-runner-vscode/src/constant.ts @@ -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 +` diff --git a/packages/gpt-runner-vscode/src/index.ts b/packages/gpt-runner-vscode/src/index.ts index 5dfb5ad..84714f7 100644 --- a/packages/gpt-runner-vscode/src/index.ts +++ b/packages/gpt-runner-vscode/src/index.ts @@ -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 } diff --git a/packages/gpt-runner-vscode/src/register/completion.ts b/packages/gpt-runner-vscode/src/register/completion.ts new file mode 100644 index 0000000..422eab1 --- /dev/null +++ b/packages/gpt-runner-vscode/src/register/completion.ts @@ -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: '"${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() + }) +} diff --git a/packages/gpt-runner-vscode/src/register/webview.ts b/packages/gpt-runner-vscode/src/register/webview.ts index a3e3ea7..2c00249 100644 --- a/packages/gpt-runner-vscode/src/register/webview.ts +++ b/packages/gpt-runner-vscode/src/register/webview.ts @@ -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( diff --git a/packages/gpt-runner-web/client/src/pages/chat/components/init-gpt-files/index.tsx b/packages/gpt-runner-web/client/src/pages/chat/components/init-gpt-files/index.tsx index 0045992..88b1ff7 100644 --- a/packages/gpt-runner-web/client/src/pages/chat/components/init-gpt-files/index.tsx +++ b/packages/gpt-runner-web/client/src/pages/chat/components/init-gpt-files/index.tsx @@ -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 = (props) => { Do you need to create a - <StyledVSCodeTag>./gpt-presets/copilot.gpt.md</StyledVSCodeTag> + <StyledVSCodeTag>./{GPT_RUNNER_OFFICIAL_FOLDER}/copilot.gpt.md</StyledVSCodeTag> file? __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') diff --git a/playground/scripts/gpt/test.gpt.md b/playground/scripts/gpt/test.gpt.md index c10fd07..750e233 100644 --- a/playground/scripts/gpt/test.gpt.md +++ b/playground/scripts/gpt/test.gpt.md @@ -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