Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/newWorkspaceFollowup.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as l10n from '@vscode/l10n';5import { ChatResponseFileTreePart, Disposable, MarkdownString, ProgressLocation, SaveDialogOptions, Tab, TabInputText, Uri, commands, env, interactive, window, workspace } from 'vscode';6import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';7import { ILogService } from '../../../platform/log/common/logService';8import * as path from '../../../util/vs/base/common/path';9import { CopilotFileScheme, CopilotWorkspaceScheme, CreateFileCommand, CreateProjectCommand, GithubWorkspaceScheme, INewWorkspacePreviewContentManager, OpenFileCommand } from '../../intents/node/newIntent';10import { NewWorkspacePreviewFileSystemProvider } from '../../intents/vscode-node/newWorkspacePreviewFileSystemProvider';11import { NewWorkspaceTextDocumentProvider } from '../../intents/vscode-node/newWorkspaceTextDocumentProvider';12import { listFilesInResponseFileTree } from '../../prompt/common/fileTreeParser';1314export function registerNewWorkspaceIntentCommand(previewContentManager: INewWorkspacePreviewContentManager, logService: ILogService, options: IConversationOptions) {15const copilotWorkspaceProvider = new NewWorkspacePreviewFileSystemProvider(previewContentManager);16const githubWorkspaceProvider = new NewWorkspacePreviewFileSystemProvider(previewContentManager);17const copilotTextDocumentProvider = new NewWorkspaceTextDocumentProvider(previewContentManager);1819return Disposable.from(20workspace.registerFileSystemProvider(CopilotWorkspaceScheme,21copilotWorkspaceProvider,22{ isReadonly: new MarkdownString(l10n.t('This file preview was generated by Copilot and may contain surprises or mistakes.\n\nAsk followup questions to refine it, then press Create Workspace.')) }),23workspace.registerFileSystemProvider(GithubWorkspaceScheme,24githubWorkspaceProvider,25{ isReadonly: true }),26workspace.registerTextDocumentContentProvider(CopilotFileScheme, copilotTextDocumentProvider),27commands.registerCommand(CreateProjectCommand, async (fileTreePart: ChatResponseFileTreePart, workspaceRoot: Uri | undefined) => {28const parentFolder = (await window.showOpenDialog({ defaultUri: workspaceRoot, title: path.basename(fileTreePart.baseUri.path), canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select as Parent Folder' }))?.[0];29if (!parentFolder) {30return;31}32await createWorkspace(logService, workspaceRoot, parentFolder, fileTreePart);33}),34commands.registerCommand(OpenFileCommand, async (fileTreePart: ChatResponseFileTreePart) => {35const pathStr = Uri.joinPath(fileTreePart.baseUri, fileTreePart.value[0].name).toString();36const document = await workspace.openTextDocument(Uri.parse(pathStr));37await window.showTextDocument(document, { preview: false });38}),39commands.registerCommand(CreateFileCommand, async (fileTreePart: ChatResponseFileTreePart) => {40const options: SaveDialogOptions = {41defaultUri: Uri.file(path.posix.join(workspace.workspaceFolders?.[0].uri.path ?? '', fileTreePart.value[0].name)),42saveLabel: l10n.t('Save File'),43};44const uri = await window.showSaveDialog(options);45if (uri) {46const pathStr = Uri.joinPath(fileTreePart.baseUri, fileTreePart.value[0].name).toString();47const document = await workspace.openTextDocument(Uri.parse(pathStr));48await workspace.fs.writeFile(uri, Buffer.from(document.getText()));4950// Close out all previews since they won't properly restore51const tabsToClose: Tab[] = [];52window.tabGroups.all.forEach(group => {53group.tabs.forEach((tab) => {54if (tab.input instanceof TabInputText && tab.input.uri.scheme === CopilotFileScheme) {55tabsToClose.push(tab);56}57});58});59window.tabGroups.close(tabsToClose, true);6061// re-open saved file62const fileDoc = await workspace.openTextDocument(Uri.file(uri.fsPath));63await window.showTextDocument(fileDoc);64}65}),66copilotWorkspaceProvider,67githubWorkspaceProvider,68copilotTextDocumentProvider,69);70}7172async function createWorkspace(logService: ILogService, workspaceRoot: Uri | undefined, parentFolder: Uri, fileTreePart: ChatResponseFileTreePart) {73// Close out all previews since they won't properly restore74const tabsToClose: Tab[] = [];75window.tabGroups.all.forEach(group => {76group.tabs.forEach((tab) => {77if (tab.input instanceof TabInputText && tab.input.uri.scheme === CopilotWorkspaceScheme) {78tabsToClose.push(tab);79}80});81});82window.tabGroups.close(tabsToClose, true);8384// remove path separator from the beginning of the path85const projectRoot = fileTreePart.baseUri.path.slice(1);86const projectName = await getUniqueProjectName(parentFolder, projectRoot);87const workspaceUri = Uri.joinPath(parentFolder, projectName);8889const files = listFilesInResponseFileTree(fileTreePart.value);90if (files.length === 0) {91return;92}9394try {95await window.withProgress({ location: ProgressLocation.Notification, cancellable: true }, async (progress, token) => {96for (const file of files) {97const relativeFilePath = path.relative(projectRoot, file);98const fileUri = Uri.joinPath(workspaceUri, relativeFilePath);99progress.report({ message: l10n.t(`Creating file {0}...`, fileUri.fsPath) });100const content = await workspace.fs.readFile(Uri.joinPath(fileTreePart.baseUri, file));101await workspace.fs.createDirectory(Uri.joinPath(fileUri, '..'));102await workspace.fs.writeFile(fileUri, content);103}104await updateAiGeneratedWorkspacesFile(workspaceUri);105});106107if (workspaceRoot && workspaceUri.fsPath.startsWith(workspaceRoot.fsPath + path.sep)) {108// If the new workspace is a subfolder of the current workspace, do nothing109return;110}111112const message = l10n.t('Would you like to open the created workspace?');113const open = l10n.t('Open');114const openNewWindow = l10n.t('Open in New Window');115const choices = [open, openNewWindow];116const result = await window.showInformationMessage(message, { modal: true }, ...choices);117if (result === open) {118119await interactive.transferActiveChat(workspaceUri);120logService.info(121'[newIntent] Opening folder: ' + workspaceUri.fsPath,122);123commands.executeCommand('vscode.openFolder', workspaceUri);124} else if (result === openNewWindow) {125commands.executeCommand('vscode.openFolder', workspaceUri, true);126}127}128catch (error) {129const errorMessage = l10n.t('Failed to create workspace: {0}', projectName);130logService.error(error, errorMessage);131window.showErrorMessage(errorMessage);132await workspace.fs.delete(workspaceUri, { recursive: true });133}134}135136137async function getUniqueProjectName(projectFolder: Uri, projectName: string): Promise<string> {138let i = 0;139let uniqueProjectNameNotFound = true;140let newProjectName = projectName.replace(/^\W+/, '');141while (uniqueProjectNameNotFound) {142try {143await workspace.fs.stat(Uri.joinPath(projectFolder, newProjectName));144newProjectName = projectName + '-' + ++i;145} catch {146uniqueProjectNameNotFound = false;147}148}149return newProjectName;150}151152async function checkFileExists(filePath: Uri): Promise<boolean> {153try {154await workspace.fs.stat(filePath);155return true;156} catch (error) {157return false;158}159}160161async function updateAiGeneratedWorkspacesFile(workspaceUris: Uri) {162const aiGeneratedFilePath = getAiGeneratedWorkspacesFile();163if (!aiGeneratedFilePath) {164return;165}166167if ((await checkFileExists(aiGeneratedFilePath))) {168const fileContnet = await workspace.fs.readFile(aiGeneratedFilePath).then(b => { return new TextDecoder().decode(b); });169const workspaces = JSON.parse(fileContnet) as string[];170workspaces.push(workspaceUris.toString());171await workspace.fs.writeFile(aiGeneratedFilePath, Buffer.from(JSON.stringify(workspaces, null, 2)));172} else {173await workspace.fs.writeFile(aiGeneratedFilePath, Buffer.from(JSON.stringify([workspaceUris.toString()], null, 2)));174}175}176177function getAiGeneratedWorkspacesFile(): Uri | undefined {178const vscodeFolderName = env.appName.indexOf('Insider') > 0 || env.appName.indexOf('Code - OSS Dev') >= 0 ? 'Code - Insiders' : 'Code';179const homeDir = Uri.file(process.env.HOME || (process.env.USERPROFILE ? process.env.USERPROFILE : ''));180switch (process.platform) {181case 'darwin':182return Uri.joinPath(183homeDir,184'Library',185'Application Support',186vscodeFolderName,187'User',188'workspaceStorage',189'aiGeneratedWorkspaces.json'190);191case 'linux':192return Uri.joinPath(homeDir, '.config', vscodeFolderName, 'User', 'workspaceStorage', 'aiGeneratedWorkspaces.json');193case 'win32':194return process.env.APPDATA195? Uri.joinPath(Uri.file(process.env.APPDATA), vscodeFolderName, 'User', 'workspaceStorage', 'aiGeneratedWorkspaces.json')196: undefined;197default:198return;199}200}201202203