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