diff --git a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 b/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 index dbbbea6a3..da611b92f 100644 --- a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 +++ b/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 @@ -77,7 +77,8 @@ FunctionsToExport = @('Register-EditorCommand', 'Out-CurrentFile', 'Join-ScriptExtent', 'Test-ScriptExtent', - 'Open-EditorFile') + 'Open-EditorFile', + 'New-EditorFile') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 index 83094c02a..5f7f1bdfb 100644 --- a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 @@ -77,13 +77,111 @@ function Unregister-EditorCommand { } } +<# +.SYNOPSIS + Creates new files and opens them in your editor window +.DESCRIPTION + Creates new files and opens them in your editor window +.EXAMPLE + PS > New-EditorFile './foo.ps1' + Creates and opens a new foo.ps1 in your editor +.EXAMPLE + PS > Get-Process | New-EditorFile proc.txt + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process +.EXAMPLE + PS > Get-Process | New-EditorFile proc.txt -Force + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process. Overwrites the file if it already exists +.INPUTS + Path + an array of files you want to open in your editor + Value + The content you want in the new files + Force + Overwrites a file if it exists +#> +function New-EditorFile { + [CmdletBinding()] + param( + [Parameter()] + [String[]] + [ValidateNotNullOrEmpty()] + $Path, + + [Parameter(ValueFromPipeline=$true)] + $Value, + + [Parameter()] + [switch] + $Force + ) + + begin { + $valueList = @() + } + + process { + $valueList += $Value + } + + end { + if ($Path) { + foreach ($fileName in $Path) + { + if (-not (Test-Path $fileName) -or $Force) { + New-Item -Path $fileName -ItemType File | Out-Null + + if ($Path.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + $psEditor.Workspace.OpenFile($fileName, $preview) + $psEditor.GetEditorContext().CurrentFile.InsertText(($valueList | Out-String)) + } else { + $PSCmdlet.WriteError( ( + New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList @( + [System.IO.IOException]"The file '$fileName' already exists.", + 'NewEditorFileIOError', + [System.Management.Automation.ErrorCategory]::WriteError, + $fileName) ) ) + } + } + } else { + $psEditor.Workspace.NewFile() + $psEditor.GetEditorContext().CurrentFile.InsertText(($valueList | Out-String)) + } + } +} + function Open-EditorFile { - param([Parameter(Mandatory=$true)]$FilePaths) + [CmdletBinding()] + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [ValidateNotNullOrEmpty()] + $Path + ) - Get-ChildItem $FilePaths -File | ForEach-Object { - $psEditor.Workspace.OpenFile($_.FullName) + begin { + $Paths = @() + } + + process { + $Paths += $Path + } + + end { + if ($Paths.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + Get-ChildItem $Paths -File | ForEach-Object { + $psEditor.Workspace.OpenFile($_.FullName, $preview) + } } } Set-Alias psedit Open-EditorFile -Scope Global -Export-ModuleMember -Function Open-EditorFile +Export-ModuleMember -Function Open-EditorFile,New-EditorFile diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs index c66582700..f5dd40217 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs @@ -111,8 +111,15 @@ public static readonly public class OpenFileRequest { public static readonly - RequestType Type = - RequestType.Create("editor/openFile"); + RequestType Type = + RequestType.Create("editor/openFile"); + } + + public class OpenFileDetails + { + public string FilePath { get; set; } + + public bool Preview { get; set; } } public class CloseFileRequest diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs index 16e1aefc0..f8989cfb5 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs @@ -12,6 +12,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server { internal class LanguageServerEditorOperations : IEditorOperations { + private const bool DefaultPreviewSetting = true; + private EditorSession editorSession; private IMessageSender messageSender; @@ -115,7 +117,24 @@ public Task OpenFile(string filePath) return this.messageSender.SendRequest( OpenFileRequest.Type, - filePath, + new OpenFileDetails + { + FilePath = filePath, + Preview = DefaultPreviewSetting + }, + true); + } + + public Task OpenFile(string filePath, bool preview) + { + return + this.messageSender.SendRequest( + OpenFileRequest.Type, + new OpenFileDetails + { + FilePath = filePath, + Preview = preview + }, true); } diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 8923b48ef..9bdd2c043 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -58,6 +58,18 @@ public void OpenFile(string filePath) this.editorOperations.OpenFile(filePath).Wait(); } + /// + /// Opens a file in the workspace. If the file is already open + /// its buffer will be made active. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path to the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + public void OpenFile(string filePath, bool preview) + { + this.editorOperations.OpenFile(filePath, preview).Wait(); + } + #endregion } } diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 35d747fc3..2cf097708 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -47,6 +47,16 @@ public interface IEditorOperations /// A Task that can be tracked for completion. Task OpenFile(string filePath); + /// + /// Causes a file to be opened in the editor. If the file is + /// already open, the editor must switch to the file. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path of the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + /// A Task that can be tracked for completion. + Task OpenFile(string filePath, bool preview); + /// /// Causes a file to be closed in the editor. /// diff --git a/src/PowerShellEditorServices/Session/RemoteFileManager.cs b/src/PowerShellEditorServices/Session/RemoteFileManager.cs index 5e3f324f0..bba8e5316 100644 --- a/src/PowerShellEditorServices/Session/RemoteFileManager.cs +++ b/src/PowerShellEditorServices/Session/RemoteFileManager.cs @@ -36,61 +36,175 @@ public class RemoteFileManager private const string RemoteSessionOpenFile = "PSESRemoteSessionOpenFile"; - private const string PSEditFunctionScript = @" - param ( - [Parameter(Mandatory=$true)] [String[]] $FileNames - ) + private const string PSEditModule = @"<# + .SYNOPSIS + Opens the specified files in your editor window + .DESCRIPTION + Opens the specified files in your editor window + .EXAMPLE + PS > Open-EditorFile './foo.ps1' + Opens foo.ps1 in your editor + .EXAMPLE + PS > gci ./myDir | Open-EditorFile + Opens everything in 'myDir' in your editor + .INPUTS + Path + an array of files you want to open in your editor + #> + function Open-EditorFile { + param ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [String[]] + $Path + ) + + begin { + $Paths = @() + } - foreach ($fileName in $FileNames) - { - dir $fileName | where { ! $_.PSIsContainer } | foreach { - $filePathName = $_.FullName + process { + $Paths += $Path + } - # Get file contents - $params = @{ Path=$filePathName; Raw=$true } - if ($PSVersionTable.PSEdition -eq 'Core') - { - $params['AsByteStream']=$true + end { + if ($Paths.Count -gt 1) { + $preview = $false + } else { + $preview = $true } - else + + foreach ($fileName in $Paths) { - $params['Encoding']='Byte' + dir $fileName | where { ! $_.PSIsContainer } | foreach { + $filePathName = $_.FullName + + # Get file contents + $params = @{ Path=$filePathName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Get-Content @params + + # Notify client for file open. + New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes, $preview) > $null + } } + } + } - $contentBytes = Get-Content @params + <# + .SYNOPSIS + Creates new files and opens them in your editor window + .DESCRIPTION + Creates new files and opens them in your editor window + .EXAMPLE + PS > New-EditorFile './foo.ps1' + Creates and opens a new foo.ps1 in your editor + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt -Force + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process. Overwrites the file if it already exists + .INPUTS + Path + an array of files you want to open in your editor + Value + The content you want in the new files + Force + Overwrites a file if it exists + #> + function New-EditorFile { + [CmdletBinding()] + param ( + [Parameter()] + [String[]] + [ValidateNotNullOrEmpty()] + $Path, + + [Parameter(ValueFromPipeline=$true)] + $Value, + + [Parameter()] + [switch] + $Force + ) + + begin { + $valueList = @() + } - # Notify client for file open. - New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes) > $null + process { + $valueList += $Value + } + + end { + if ($Path) { + foreach ($fileName in $Path) + { + if (-not (Test-Path $fileName) -or $Force) { + $valueList > $fileName + + # Get file contents + $params = @{ Path=$fileName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Get-Content @params + + if ($Path.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + # Notify client for file open. + New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($fileName, $contentBytes, $preview) > $null + } else { + $PSCmdlet.WriteError( ( + New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList @( + [System.Exception]'File already exists.' + $Null + [System.Management.Automation.ErrorCategory]::ResourceExists + $fileName ) ) ) + } + } + } else { + $bytes = [System.Text.Encoding]::UTF8.GetBytes(($valueList | Out-String)) + New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($null, $bytes) > $null + } } } + + Set-Alias psedit Open-EditorFile -Scope Global + Export-ModuleMember -Function Open-EditorFile, New-EditorFile "; // This script is templated so that the '-Forward' parameter can be added // to the script when in non-local sessions private const string CreatePSEditFunctionScript = @" param ( - [string] $PSEditFunction + [string] $PSEditModule ) Register-EngineEvent -SourceIdentifier PSESRemoteSessionOpenFile -Forward - - if ((Test-Path -Path 'function:\global:Open-EditorFile') -eq $false) - {{ - Set-Item -Path 'function:\global:Open-EditorFile' -Value $PSEditFunction - Set-Alias psedit Open-EditorFile -Scope Global - }} + New-Module -ScriptBlock ([Scriptblock]::Create($PSEditModule)) -Name PSEdit | Import-Module -Global "; private const string RemovePSEditFunctionScript = @" - if (Test-Path -Path 'function:\global:Open-EditorFile') - { - Remove-Item -Path 'function:\global:Open-EditorFile' -Force - } - - if (Test-Path -Path 'alias:\psedit') - { - Remove-Item -Path 'alias:\psedit' -Force - } + Get-Module PSEdit | Remove-Module Get-EventSubscriber -SourceIdentifier PSESRemoteSessionOpenFile -EA Ignore | Unregister-Event "; @@ -427,7 +541,7 @@ private async void HandleRunspaceChanged(object sender, RunspaceChangedEventArgs } } - private void HandlePSEventReceived(object sender, PSEventArgs args) + private async void HandlePSEventReceived(object sender, PSEventArgs args) { if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) { @@ -447,13 +561,18 @@ private void HandlePSEventReceived(object sender, PSEventArgs args) { byte[] fileContent = null; - if (args.SourceArgs.Length == 2) + if (args.SourceArgs.Length >= 2) { + // Try to cast as a PSObject to get the BaseObject, if not, then try to case as a byte[] PSObject sourceObj = args.SourceArgs[1] as PSObject; if (sourceObj != null) { fileContent = sourceObj.BaseObject as byte[]; } + else + { + fileContent = args.SourceArgs[1] as byte[]; + } } // If fileContent is still null after trying to @@ -461,15 +580,31 @@ private void HandlePSEventReceived(object sender, PSEventArgs args) // array. fileContent = fileContent ?? new byte[0]; - localFilePath = - this.StoreRemoteFile( - remoteFilePath, - fileContent, - this.powerShellContext.CurrentRunspace); + if (remoteFilePath != null) + { + localFilePath = + this.StoreRemoteFile( + remoteFilePath, + fileContent, + this.powerShellContext.CurrentRunspace); + } + else + { + await this.editorOperations?.NewFile(); + EditorContext context = await this.editorOperations?.GetEditorContext(); + context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); + } + } + + bool preview = true; + if (args.SourceArgs.Length >= 3) + { + bool? previewCheck = args.SourceArgs[2] as bool?; + preview = previewCheck ?? true; } // Open the file in the editor - this.editorOperations?.OpenFile(localFilePath); + this.editorOperations?.OpenFile(localFilePath, preview); } } catch (NullReferenceException e) @@ -488,17 +623,10 @@ private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) { runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceived; - var createScript = - string.Format( - CreatePSEditFunctionScript, - (runspaceDetails.Location == RunspaceLocation.Local && - runspaceDetails.Context == RunspaceContext.Original) - ? string.Empty : "-Forward"); - PSCommand createCommand = new PSCommand(); createCommand - .AddScript(createScript) - .AddParameter("PSEditFunction", PSEditFunctionScript); + .AddScript(CreatePSEditFunctionScript) + .AddParameter("PSEditModule", PSEditModule); if (runspaceDetails.Context == RunspaceContext.DebuggedRunspace) { diff --git a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs index be348a402..858ccaa98 100644 --- a/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/ExtensionServiceTests.cs @@ -195,6 +195,11 @@ public Task OpenFile(string filePath) throw new NotImplementedException(); } + public Task OpenFile(string filePath, bool preview) + { + throw new NotImplementedException(); + } + public Task CloseFile(string filePath) { throw new NotImplementedException();