diff --git a/src/features/ExtensionCommands.ts b/src/features/ExtensionCommands.ts index 049c3d23be..c5863cb939 100644 --- a/src/features/ExtensionCommands.ts +++ b/src/features/ExtensionCommands.ts @@ -8,6 +8,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { LanguageClient, NotificationType, Position, Range, RequestType } from "vscode-languageclient"; import { IFeature } from "../feature"; +import { Logger } from "../logging"; export interface IExtensionCommand { name: string; @@ -172,10 +173,11 @@ export class ExtensionCommandsFeature implements IFeature { private languageClient: LanguageClient; private extensionCommands: IExtensionCommand[] = []; - constructor() { + constructor(private log: Logger) { this.command = vscode.commands.registerCommand("PowerShell.ShowAdditionalCommands", () => { if (this.languageClient === undefined) { - // TODO: Log error message + this.log.writeAndShowError(`<${ExtensionCommandsFeature.name}>: ` + + "Unable to instantiate; language client undefined."); return; } @@ -388,44 +390,127 @@ export class ExtensionCommandsFeature implements IFeature { return promise; } + /** + * Save a file, possibly to a new path. If the save is not possible, return a completed response + * @param saveFileDetails the object detailing the path of the file to save and optionally its new path to save to + */ private async saveFile(saveFileDetails: ISaveFileDetails): Promise { - - // If the file to save can't be found, just complete the request - if (!this.findTextDocument(this.normalizeFilePath(saveFileDetails.filePath))) { - return EditorOperationResponse.Completed; + // Try to interpret the filepath as a URI, defaulting to "file://" if we don't succeed + let currentFileUri: vscode.Uri; + if (saveFileDetails.filePath.startsWith("untitled") || saveFileDetails.filePath.startsWith("file")) { + currentFileUri = vscode.Uri.parse(saveFileDetails.filePath); + } else { + currentFileUri = vscode.Uri.file(saveFileDetails.filePath); } - // If no newFile is given, just save the current file - if (!saveFileDetails.newPath) { - const doc = await vscode.workspace.openTextDocument(saveFileDetails.filePath); - if (doc.isDirty) { - await doc.save(); - } - - return EditorOperationResponse.Completed; + let newFileAbsolutePath: string; + switch (currentFileUri.scheme) { + case "file": + // If the file to save can't be found, just complete the request + if (!this.findTextDocument(this.normalizeFilePath(currentFileUri.fsPath))) { + this.log.writeAndShowError(`File to save not found: ${currentFileUri.fsPath}.`); + return EditorOperationResponse.Completed; + } + + // If no newFile is given, just save the current file + if (!saveFileDetails.newPath) { + const doc = await vscode.workspace.openTextDocument(currentFileUri.fsPath); + if (doc.isDirty) { + await doc.save(); + } + return EditorOperationResponse.Completed; + } + + // Make sure we have an absolute path + if (path.isAbsolute(saveFileDetails.newPath)) { + newFileAbsolutePath = saveFileDetails.newPath; + } else { + // If not, interpret the path as relative to the current file + newFileAbsolutePath = path.join(path.dirname(currentFileUri.fsPath), saveFileDetails.newPath); + } + break; + + case "untitled": + // We need a new name to save an untitled file + if (!saveFileDetails.newPath) { + // TODO: Create a class handle vscode warnings and errors so we can warn easily + // without logging + this.log.writeAndShowWarning( + "Cannot save untitled file. Try SaveAs(\"path/to/file.ps1\") instead."); + return EditorOperationResponse.Completed; + } + + // Make sure we have an absolute path + if (path.isAbsolute(saveFileDetails.newPath)) { + newFileAbsolutePath = saveFileDetails.newPath; + } else { + // In fresh contexts, workspaceFolders is not defined... + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + this.log.writeAndShowWarning("Cannot save file to relative path: no workspaces are open. " + + "Try saving to an absolute path, or open a workspace."); + return EditorOperationResponse.Completed; + } + + // If not, interpret the path as relative to the workspace root + const workspaceRootUri = vscode.workspace.workspaceFolders[0].uri; + // We don't support saving to a non-file URI-schemed workspace + if (workspaceRootUri.scheme !== "file") { + this.log.writeAndShowWarning( + "Cannot save untitled file to a relative path in an untitled workspace. " + + "Try saving to an absolute path or opening a workspace folder."); + return EditorOperationResponse.Completed; + } + newFileAbsolutePath = path.join(workspaceRootUri.fsPath, saveFileDetails.newPath); + } + break; + + default: + // Other URI schemes are not supported + const msg = JSON.stringify(saveFileDetails); + this.log.writeVerbose( + `<${ExtensionCommandsFeature.name}>: Saving a document with scheme '${currentFileUri.scheme}' ` + + `is currently unsupported. Message: '${msg}'`); + return EditorOperationResponse.Completed; } - // Otherwise we want to save as a new file - - // First turn the path we were given into an absolute path - // Relative paths are interpreted as relative to the original file - const newFileAbsolutePath = path.isAbsolute(saveFileDetails.newPath) ? - saveFileDetails.newPath : - path.resolve(path.dirname(saveFileDetails.filePath), saveFileDetails.newPath); - - // Retrieve the text out of the current document - const oldDocument = await vscode.workspace.openTextDocument(saveFileDetails.filePath); + await this.saveDocumentContentToAbsolutePath(currentFileUri, newFileAbsolutePath); + return EditorOperationResponse.Completed; - // Write it to the new document path - fs.writeFileSync(newFileAbsolutePath, oldDocument.getText()); + } - // Finally open the new document - const newFileUri = vscode.Uri.file(newFileAbsolutePath); - const newFile = await vscode.workspace.openTextDocument(newFileUri); - vscode.window.showTextDocument(newFile, { preview: true }); + /** + * Take a document available to vscode at the given URI and save it to the given absolute path + * @param documentUri the URI of the vscode document to save + * @param destinationAbsolutePath the absolute path to save the document contents to + */ + private async saveDocumentContentToAbsolutePath( + documentUri: vscode.Uri, + destinationAbsolutePath: string): Promise { + // Retrieve the text out of the current document + const oldDocument = await vscode.workspace.openTextDocument(documentUri); + + // Write it to the new document path + try { + // TODO: Change this to be asyncronous + await new Promise((resolve, reject) => { + fs.writeFile(destinationAbsolutePath, oldDocument.getText(), (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } catch (e) { + this.log.writeAndShowWarning(`<${ExtensionCommandsFeature.name}>: ` + + `Unable to save file to path '${destinationAbsolutePath}': ${e}`); + return; + } - return EditorOperationResponse.Completed; - } + // Finally open the new document + const newFileUri = vscode.Uri.file(destinationAbsolutePath); + const newFile = await vscode.workspace.openTextDocument(newFileUri); + vscode.window.showTextDocument(newFile, { preview: true }); + } private normalizeFilePath(filePath: string): string { const platform = os.platform(); diff --git a/src/features/HelpCompletion.ts b/src/features/HelpCompletion.ts index 0dad935e71..0892c8552c 100644 --- a/src/features/HelpCompletion.ts +++ b/src/features/HelpCompletion.ts @@ -56,7 +56,8 @@ export class HelpCompletionFeature implements IFeature { public onEvent(changeEvent: TextDocumentChangeEvent): void { if (!(changeEvent && changeEvent.contentChanges)) { - this.log.write(`Bad TextDocumentChangeEvent message: ${JSON.stringify(changeEvent)}`); + this.log.writeWarning(`<${HelpCompletionFeature.name}>: ` + + `Bad TextDocumentChangeEvent message: ${JSON.stringify(changeEvent)}`); return; } diff --git a/src/main.ts b/src/main.ts index abc30b56e2..b5e7a5db2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -121,7 +121,7 @@ export function activate(context: vscode.ExtensionContext): void { new ShowHelpFeature(), new FindModuleFeature(), new PesterTestsFeature(sessionManager), - new ExtensionCommandsFeature(), + new ExtensionCommandsFeature(logger), new SelectPSSARulesFeature(), new CodeActionsFeature(), new NewFileOrProjectFeature(),