Skip to content

Commit 6c597e7

Browse files
authored
Make SaveAs work for untitled files (#1305)
* Add saveas support for untitled files * Improve comment about not saving to non-file URI schemes * Improve logging in extension commands * Improve logging code * Address PR feedback * Add helpful messages * Add logging and save failure warnings
1 parent c1b643e commit 6c597e7

File tree

3 files changed

+120
-34
lines changed

3 files changed

+120
-34
lines changed

src/features/ExtensionCommands.ts

+117-32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as path from "path";
88
import * as vscode from "vscode";
99
import { LanguageClient, NotificationType, Position, Range, RequestType } from "vscode-languageclient";
1010
import { IFeature } from "../feature";
11+
import { Logger } from "../logging";
1112

1213
export interface IExtensionCommand {
1314
name: string;
@@ -172,10 +173,11 @@ export class ExtensionCommandsFeature implements IFeature {
172173
private languageClient: LanguageClient;
173174
private extensionCommands: IExtensionCommand[] = [];
174175

175-
constructor() {
176+
constructor(private log: Logger) {
176177
this.command = vscode.commands.registerCommand("PowerShell.ShowAdditionalCommands", () => {
177178
if (this.languageClient === undefined) {
178-
// TODO: Log error message
179+
this.log.writeAndShowError(`<${ExtensionCommandsFeature.name}>: ` +
180+
"Unable to instantiate; language client undefined.");
179181
return;
180182
}
181183

@@ -388,44 +390,127 @@ export class ExtensionCommandsFeature implements IFeature {
388390
return promise;
389391
}
390392

393+
/**
394+
* Save a file, possibly to a new path. If the save is not possible, return a completed response
395+
* @param saveFileDetails the object detailing the path of the file to save and optionally its new path to save to
396+
*/
391397
private async saveFile(saveFileDetails: ISaveFileDetails): Promise<EditorOperationResponse> {
392-
393-
// If the file to save can't be found, just complete the request
394-
if (!this.findTextDocument(this.normalizeFilePath(saveFileDetails.filePath))) {
395-
return EditorOperationResponse.Completed;
398+
// Try to interpret the filepath as a URI, defaulting to "file://" if we don't succeed
399+
let currentFileUri: vscode.Uri;
400+
if (saveFileDetails.filePath.startsWith("untitled") || saveFileDetails.filePath.startsWith("file")) {
401+
currentFileUri = vscode.Uri.parse(saveFileDetails.filePath);
402+
} else {
403+
currentFileUri = vscode.Uri.file(saveFileDetails.filePath);
396404
}
397405

398-
// If no newFile is given, just save the current file
399-
if (!saveFileDetails.newPath) {
400-
const doc = await vscode.workspace.openTextDocument(saveFileDetails.filePath);
401-
if (doc.isDirty) {
402-
await doc.save();
403-
}
404-
405-
return EditorOperationResponse.Completed;
406+
let newFileAbsolutePath: string;
407+
switch (currentFileUri.scheme) {
408+
case "file":
409+
// If the file to save can't be found, just complete the request
410+
if (!this.findTextDocument(this.normalizeFilePath(currentFileUri.fsPath))) {
411+
this.log.writeAndShowError(`File to save not found: ${currentFileUri.fsPath}.`);
412+
return EditorOperationResponse.Completed;
413+
}
414+
415+
// If no newFile is given, just save the current file
416+
if (!saveFileDetails.newPath) {
417+
const doc = await vscode.workspace.openTextDocument(currentFileUri.fsPath);
418+
if (doc.isDirty) {
419+
await doc.save();
420+
}
421+
return EditorOperationResponse.Completed;
422+
}
423+
424+
// Make sure we have an absolute path
425+
if (path.isAbsolute(saveFileDetails.newPath)) {
426+
newFileAbsolutePath = saveFileDetails.newPath;
427+
} else {
428+
// If not, interpret the path as relative to the current file
429+
newFileAbsolutePath = path.join(path.dirname(currentFileUri.fsPath), saveFileDetails.newPath);
430+
}
431+
break;
432+
433+
case "untitled":
434+
// We need a new name to save an untitled file
435+
if (!saveFileDetails.newPath) {
436+
// TODO: Create a class handle vscode warnings and errors so we can warn easily
437+
// without logging
438+
this.log.writeAndShowWarning(
439+
"Cannot save untitled file. Try SaveAs(\"path/to/file.ps1\") instead.");
440+
return EditorOperationResponse.Completed;
441+
}
442+
443+
// Make sure we have an absolute path
444+
if (path.isAbsolute(saveFileDetails.newPath)) {
445+
newFileAbsolutePath = saveFileDetails.newPath;
446+
} else {
447+
// In fresh contexts, workspaceFolders is not defined...
448+
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
449+
this.log.writeAndShowWarning("Cannot save file to relative path: no workspaces are open. " +
450+
"Try saving to an absolute path, or open a workspace.");
451+
return EditorOperationResponse.Completed;
452+
}
453+
454+
// If not, interpret the path as relative to the workspace root
455+
const workspaceRootUri = vscode.workspace.workspaceFolders[0].uri;
456+
// We don't support saving to a non-file URI-schemed workspace
457+
if (workspaceRootUri.scheme !== "file") {
458+
this.log.writeAndShowWarning(
459+
"Cannot save untitled file to a relative path in an untitled workspace. " +
460+
"Try saving to an absolute path or opening a workspace folder.");
461+
return EditorOperationResponse.Completed;
462+
}
463+
newFileAbsolutePath = path.join(workspaceRootUri.fsPath, saveFileDetails.newPath);
464+
}
465+
break;
466+
467+
default:
468+
// Other URI schemes are not supported
469+
const msg = JSON.stringify(saveFileDetails);
470+
this.log.writeVerbose(
471+
`<${ExtensionCommandsFeature.name}>: Saving a document with scheme '${currentFileUri.scheme}' ` +
472+
`is currently unsupported. Message: '${msg}'`);
473+
return EditorOperationResponse.Completed;
406474
}
407475

408-
// Otherwise we want to save as a new file
409-
410-
// First turn the path we were given into an absolute path
411-
// Relative paths are interpreted as relative to the original file
412-
const newFileAbsolutePath = path.isAbsolute(saveFileDetails.newPath) ?
413-
saveFileDetails.newPath :
414-
path.resolve(path.dirname(saveFileDetails.filePath), saveFileDetails.newPath);
415-
416-
// Retrieve the text out of the current document
417-
const oldDocument = await vscode.workspace.openTextDocument(saveFileDetails.filePath);
476+
await this.saveDocumentContentToAbsolutePath(currentFileUri, newFileAbsolutePath);
477+
return EditorOperationResponse.Completed;
418478

419-
// Write it to the new document path
420-
fs.writeFileSync(newFileAbsolutePath, oldDocument.getText());
479+
}
421480

422-
// Finally open the new document
423-
const newFileUri = vscode.Uri.file(newFileAbsolutePath);
424-
const newFile = await vscode.workspace.openTextDocument(newFileUri);
425-
vscode.window.showTextDocument(newFile, { preview: true });
481+
/**
482+
* Take a document available to vscode at the given URI and save it to the given absolute path
483+
* @param documentUri the URI of the vscode document to save
484+
* @param destinationAbsolutePath the absolute path to save the document contents to
485+
*/
486+
private async saveDocumentContentToAbsolutePath(
487+
documentUri: vscode.Uri,
488+
destinationAbsolutePath: string): Promise<void> {
489+
// Retrieve the text out of the current document
490+
const oldDocument = await vscode.workspace.openTextDocument(documentUri);
491+
492+
// Write it to the new document path
493+
try {
494+
// TODO: Change this to be asyncronous
495+
await new Promise<void>((resolve, reject) => {
496+
fs.writeFile(destinationAbsolutePath, oldDocument.getText(), (err) => {
497+
if (err) {
498+
return reject(err);
499+
}
500+
return resolve();
501+
});
502+
});
503+
} catch (e) {
504+
this.log.writeAndShowWarning(`<${ExtensionCommandsFeature.name}>: ` +
505+
`Unable to save file to path '${destinationAbsolutePath}': ${e}`);
506+
return;
507+
}
426508

427-
return EditorOperationResponse.Completed;
428-
}
509+
// Finally open the new document
510+
const newFileUri = vscode.Uri.file(destinationAbsolutePath);
511+
const newFile = await vscode.workspace.openTextDocument(newFileUri);
512+
vscode.window.showTextDocument(newFile, { preview: true });
513+
}
429514

430515
private normalizeFilePath(filePath: string): string {
431516
const platform = os.platform();

src/features/HelpCompletion.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export class HelpCompletionFeature implements IFeature {
5656

5757
public onEvent(changeEvent: TextDocumentChangeEvent): void {
5858
if (!(changeEvent && changeEvent.contentChanges)) {
59-
this.log.write(`Bad TextDocumentChangeEvent message: ${JSON.stringify(changeEvent)}`);
59+
this.log.writeWarning(`<${HelpCompletionFeature.name}>: ` +
60+
`Bad TextDocumentChangeEvent message: ${JSON.stringify(changeEvent)}`);
6061
return;
6162
}
6263

src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export function activate(context: vscode.ExtensionContext): void {
121121
new ShowHelpFeature(),
122122
new FindModuleFeature(),
123123
new PesterTestsFeature(sessionManager),
124-
new ExtensionCommandsFeature(),
124+
new ExtensionCommandsFeature(logger),
125125
new SelectPSSARulesFeature(),
126126
new CodeActionsFeature(),
127127
new NewFileOrProjectFeature(),

0 commit comments

Comments
 (0)