diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ed6903bd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +_ReSharper* +[Bb]in +obj +objd +out/ +tmp/ +App_Data +*.user +*.sln.cache +*.suo +TestResults +[Tt]humbs.db +buildd.* +build/cxtcache/ +*.log +*.bak +packages +OACRTemp/ +build_logs/ +lock +/public/inc/bldver.* +/public/inc/sources.ver +/data +/target +.corext/gen +registered_data.ini + + +# quickbuild.exe +/VersionGeneratingLogs/ +QLogs +QLocal +QTestLogs + +# bad tlb/chm generators in nmake tree +*.tlb +*.chm + +# dumb silverlight +ClientBin/ + +# dump azure +*.build.csdef +csx/ + +# Temporarily exclude files generated by Script Analyzer build +PSLanguageService/Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.dll +PSLanguageService/Microsoft.Windows.PowerShell.ScriptAnalyzer.dll +PSLanguageService/PSScriptAnalyzer.psd1 +PSLanguageService/ScriptAnalyzer.format.ps1xml +PSLanguageService/ScriptAnalyzer.types.ps1xml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..104d36e64 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "submodules/PSScriptAnalyzer"] + path = submodules/PSScriptAnalyzer + url = https://github.com/PowerShell/PSScriptAnalyzer.git + branch = development diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 000000000..67f8ea046 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 000000000..9ca66594f Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 000000000..3f8c37b22 --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,144 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + + + + $(MSBuildProjectDirectory)\packages.config + $(PackagesProjectConfig) + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 "$(NuGetExePath)" + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..770ac01e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln new file mode 100644 index 000000000..29fd673a6 --- /dev/null +++ b/PowerShellEditorServices.sln @@ -0,0 +1,94 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E72-4E51-A496-B019C2BA3180}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{422E561A-8118-4BE7-A54F-9309E4F03AAE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "submodules", "submodules", "{AF08DA0C-B0A6-47AD-AC55-E13C687D4A91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptAnalyzerEngine", "submodules\PSScriptAnalyzer\Engine\ScriptAnalyzerEngine.csproj", "{F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptAnalyzerBuiltinRules", "submodules\PSScriptAnalyzer\Rules\ScriptAnalyzerBuiltinRules.csproj", "{C33B6B9D-E61C-45A3-9103-895FD82A5C1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{81E8CBCD-6319-49E7-9662-0475BD0791F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Host", "src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj", "{B2F6369A-D737-4AFD-8B81-9B094DB07DA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Transport.Stdio", "src\PowerShellEditorServices.Transport.Stdio\PowerShellEditorServices.Transport.Stdio.csproj", "{F8A0946A-5D25-4651-8079-B8D5776916FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.Host", "test\PowerShellEditorServices.Test.Host\PowerShellEditorServices.Test.Host.csproj", "{3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.Transport.Stdio", "test\PowerShellEditorServices.Test.Transport.Stdio\PowerShellEditorServices.Test.Transport.Stdio.csproj", "{E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test", "test\PowerShellEditorServices.Test\PowerShellEditorServices.Test.csproj", "{8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{E51470FB-0AF2-4A37-B4E4-78D9C6D0AFA6}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.Shared", "test\PowerShellEditorServices.Test.Shared\PowerShellEditorServices.Test.Shared.csproj", "{6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Release|Any CPU.Build.0 = Release|Any CPU + {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Release|Any CPU.Build.0 = Release|Any CPU + {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|Any CPU.Build.0 = Release|Any CPU + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|Any CPU.Build.0 = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|Any CPU.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|Any CPU.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|Any CPU.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60} = {AF08DA0C-B0A6-47AD-AC55-E13C687D4A91} + {C33B6B9D-E61C-45A3-9103-895FD82A5C1E} = {AF08DA0C-B0A6-47AD-AC55-E13C687D4A91} + {81E8CBCD-6319-49E7-9662-0475BD0791F4} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + {F8A0946A-5D25-4651-8079-B8D5776916FB} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index e7addaf57..8090711b8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,33 @@ -# Microsoft.PowerShell.EditorTools +# PowerShell Editor Services -The Microsoft.PowerShell.EditorTools library provides common functionality -that is needed to support a consistent and robust PowerShell development experience +PowerShell Editor Services provides common functionality that is needed +to enable a consistent and robust PowerShell development experience across multiple editors. ## Features -- PowerShell runtime management -- Code completion (IntelliSense) -- Interactive development console (REPL) -- Debugging (breakpoints, locals, etc) +- The Language Service provides code navigation actions (find references, go to definition) and statement completions (IntelliSense) +- The Analysis Service integrates PowerShell Script Analyzer to provide real-time semantic analysis of scripts +- The Console Service provides a simplified PowerShell host for an interactive console (REPL) +- The Debugging Service simplifies interaction with the PowerShell debugger (breakpoints, locals, etc) - COMING SOON + +The core Editor Services library is intended to be consumed in any type of host application, whether +it is a WPF UI, console application, or web service. A standard console application host is included +so that you can easily consume Editor Services functionality in any editor using either the included +standard input/output transport protocol or a transport of your own design. + +## Cloning the Code + +To clone the repository and initialize all the submodules at once you can run: + +``` +git clone --recursive https://github.com/PowerShell/PowerShellEditorServices.git +``` + +If you have already cloned the repository without `--recursive` option, you can run following commands to initialize the submodules: + +``` +git submodule init +git submodule update +``` + diff --git a/src/PowerShellEditorServices.Host/App.config b/src/PowerShellEditorServices.Host/App.config new file mode 100644 index 000000000..8e1564635 --- /dev/null +++ b/src/PowerShellEditorServices.Host/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj new file mode 100644 index 000000000..e6ceeb9fa --- /dev/null +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {B2F6369A-D737-4AFD-8B81-9B094DB07DA7} + Exe + Properties + Microsoft.PowerShell.EditorServices.Host + Microsoft.PowerShell.EditorServices.Host + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {f4bde3d0-3eef-4157-8a3e-722df7adef60} + ScriptAnalyzerEngine + + + {c33b6b9d-e61c-45a3-9103-895fd82a5c1e} + ScriptAnalyzerBuiltinRules + + + {f8a0946a-5d25-4651-8079-b8d5776916fb} + PowerShellEditorServices.Transport.Stdio + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + + + \ No newline at end of file diff --git a/src/PowerShellEditorServices.Host/Program.cs b/src/PowerShellEditorServices.Host/Program.cs new file mode 100644 index 000000000..448705d83 --- /dev/null +++ b/src/PowerShellEditorServices.Host/Program.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + class Program + { + [STAThread] + static void Main(string[] args) + { + // In the future, a more robust argument parser will be added here + bool waitForDebugger = + args.Any( + arg => + string.Equals( + arg, + "/waitForDebugger", + StringComparison.InvariantCultureIgnoreCase)); + + // Should we wait for the debugger before starting? + if (waitForDebugger) + { + while (!Debugger.IsAttached) + { + Thread.Sleep(500); + } + } + + // TODO: Select host, console host, and transport based on command line arguments + + IHost host = new StdioHost(); + host.Start(); + } + } +} diff --git a/src/PowerShellEditorServices.Host/Properties/AssemblyInfo.cs b/src/PowerShellEditorServices.Host/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8c7db5d9e --- /dev/null +++ b/src/PowerShellEditorServices.Host/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Host")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Host")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("22ca7f41-70ac-488f-a98a-30b41327e81d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/PowerShellEditorServices.Transport.Stdio/Constants.cs b/src/PowerShellEditorServices.Transport.Stdio/Constants.cs new file mode 100644 index 000000000..243bdd1e8 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Constants.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio +{ + public static class Constants + { + public const string ContentLengthString = "Content-Length: "; + public static readonly JsonSerializerSettings JsonSerializerSettings; + + static Constants() + { + JsonSerializerSettings = new JsonSerializerSettings(); + + // Camel case all object properties + JsonSerializerSettings.ContractResolver = + new CamelCasePropertyNamesContractResolver(); + + // Convert enum values to their string representation with camel casing + JsonSerializerSettings.Converters.Add( + new StringEnumConverter + { + CamelCaseText = true + }); + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs new file mode 100644 index 000000000..25aa70e7a --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + [MessageTypeName("syntaxDiag")] + public class SyntaxDiagnosticEvent : EventBase + { + public static SyntaxDiagnosticEvent Create( + string filePath, + ScriptFileMarker[] syntaxMarkers) + { + return new SyntaxDiagnosticEvent + { + Body = + DiagnosticEventBody.Create( + filePath, + syntaxMarkers) + }; + } + } + + [MessageTypeName("semanticDiag")] + public class SemanticDiagnosticEvent : EventBase + { + public static SemanticDiagnosticEvent Create( + string filePath, + ScriptFileMarker[] semanticMarkers) + { + return new SemanticDiagnosticEvent + { + Body = + DiagnosticEventBody.Create( + filePath, + semanticMarkers) + }; + } + } + + public class DiagnosticEventBody + { + public string File { get; set; } + + public Diagnostic[] Diagnostics { get; set; } + + public static DiagnosticEventBody Create( + string filePath, + ScriptFileMarker[] diagnosticMarkers) + { + List diagnosticList = new List(); + + foreach (ScriptFileMarker diagnosticMarker in diagnosticMarkers) + { + diagnosticList.Add( + new Diagnostic + { + Text = diagnosticMarker.Message, + Start = new Location + { + Line = diagnosticMarker.ScriptRegion.StartLineNumber, + Offset = diagnosticMarker.ScriptRegion.StartColumnNumber + }, + End = new Location + { + Line = diagnosticMarker.ScriptRegion.EndLineNumber, + Offset = diagnosticMarker.ScriptRegion.EndColumnNumber + } + }); + } + + return + new DiagnosticEventBody + { + File = filePath, + Diagnostics = diagnosticList.ToArray() + }; + } + } + + public class Location + { + public int Line { get; set; } + + public int Offset { get; set; } + } + + public class Diagnostic + { + public Location Start { get; set; } + + public Location End { get; set; } + + public string Text { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/Eventbase.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/Eventbase.cs new file mode 100644 index 000000000..9c0d18a80 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/Eventbase.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Newtonsoft.Json; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + public abstract class EventBase : MessageBase + { + [JsonProperty("event")] + public string EventType { get; set; } + + public TBody Body { get; set; } + + internal override string PayloadType + { + get { return this.EventType; } + set { this.EventType = value; } + } + + public EventBase() + { + this.Type = MessageType.Event; + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/ReplPromptChoiceEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/ReplPromptChoiceEvent.cs new file mode 100644 index 000000000..a9d258d93 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/ReplPromptChoiceEvent.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + [MessageTypeName("replPromptChoice")] + public class ReplPromptChoiceEvent : EventBase + { + } + + public class ReplPromptChoiceEventBody + { + public int Seq { get; set; } + + public string Caption { get; set; } + + public string Message { get; set; } + + public ReplPromptChoiceDetails[] Choices { get; set; } + + public int DefaultChoice { get; set; } + } + + public class ReplPromptChoiceDetails + { + public string HelpMessage { get; set; } + + public string Label { get; set; } + + public static ReplPromptChoiceDetails FromChoiceDescription( + ChoiceDetails choiceDetails) + { + return new ReplPromptChoiceDetails + { + Label = choiceDetails.Label, + HelpMessage = choiceDetails.HelpMessage + }; + } + } + +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/ReplWriteOutputEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/ReplWriteOutputEvent.cs new file mode 100644 index 000000000..70c2ed108 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/ReplWriteOutputEvent.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + [MessageTypeName("replWriteOutput")] + public class ReplWriteOutputEvent : EventBase + { + } + + public class ReplWriteOutputEventBody + { + public string LineContents { get; set; } + + public bool IncludeNewLine { get; set; } + + public OutputType LineType { get; set; } + + public ConsoleColor ForegroundColor { get; set; } + + public ConsoleColor BackgroundColor { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/StartedEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/StartedEvent.cs new file mode 100644 index 000000000..6877fef70 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/StartedEvent.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Event +{ + [MessageTypeName("started")] + public class StartedEvent : EventBase + { + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/IMessageProcessor.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/IMessageProcessor.cs new file mode 100644 index 000000000..c9e07befd --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/IMessageProcessor.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + /// + /// Provides an interface for classes that can process an incoming + /// message of a given type. + /// + public interface IMessageProcessor + { + /// + /// Performs some action + /// + /// + /// + void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter); + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageBase.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageBase.cs new file mode 100644 index 000000000..c54d8f05f --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageBase.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + /// + /// Provides the base class for all message types in the + /// standard I/O protocol. + /// + public abstract class MessageBase + { + /// + /// Gets or sets the sequence identifier for this message. + /// + public int Seq { get; set; } + + /// + /// Gets or sets the string identifying the type of this message. + /// + public MessageType Type { get; set; } + + /// + /// Gets or sets the payload type name of this message. Subclasses + /// will use this to generalize access to the property that + /// identifies its protocol payload type. + /// + internal abstract string PayloadType { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageFormat.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageFormat.cs new file mode 100644 index 000000000..600b01ed6 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageFormat.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public enum MessageFormat + { + WithoutContentLength = 0, + WithContentLength + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParseException.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParseException.cs new file mode 100644 index 000000000..a90e61fd3 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParseException.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public class MessageParseException : Exception + { + public string OriginalMessageText { get; private set; } + + public MessageParseException( + string originalMessageText, + string errorMessage, + params object[] errorMessageArgs) + : base(string.Format(errorMessage, errorMessageArgs)) + { + this.OriginalMessageText = originalMessageText; + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs new file mode 100644 index 000000000..477e3a39f --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs @@ -0,0 +1,152 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public class MessageParser + { + #region Private Fields + + private JsonSerializer jsonSerializer = + JsonSerializer.Create( + Constants.JsonSerializerSettings); + + private MessageTypeResolver messageTypeResolver; + + #endregion + + #region Constructors + + public MessageParser(MessageTypeResolver messageTypeResolver) + { + Validate.IsNotNull("messageTypeResolver", messageTypeResolver); + + this.messageTypeResolver = messageTypeResolver; + } + + #endregion + + #region Public Methods + + public MessageBase ParseMessage(string messageJson) + { + string messageTypeName = null; + Type concreteMessageType = null; + MessageType messageType = MessageType.Unknown; + + // Parse the JSON string to a JObject + JObject messageObject = JObject.Parse(messageJson); + + // Get the message type and name from the JSON object + if (!this.TryGetMessageTypeAndName( + messageObject, + out messageType, + out messageTypeName)) + { + throw new MessageParseException( + messageObject.ToString(), + "Unknown message type: {0}", + messageTypeName); + } + + // Look up the message type by name + if (!this.messageTypeResolver.TryGetMessageTypeByName( + messageType, + messageTypeName, + out concreteMessageType)) + { + throw new MessageParseException( + messageObject.ToString(), + "Could not locate message type by name: {0}", + messageTypeName); + } + + // Return the deserialized message + return + (MessageBase)messageObject.ToObject( + concreteMessageType, + this.jsonSerializer); + } + + #endregion + + #region Private Helper Methods + + private bool TryGetMessageTypeAndName( + JObject messageObject, + out MessageType messageType, + out string messageTypeName) + { + messageType = MessageType.Unknown; + messageTypeName = null; + + if (TryGetValueString(messageObject, "type", out messageTypeName)) + { + switch (messageTypeName) + { + case "request": + messageType = MessageType.Request; + return TryGetValueString(messageObject, "command", out messageTypeName); + + case "response": + messageType = MessageType.Response; + return TryGetValueString(messageObject, "command", out messageTypeName); + + case "event": + messageType = MessageType.Event; + return TryGetValueString(messageObject, "event", out messageTypeName); + + default: + return false; + } + } + + return false; + } + + private static bool TryGetValueString(JObject jsonObject, string valueName, out string valueString) + { + valueString = null; + + JToken valueToken = null; + if (jsonObject.TryGetValue(valueName, out valueToken)) + { + JValue realValueToken = valueToken as JValue; + if (realValueToken != null) + { + if (realValueToken.Type == JTokenType.String) + { + valueString = (string)realValueToken.Value; + } + else if (realValueToken.Type == JTokenType.Null) + { + // If the value is null, return it too + valueString = null; + } + else + { + // No other value type is valid + return false; + } + + return true; + } + else + { + // TODO: Trace unexpected condition + } + } + + return false; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageReader.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageReader.cs new file mode 100644 index 000000000..8521e15d6 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageReader.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public class MessageReader + { + #region Private Fields + + private TextReader textReader; + private bool expectsContentLength; + private MessageParser messageParser; + private char[] buffer = new char[8192]; + + #endregion + + #region Constructors + + public MessageReader( + TextReader textReader, + MessageFormat messageFormat, + MessageTypeResolver messageTypeResolver) + { + Validate.IsNotNull("textReader", textReader); + Validate.IsNotNull("messageTypeResolver", messageTypeResolver); + + this.textReader = textReader; + this.messageParser = new MessageParser(messageTypeResolver); + this.expectsContentLength = + messageFormat == MessageFormat.WithContentLength; + } + + #endregion + + #region Public Methods + + public async Task ReadMessage() + { + string messageLine = await this.textReader.ReadLineAsync(); + + // If we're expecting Content-Length lines, check for it + if (this.expectsContentLength) + { + if (messageLine.StartsWith(Constants.ContentLengthString)) + { + int contentLength = -1; + string contentLengthIntString = + messageLine.Substring( + Constants.ContentLengthString.Length); + + // Attempt to parse the Content-Length integer + if (!int.TryParse(contentLengthIntString, out contentLength)) + { + throw new MessageParseException( + messageLine, + "Could not parse integer string provided for Content-Length: {0}", + messageLine); + } + + // Make sure Content-Length isn't + if (contentLength <= 0) + { + throw new MessageParseException( + messageLine, + "Received invalid Content-Length value of {0}", + contentLength); + } + + // Skip the next newline + await this.textReader.ReadAsync(this.buffer, 0, Environment.NewLine.Length); + + // NOTE: At this point, we don't actually use the Content-Length + // count to read the text because the messages coming from the client + // are all on a single line anyway. We may need to revisit this in + // the future. + + // Read the message content + messageLine = await this.textReader.ReadLineAsync(); + } + else + { + throw new MessageParseException( + messageLine, + "Unexpected line found while waiting for Content-Length"); + } + } + + // Return the parsed message + return this.messageParser.ParseMessage(messageLine); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageType.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageType.cs new file mode 100644 index 000000000..21db1b89d --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageType.cs @@ -0,0 +1,30 @@ + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + /// + /// Indentifies the type of a given message. + /// + public enum MessageType + { + /// + /// The message type is unknown. + /// + Unknown = 0, + + /// + /// The message is a request. + /// + Request, + + /// + /// The message is a response. + /// + Response, + + /// + /// The message is an event. + /// + Event + } + +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeNameAttribute.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeNameAttribute.cs new file mode 100644 index 000000000..e20f8cb76 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeNameAttribute.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + /// + /// Marks a type deriving from MessageBase with a name that is + /// used to identify the message's type. This is exposed as the + /// "command" field for Requests and Responses and the "event" + /// field for Events. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class MessageTypeNameAttribute : Attribute + { + /// + /// Gets the message type's name. + /// + public string MessageTypeName { get; private set; } + + /// + /// Creates an instance of the MessageTypeNameAttribute class with + /// the given messageTypeName. + /// + /// The type name for this message class. + public MessageTypeNameAttribute(string messageTypeName) + { + Validate.IsNotNullOrEmptyString("messageTypeName", messageTypeName); + + this.MessageTypeName = messageTypeName; + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeResolver.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeResolver.cs new file mode 100644 index 000000000..8e8bedc2c --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageTypeResolver.cs @@ -0,0 +1,205 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Request; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public class MessageTypeResolver + { + #region Private Fields + + // Cache a HashSet of raw generic message types for quick comparisons + private static readonly HashSet rawGenericMessageTypes = + new HashSet( + new List + { + typeof(RequestBase<>), + typeof(ResponseBase<>), + typeof(EventBase<>) + }); + + // Cache a dictionary of raw generic message types to MessageTypes + private static readonly Dictionary rawGenericTypesToMessageTypes = + new Dictionary + { + { typeof(RequestBase<>), MessageType.Request }, + { typeof(ResponseBase<>), MessageType.Response }, + { typeof(EventBase<>), MessageType.Event }, + }; + + private Dictionary> typeNameToMessageTypeIndex = + new Dictionary>(); + + private Dictionary typeToTypeNameIndex = + new Dictionary(); + + #endregion + + #region Public Methods + + public void ScanForMessageTypes(Assembly sourceAssembly) + { + Validate.IsNotNull("sourceAssembly", sourceAssembly); + + // Find all types deriving from MessageBase + Type messageBaseType = typeof(MessageBase); + IEnumerable messageTypes = + sourceAssembly + .GetTypes() + .Where(t => messageBaseType.IsAssignableFrom(t) && + t.IsAbstract == false); + + foreach (Type concreteMessageType in messageTypes) + { + // Which specific message interface does the type implement? + MessageType messageType = this.GetMessageTypeOfType(concreteMessageType); + + if (messageType != MessageType.Unknown) + { + this.AddConcreteMessageTypeToIndex( + messageType, + concreteMessageType); + } + else + { + // TODO: Trace warning message + } + } + } + + public bool TryGetMessageTypeByName( + MessageType messageType, + string messageTypeName, + out Type concreteMessageType) + { + Validate.IsNotEqual("messageType", messageType, MessageType.Unknown); + Validate.IsNotNullOrEmptyString("messageTypeName", messageTypeName); + + concreteMessageType = null; + + Dictionary messageTypeIndex = null; + + if (this.typeNameToMessageTypeIndex.TryGetValue( + messageType, + out messageTypeIndex)) + { + return + messageTypeIndex.TryGetValue( + messageTypeName, + out concreteMessageType); + } + + return false; + } + + public bool TryGetMessageTypeNameByType( + Type concreteMessageType, + out string messageTypeName) + { + Validate.IsNotNull("concreteMessageType", concreteMessageType); + + messageTypeName = null; + + return + this.typeToTypeNameIndex.TryGetValue( + concreteMessageType, + out messageTypeName); + } + + #endregion + + #region Private Helper Methods + + private MessageType GetMessageTypeOfType(Type typeToCheck) + { + MessageType messageType = MessageType.Unknown; + + // Walk up the inheritance tree to see if the type to check + // derives from the given generic type. Stop if we reach a + // type that inherits directly from MessageBase (which should + // only be the generic base types for Request, Response, and + // Event) + while (typeToCheck != null && + typeToCheck != typeof(MessageBase)) + { + // If the current type is a generic type and it + if (typeToCheck.IsGenericType) + { + // Is the raw generic type one of the message types? + Type rawGenericType = typeToCheck.GetGenericTypeDefinition(); + if (rawGenericMessageTypes.Contains(rawGenericType)) + { + // Find the MessageType corresponding to the generic type + if (!rawGenericTypesToMessageTypes.TryGetValue( + rawGenericType, + out messageType)) + { + // TODO Comment + Debug.Assert(false, "BOO"); + } + + // Return the message type even if the result will + // be Unknown. Searching further is pointless in + // the error condition. + return messageType; + } + } + + // Check the type's parent next + typeToCheck = typeToCheck.BaseType; + } + + return messageType; + } + + private void AddConcreteMessageTypeToIndex( + MessageType messageType, + Type concreteMessageType) + { + // Check for the MessageTypeAttribute + var messageTypeAttribute = + concreteMessageType.GetCustomAttribute(); + + // Assert if the attribute is null + Debug.Assert( + messageTypeAttribute != null, + "Missing MessageTypeAttribute on message type " + concreteMessageType.Name); + + // Try to find the type index for the given message type + Dictionary messageTypeIndex = null; + if (!this.typeNameToMessageTypeIndex.TryGetValue( + messageType, + out messageTypeIndex)) + { + // Create the index for this MessageType and store it + messageTypeIndex = new Dictionary(); + this.typeNameToMessageTypeIndex.Add( + messageType, + messageTypeIndex); + } + + // Store the concrete message type relative to its MessageType + messageTypeIndex.Add( + messageTypeAttribute.MessageTypeName, + concreteMessageType); + + // Store the relative to its concrete type + this.typeToTypeNameIndex.Add( + concreteMessageType, + messageTypeAttribute.MessageTypeName); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs new file mode 100644 index 000000000..443ec77f4 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using Newtonsoft.Json; +using System.IO; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Message +{ + public class MessageWriter + { + #region Private Fields + + private TextWriter textWriter; + private bool includeContentLength; + private MessageTypeResolver messageTypeResolver; + + #endregion + + #region Constructors + + public MessageWriter( + TextWriter textWriter, + MessageFormat messageFormat, + MessageTypeResolver messageTypeResolver) + { + Validate.IsNotNull("textWriter", textWriter); + Validate.IsNotNull("messageTypeResolver", messageTypeResolver); + + this.textWriter = textWriter; + this.messageTypeResolver = messageTypeResolver; + this.includeContentLength = + messageFormat == MessageFormat.WithContentLength; + } + + #endregion + + #region Public Methods + + // TODO: Change back to async? + + public void WriteMessage(MessageBase messageToWrite) + { + Validate.IsNotNull("messageToWrite", messageToWrite); + + string messageTypeName = null; + if (!this.messageTypeResolver.TryGetMessageTypeNameByType( + messageToWrite.GetType(), + out messageTypeName)) + { + // TODO: Trace or throw exception? + } + + // Insert the message's type name before serializing + messageToWrite.PayloadType = messageTypeName; + + // Serialize the message + string serializedMessage = + JsonConvert.SerializeObject( + messageToWrite, + Constants.JsonSerializerSettings); + + // Construct the payload string + string payloadString = serializedMessage + "\r\n"; + + if (this.includeContentLength) + { + payloadString = + string.Format( + "{0}{1}\r\n\r\n{2}", + Constants.ContentLengthString, + Encoding.UTF8.GetByteCount(serializedMessage), + payloadString); + } + + // Send the message + this.textWriter.Write(payloadString); + this.textWriter.Flush(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj b/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj new file mode 100644 index 000000000..7ca31e292 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/PowerShellEditorServices.Transport.Stdio.csproj @@ -0,0 +1,120 @@ + + + + + Debug + AnyCPU + {F8A0946A-5D25-4651-8079-B8D5776916FB} + Library + Properties + Microsoft.PowerShell.EditorServices.Transport.Stdio + Microsoft.PowerShell.EditorServices.Transport.Stdio + v4.5 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.dll + True + + + ..\..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Concurrent.dll + True + + + ..\..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Enlightenment.dll + True + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/PowerShellEditorServices.Transport.Stdio/Properties/AssemblyInfo.cs b/src/PowerShellEditorServices.Transport.Stdio/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9dfb27352 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Transport.Stdio")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Transport.Stdio")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("78caf6c3-5955-4b15-a302-2bd6b7871d5b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ChangeFileRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ChangeFileRequest.cs new file mode 100644 index 000000000..4d21ab0f2 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ChangeFileRequest.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + [MessageTypeName("change")] + public class ChangeFileRequest : FileRequest + { + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + ScriptFile scriptFile = this.GetScriptFile(editorSession); + scriptFile.ApplyChange(this.Arguments.GetFileChangeDetails()); + } + } + + public class FormatRequestArguments : FileLocationRequestArgs + { + // TODO: This class may need to move somewhere else when used by other arg types + + public int EndLine { get; set; } + + public int EndOffset { get; set; } + } + + public class ChangeFileRequestArguments : FormatRequestArguments + { + public string InsertString { get; set; } + + public FileChange GetFileChangeDetails() + { + return new FileChange + { + InsertString = this.InsertString, + Line = this.Line, + Offset = this.Offset, + EndLine = this.EndLine, + EndOffset = this.EndOffset + }; + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/CompletionsRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/CompletionsRequest.cs new file mode 100644 index 000000000..a829ca545 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/CompletionsRequest.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + [MessageTypeName("completions")] + public class CompletionsRequest : FileRequest + { + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + ScriptFile scriptFile = this.GetScriptFile(editorSession); + + CompletionResults completions = + editorSession.LanguageService.GetCompletionsInFile( + scriptFile, + this.Arguments.Line, + this.Arguments.Offset); + + messageWriter.WriteMessage( + this.PrepareResponse( + CompletionsResponse.Create( + completions))); + } + } + + public class CompletionsRequestArgs : FileLocationRequestArgs + { + public string Prefix { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs new file mode 100644 index 000000000..db6eb197c --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + [MessageTypeName("geterr")] + public class ErrorRequest : RequestBase + { + public static ErrorRequest Create(params string[] filePaths) + { + return new ErrorRequest + { + Arguments = new ErrorRequestArguments + { + Files = filePaths + } + }; + } + + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + List fileList = new List(); + + // Get the requested files + foreach (string filePath in this.Arguments.Files) + { + ScriptFile scriptFile = null; + + if (!editorSession.TryGetFile(filePath, out scriptFile)) + { + // Skip this file and log the file load error + // TODO: Trace out the error message + continue; + } + + var semanticMarkers = + editorSession.AnalysisService.GetSemanticMarkers( + scriptFile); + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + messageWriter.WriteMessage( + SyntaxDiagnosticEvent.Create( + scriptFile.FilePath, + scriptFile.SyntaxMarkers)); + + messageWriter.WriteMessage( + SemanticDiagnosticEvent.Create( + scriptFile.FilePath, + semanticMarkers)); + } + } + } + + public class ErrorRequestArguments + { + public string[] Files { get; set; } + + public int Delay { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs new file mode 100644 index 000000000..f2b31c650 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using System.IO; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + public abstract class FileRequest : RequestBase + where TArguments : FileRequestArguments + { + protected ScriptFile GetScriptFile(EditorSession editorSession) + { + ScriptFile scriptFile = null; + + if(!editorSession.TryGetFile( + this.Arguments.File, + out scriptFile)) + { + // TODO: Throw an exception that the message loop can create a response out of + + throw new FileNotFoundException( + "A ScriptFile with the following path was not found in the EditorSession: {0}", + this.Arguments.File); + } + + return scriptFile; + } + } + + public class FileRequestArguments + { + public string File { get; set; } + } + + public class FileLocationRequestArgs : FileRequestArguments + { + public int Line { get; set; } + + public int Offset { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs new file mode 100644 index 000000000..98a40552d --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs @@ -0,0 +1,33 @@ +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + [MessageTypeName("open")] + public class OpenFileRequest : RequestBase + { + public static OpenFileRequest Create(string filePath) + { + return new OpenFileRequest + { + Arguments = new FileRequestArguments + { + File = filePath + } + }; + } + + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + // Open the file in the current session + editorSession.OpenFile(this.Arguments.File); + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ReplExecuteRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ReplExecuteRequest.cs new file mode 100644 index 000000000..ec589049c --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ReplExecuteRequest.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + [MessageTypeName("replExecute")] + public class ReplExecuteRequest : RequestBase + { + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + editorSession.ConsoleService.ExecuteCommand( + this.Arguments.CommandString); + } + } + + public class ReplExecuteArgs + { + public string CommandString { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/RequestBase.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/RequestBase.cs new file mode 100644 index 000000000..96f96dd82 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/RequestBase.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request +{ + public abstract class RequestBase : MessageBase, IMessageProcessor + { + public string Command { get; set; } + + public TArgs Arguments { get; set; } + + internal override string PayloadType + { + get { return this.Command; } + set { this.Command = value; } + } + + public abstract void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter); + + public RequestBase() + { + this.Type = MessageType.Request; + } + + protected ResponseBase PrepareResponse( + ResponseBase response, + bool isSuccess = true) + { + response.RequestSeq = this.Seq; + response.Success = true; + + return response; + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/CompletionsResponse.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/CompletionsResponse.cs new file mode 100644 index 000000000..2aa9810df --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/CompletionsResponse.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Response +{ + [MessageTypeName("completions")] + public class CompletionsResponse : ResponseBase + { + public static CompletionsResponse Create(CompletionResults completionResults) + { + List completionResult = new List(); + + foreach (var completion in completionResults.Completions) + { + completionResult.Add( + new CompletionEntry + { + Name = completion.CompletionText, + Kind = GetCompletionKind(completion.CompletionType), + }); + } + + return new CompletionsResponse + { + Body = completionResult.ToArray() + }; + } + + private static string GetCompletionKind(CompletionType completionType) + { + switch (completionType) + { + case CompletionType.Command: + case CompletionType.Method: + return "method"; + default: + // TODO: Better default + return "variable"; + } + } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/MessageErrorResponse.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/MessageErrorResponse.cs new file mode 100644 index 000000000..adc301e52 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/MessageErrorResponse.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Response +{ + [MessageTypeName("messageHandlingError")] + public class MessageErrorResponse : ResponseBase + { + private MessageErrorResponse() + { + // This class always returns an error + this.Success = false; + } + + public static MessageErrorResponse CreateUnhandledMessageResponse( + MessageBase unhandledMessage) + { + return new MessageErrorResponse + { + RequestSeq = unhandledMessage.Seq, + Body = new MessageErrorResponseDetails + { + ErrorMessage = "A message was not able to be handled by the service.", + MessageType = unhandledMessage.Type, + PayloadType = unhandledMessage.PayloadType + } + }; + } + + public static MessageErrorResponse CreateParseErrorResponse( + MessageParseException parseException) + { + return new MessageErrorResponse + { + Body = new MessageErrorResponseDetails + { + ErrorMessage = + string.Format( + "A message was not able to be parsed by the service: {0}", + parseException.OriginalMessageText), + MessageType = MessageType.Unknown, + PayloadType = "unknown" + } + }; + } + } + + public class MessageErrorResponseDetails + { + public string ErrorMessage { get; set; } + + public MessageType MessageType { get; set; } + + public string PayloadType { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/ReplPromptChoiceResponse.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/ReplPromptChoiceResponse.cs new file mode 100644 index 000000000..26f18dd59 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/ReplPromptChoiceResponse.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Response +{ + [MessageTypeName("replPromptChoice")] + public class ReplPromptChoiceResponse : ResponseBase, IMessageProcessor + { + public void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + editorSession.ConsoleService.ReceiveChoicePromptResult( + 0, // TODO: Need to pass prompt ID! + this.Body.Choice); + } + } + + public class ReplPromptChoiceResponseBody + { + public int Choice { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/ResponseBase.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/ResponseBase.cs new file mode 100644 index 000000000..bcb4c5f29 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/ResponseBase.cs @@ -0,0 +1,104 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Newtonsoft.Json; +using System.Text.RegularExpressions; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Response +{ + public abstract class ResponseBase : MessageBase + { + [JsonProperty("request_seq")] + public int RequestSeq { get; set; } + + public bool Success { get; set; } + + public string Command { get; set; } + + public string Message { get; set; } + + public TBody Body { get; set; } + + internal override string PayloadType + { + get { return this.Command; } + set { this.Command = value; } + } + + public ResponseBase() + { + this.Type = MessageType.Response; + } + } + + public class CompletionEntry + { + public string Name { get; set; } + + public string Kind { get; set; } + + public string KindModifiers { get; set; } + + public string SortText { get; set; } + } + + public class CompletionEntryDetails + { + public CompletionEntryDetails(CompletionDetails completionDetails, string entryName) + { + Kind = null; + KindModifiers = null; + DisplayParts = null; + Documentation = null; + DocString = null; + + // if the result type is a command return null + if (!(completionDetails.CompletionType.Equals(CompletionType.Command))) + { + //find matches on square brackets in the the tool tip + var matches = Regex.Matches(completionDetails.ToolTipText, @"^\[(.+)\]"); + string strippedEntryName = Regex.Replace(entryName, @"^[$_-]","").Replace("{","").Replace("}",""); + + if (matches.Count > 0 && matches[0].Groups.Count > 1) + { + Name = matches[0].Groups[1].Value; + } + // if there are nobracets and the only content is the completion name + else if (completionDetails.ToolTipText.Equals(strippedEntryName)) + { + Name = null; + } + else + { + Name = null; + DocString = completionDetails.ToolTipText; + } + } + + else { Name = null; } + } + public string Name { get; set; } + + public string Kind { get; set; } + + public string KindModifiers { get; set; } + + public SymbolDisplayPart[] DisplayParts { get; set; } + + public SymbolDisplayPart[] Documentation { get; set; } + + public string DocString { get; set; } + + } + + public class SymbolDisplayPart + { + public string Text { get; set; } + + public string Kind { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/StdioConsoleHost.cs b/src/PowerShellEditorServices.Transport.Stdio/StdioConsoleHost.cs new file mode 100644 index 000000000..5cebb0d2b --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/StdioConsoleHost.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio +{ + public class StdioConsoleHost : IConsoleHost + { + #region Private Fields + + private MessageWriter messageWriter; + private int currentReplEventSequence = 0; + private TaskCompletionSource currentPromptChoiceTask; + + #endregion + + #region Constructors + + public StdioConsoleHost(MessageWriter messageWriter) + { + Validate.IsNotNull("messageWriter", messageWriter); + + this.messageWriter = messageWriter; + } + + #endregion + + #region IConsoleHost Implementation + + void IConsoleHost.WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + this.messageWriter.WriteMessage( + new ReplWriteOutputEvent + { + Body = new ReplWriteOutputEventBody + { + LineContents = outputString, + LineType = outputType, + IncludeNewLine = includeNewLine, + ForegroundColor = foregroundColor, + BackgroundColor = backgroundColor + } + }); + } + + Task IConsoleHost.PromptForChoice( + string caption, + string message, + IEnumerable choices, + int defaultChoice) + { + // Create and store a TaskCompletionSource that will be + // used to send the user's response back to the caller + this.currentPromptChoiceTask = new TaskCompletionSource(); + this.currentReplEventSequence++; + + this.messageWriter.WriteMessage( + new ReplPromptChoiceEvent + { + Body = new ReplPromptChoiceEventBody + { + Seq = this.currentReplEventSequence, + Caption = caption, + Message = message, + DefaultChoice = defaultChoice, + Choices = + choices + .Select(ReplPromptChoiceDetails.FromChoiceDescription) + .ToArray() + } + }); + + return this.currentPromptChoiceTask.Task; + } + + void IConsoleHost.PromptForChoiceResult( + int promptId, + int choiceResult) + { + // TODO: Validate that prompt ID exists + Validate.IsNotNull("currentPromptChoiceTask", this.currentPromptChoiceTask); + + this.currentPromptChoiceTask.SetResult(choiceResult); + this.currentPromptChoiceTask = null; + } + + void IConsoleHost.UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + // TODO: Implement message for this + } + + void IConsoleHost.ExitSession(int exitCode) + { + // TODO: Implement message for this + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs b/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs new file mode 100644 index 000000000..bc92f92ac --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs @@ -0,0 +1,218 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; +using Nito.AsyncEx; +using System; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Transport.Stdio +{ + public class StdioHost : IHost + { + #region Private Fields + + private IConsoleHost consoleHost; + private EditorSession editorSession; + private SynchronizationContext syncContext; + private AsyncContextThread messageLoopThread; + + #endregion + + #region IHost Implementation + + string IHost.Name + { + get { throw new NotImplementedException(); } + } + + Version IHost.Version + { + get { throw new NotImplementedException(); } + } + + void IHost.Start() + { + // Start a new EditorSession + // TODO: Allow multiple sessions? + this.editorSession = new EditorSession(); + + // Start the main message loop + AsyncContext.Run((Func)this.StartMessageLoop); + } + + #endregion + + #region Private Methods + + private async Task StartMessageLoop() + { + // Hold on to the current synchronization context + this.syncContext = SynchronizationContext.Current; + + // Start the message listener on another thread + this.messageLoopThread = new AsyncContextThread(true); + await this.messageLoopThread.Factory.Run(() => this.ListenForMessages()); + } + + //private async Task ListenForMessages() + //{ + // // Ensure that the console is using UTF-8 encoding + // System.Console.InputEncoding = Encoding.UTF8; + // System.Console.OutputEncoding = Encoding.UTF8; + + // // Set up the reader and writer + // MessageReader messageReader = + // new MessageReader( + // System.Console.In, + // MessageFormat.WithoutContentLength); + + // MessageWriter messageWriter = + // new MessageWriter( + // System.Console.Out, + // MessageFormat.WithContentLength); + + // this.ConsoleHost = new StdioConsoleHost(messageWriter); + + // // Set up the PowerShell session + // // TODO: Do this elsewhere + // EditorSession editorSession = new EditorSession(); + // editorSession.StartSession(this.ConsoleHost); + + // // Send a "started" event + // messageWriter.WriteMessage( + // new Event + // { + // EventType = "started" + // }); + + // // Run the message loop + // bool isRunning = true; + // while(isRunning) + // { + // // Read a message + // Message newMessage = await messageReader.ReadMessage(); + + // // Is the message a request? + // IMessageProcessor messageProcessor = newMessage as IMessageProcessor; + // if (messageProcessor != null) + // { + // // Process the request on the host thread + // messageProcessor.ProcessMessage( + // editorSession, + // messageWriter); + // } + // else + // { + // if (newMessage != null) + // { + // // Return an error response to keep the client moving + // messageWriter.WriteMessage( + // new Response + // { + // Command = request != null ? request.Command : string.Empty, + // RequestSeq = newMessage.Seq, + // Success = false, + // }); + // } + // } + // } + //} + async Task ListenForMessages() + { + // Ensure that the console is using UTF-8 encoding + System.Console.InputEncoding = Encoding.UTF8; + System.Console.OutputEncoding = Encoding.UTF8; + + // Find all message types in this assembly + MessageTypeResolver messageTypeResolver = new MessageTypeResolver(); + messageTypeResolver.ScanForMessageTypes(Assembly.GetExecutingAssembly()); + + // Set up the reader and writer + MessageReader messageReader = + new MessageReader( + System.Console.In, + MessageFormat.WithoutContentLength, + messageTypeResolver); + + MessageWriter messageWriter = + new MessageWriter( + System.Console.Out, + MessageFormat.WithContentLength, + messageTypeResolver); + + // Set up the console host which will send events + // through the MessageWriter + this.consoleHost = new StdioConsoleHost(messageWriter); + + // Set up the PowerShell session + // TODO: Do this elsewhere + EditorSession editorSession = new EditorSession(); + editorSession.StartSession(this.consoleHost); + + // Send a "started" event + messageWriter.WriteMessage( + new StartedEvent()); + + // Run the message loop + bool isRunning = true; + while (isRunning) + { + MessageBase newMessage = null; + + try + { + // Read a message from stdin + newMessage = await messageReader.ReadMessage(); + } + catch (MessageParseException e) + { + // Write an error response + messageWriter.WriteMessage( + MessageErrorResponse.CreateParseErrorResponse(e)); + + // Continue the loop + continue; + } + + // Is the message a request? + IMessageProcessor messageProcessor = newMessage as IMessageProcessor; + if (messageProcessor != null) + { + // Process the message. The processor will take care + // of writing responses throguh the messageWriter. + messageProcessor.ProcessMessage( + editorSession, + messageWriter); + } + else + { + if (newMessage != null) + { + // Return an error response to keep the client moving + messageWriter.WriteMessage( + MessageErrorResponse.CreateUnhandledMessageResponse( + newMessage)); + } + else + { + // TODO: Some other problem must have occurred, + // design a message response for this case. + } + } + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Transport.Stdio/packages.config b/src/PowerShellEditorServices.Transport.Stdio/packages.config new file mode 100644 index 000000000..b658f0b82 --- /dev/null +++ b/src/PowerShellEditorServices.Transport.Stdio/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs b/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs new file mode 100644 index 000000000..4503ea8f6 --- /dev/null +++ b/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Windows.PowerShell.ScriptAnalyzer; +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Analysis +{ + /// + /// Provides an implementation of ScriptAnalyzer's IOutputWriter + /// interface that writes to trace logs. + /// + internal class AnalysisOutputWriter : IOutputWriter + { + #region IOutputWriter Implementation + + void IOutputWriter.WriteError(ErrorRecord error) + { + // TODO: Find a way to trace out this output! + } + + void IOutputWriter.WriteWarning(string message) + { + // TODO: Find a way to trace out this output! + } + + void IOutputWriter.WriteVerbose(string message) + { + // TODO: Find a way to trace out this output! + } + + void IOutputWriter.WriteDebug(string message) + { + // TODO: Find a way to trace out this output! + } + + void IOutputWriter.ThrowTerminatingError(ErrorRecord record) + { + // TODO: Find a way to trace out this output! + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs new file mode 100644 index 000000000..181360154 --- /dev/null +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.Windows.PowerShell.ScriptAnalyzer; +using System.Linq; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Analysis +{ + /// + /// Provides a high-level service for performing semantic analysis + /// of PowerShell scripts. + /// + public class AnalysisService + { + #region Private Fields + + private Runspace runspace; + private ScriptAnalyzer scriptAnalyzer; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the AnalysisService class with a + /// Runspace to use for analysis operations. + /// + /// + /// The Runspace in which analysis operations will be performed. + /// + public AnalysisService(Runspace analysisRunspace) + { + this.runspace = analysisRunspace; + this.scriptAnalyzer = new ScriptAnalyzer(); + this.scriptAnalyzer.Initialize( + analysisRunspace, + new AnalysisOutputWriter()); + } + + #endregion + + #region Public Methods + + /// + /// Performs semantic analysis on the given ScriptFile and returns + /// an array of ScriptFileMarkers. + /// + /// The ScriptFile which will be analyzed for semantic markers. + /// An array of ScriptFileMarkers containing semantic analysis results. + public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file) + { + // TODO: This is a temporary fix until we can change how + // ScriptAnalyzer invokes their async tasks. + Task analysisTask = + Task.Factory.StartNew( + () => + { + return + this.scriptAnalyzer + .AnalyzeSyntaxTree( + file.ScriptAst, + file.ScriptTokens, + file.FilePath) + .Select(ScriptFileMarker.FromDiagnosticRecord) + .ToArray(); + }, + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + + return analysisTask.Result; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/ChoiceDetails.cs b/src/PowerShellEditorServices/Console/ChoiceDetails.cs new file mode 100644 index 000000000..f3971d5a5 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ChoiceDetails.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details about a choice that should be displayed + /// to the user. This class is meant to be serializable to the + /// user's UI. + /// + public class ChoiceDetails + { + #region Properties + + /// + /// Gets or sets the label for the choice. + /// + public string Label { get; set; } + + /// + /// Gets or sets the help string that describes the choice. + /// + public string HelpMessage { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ChoicePromptDetails class + /// based on a ChoiceDescription from the PowerShell layer. + /// + /// + /// A ChoiceDescription on which this instance will be based. + /// + /// A new ChoicePromptDetails instance. + public static ChoiceDetails Create(ChoiceDescription choiceDescription) + { + return new ChoiceDetails + { + Label = choiceDescription.Label, + HelpMessage = choiceDescription.HelpMessage + }; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleService.cs b/src/PowerShellEditorServices/Console/ConsoleService.cs new file mode 100644 index 000000000..6384963d4 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleService.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + /// + /// Provides a high-level service for managing an active + /// interactive console session. + /// + public class ConsoleService : IDisposable + { + #region Private Fields + + private IConsoleHost consoleHost; + private Runspace currentRunspace; + private InitialSessionState initialSessionState; + private ConsoleServicePSHost consoleServicePSHost; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleService class using the + /// given IConsoleHost implementation to invoke host operations + /// on behalf of the ConsolePSHost. An InitialSessionState may + /// be provided to create the console runspace using a particular + /// configuraiton. + /// + /// + /// An IConsoleHost implementation which handles host operations. + /// + /// + /// An optional InitialSessionState to use in creating the console runspace. + /// + public ConsoleService( + IConsoleHost consoleHost, + InitialSessionState initialSessionState = null) + { + Validate.IsNotNull("consoleHost", consoleHost); + + // If no InitialSessionState is provided, create one from defaults + this.initialSessionState = initialSessionState; + if (this.initialSessionState == null) + { + this.initialSessionState = InitialSessionState.CreateDefault2(); + } + + this.consoleHost = consoleHost; + this.consoleServicePSHost = new ConsoleServicePSHost(consoleHost); + + this.currentRunspace = RunspaceFactory.CreateRunspace(consoleServicePSHost, this.initialSessionState); + this.currentRunspace.ApartmentState = ApartmentState.STA; + this.currentRunspace.ThreadOptions = PSThreadOptions.ReuseThread; + this.currentRunspace.Open(); + } + + #endregion + + #region Public Methods + + /// + /// Executes a command in the console runspace. + /// + /// The command string to execute. + /// A Task that can be awaited for the command completion. + public async Task ExecuteCommand(string commandString) + { + PowerShell powerShell = PowerShell.Create(); + + try + { + // Set the runspace + powerShell.Runspace = this.currentRunspace; + + // Add the command to the pipeline + powerShell.AddScript(commandString); + + // Instruct PowerShell to send output and errors to the host + powerShell.Commands.Commands[0].MergeMyResults( + PipelineResultTypes.Error, + PipelineResultTypes.Output); + powerShell.AddCommand("out-default"); + + // Invoke the pipeline on a background thread + await Task.Factory.StartNew( + () => + { + + var output = powerShell.Invoke(); + var count = output.Count; + }, + CancellationToken.None, // Might need a cancellation token + TaskCreationOptions.None, + TaskScheduler.Default + ); + } + catch (RuntimeException e) + { + // TODO: Return an error + string boo = e.Message; + } + finally + { + if (powerShell != null) + { + powerShell.Dispose(); + } + } + } + + /// + /// Sends a user's prompt choice response back to the specified prompt ID. + /// + /// + /// The ID of the prompt to which the user is responding. + /// + /// + /// The index of the choice that the user selected. + /// + public void ReceiveChoicePromptResult( + int promptId, + int choiceResult) + { + // TODO: Any validation or error handling? + this.consoleHost.PromptForChoiceResult(promptId, choiceResult); + } + + /// + /// Sends a CTRL+C signal to the console to halt execution of + /// the current command. + /// + public void SendControlC() + { + // TODO: Cancel the current pipeline execution + } + + #endregion + + #region IDisposable Implementation + + /// + /// Disposes the runspace in use by the ConsoleService. + /// + public void Dispose() + { + if (this.currentRunspace != null) + { + this.currentRunspace.Dispose(); + this.currentRunspace = null; + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleServicePSHost.cs b/src/PowerShellEditorServices/Console/ConsoleServicePSHost.cs new file mode 100644 index 000000000..0b9de0b2f --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleServicePSHost.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides an implementation of the PSHost class for the + /// ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + internal class ConsoleServicePSHost : PSHost + { + #region Private Fields + + private IConsoleHost consoleHost; + private Guid instanceId = Guid.NewGuid(); + private ConsoleServicePSHostUserInterface hostUserInterface; + + #endregion + + #region Constructors + /// + /// Creates a new instance of the ConsoleServicePSHost class + /// with the given IConsoleHost implementation. + /// + /// + /// The IConsoleHost that will be used to perform host actions for this class. + /// + public ConsoleServicePSHost(IConsoleHost consoleHost) + { + this.consoleHost = consoleHost; + this.hostUserInterface = new ConsoleServicePSHostUserInterface(consoleHost); + } + + #endregion + + #region PSHost Implementation + + public override Guid InstanceId + { + get { return this.instanceId; } + } + + public override string Name + { + // TODO: Change this based on proper naming! + get { return "PowerShell Editor Services"; } + } + + public override Version Version + { + // TODO: Pull this from the host application + get { return new Version("0.1.0"); } + } + + // TODO: Pull these from IConsoleHost + + public override System.Globalization.CultureInfo CurrentCulture + { + get { return System.Threading.Thread.CurrentThread.CurrentCulture; } + } + + public override System.Globalization.CultureInfo CurrentUICulture + { + get { return System.Threading.Thread.CurrentThread.CurrentUICulture; } + } + + public override PSHostUserInterface UI + { + get { return this.hostUserInterface; } + } + + public override void EnterNestedPrompt() + { + throw new NotImplementedException(); + } + + public override void ExitNestedPrompt() + { + throw new NotImplementedException(); + } + + public override void NotifyBeginApplication() + { + throw new NotImplementedException(); + } + + public override void NotifyEndApplication() + { + throw new NotImplementedException(); + } + + public override void SetShouldExit(int exitCode) + { + this.consoleHost.ExitSession(exitCode); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleServicePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Console/ConsoleServicePSHostRawUserInterface.cs new file mode 100644 index 000000000..0bbc6d8d2 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleServicePSHostRawUserInterface.cs @@ -0,0 +1,241 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides an implementation of the PSHostRawUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + internal class ConsoleServicePSHostRawUserInterface : PSHostRawUserInterface + { + #region Private Fields + + private IConsoleHost consoleHost; + private Size currentBufferSize = new Size(80, 100); + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// + /// The IConsoleHost that will be used to perform host actions for this class. + /// + public ConsoleServicePSHostRawUserInterface(IConsoleHost consoleHost) + { + this.consoleHost = consoleHost; + this.ForegroundColor = ConsoleColor.White; + this.BackgroundColor = ConsoleColor.Black; + } + + #endregion + + #region PSHostRawUserInterface Implementation + + /// + /// Gets or sets the background color of the console. + /// + public override ConsoleColor BackgroundColor + { + get; + set; + } + + /// + /// Gets or sets the foreground color of the console. + /// + public override ConsoleColor ForegroundColor + { + get; + set; + } + + /// + /// Gets or sets the size of the console buffer. + /// + public override Size BufferSize + { + get + { + return this.currentBufferSize; + } + set + { + this.currentBufferSize = value; + } + } + + /// + /// Gets or sets the cursor's position in the console buffer. + /// + public override Coordinates CursorPosition + { + get + { + throw new System.NotImplementedException(); + } + set + { + throw new System.NotImplementedException(); + } + } + + /// + /// Gets or sets the size of the cursor in the console buffer. + /// + public override int CursorSize + { + get + { + throw new System.NotImplementedException(); + } + set + { + throw new System.NotImplementedException(); + } + } + + /// + /// Gets or sets the position of the console's window. + /// + public override Coordinates WindowPosition + { + get + { + throw new System.NotImplementedException(); + } + set + { + throw new System.NotImplementedException(); + } + } + + /// + /// Gets or sets the size of the console's window. + /// + public override Size WindowSize + { + get + { + throw new System.NotImplementedException(); + } + set + { + throw new System.NotImplementedException(); + } + } + + /// + /// Gets or sets the console window's title. + /// + public override string WindowTitle + { + get; + set; + } + + /// + /// Gets a boolean that determines whether a keypress is available. + /// + public override bool KeyAvailable + { + get { throw new System.NotImplementedException(); } + } + + /// + /// Gets the maximum physical size of the console window. + /// + public override Size MaxPhysicalWindowSize + { + get { throw new System.NotImplementedException(); } + } + + /// + /// Gets the maximum size of the console window. + /// + public override Size MaxWindowSize + { + get { throw new System.NotImplementedException(); } + } + + /// + /// Reads the current key pressed in the console. + /// + /// Options for reading the current keypress. + /// A KeyInfo struct with details about the current keypress. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new System.NotImplementedException(); + } + + /// + /// Flushes the current input buffer. + /// + public override void FlushInputBuffer() + { + throw new System.NotImplementedException(); + } + + /// + /// Gets the contents of the console buffer in a rectangular area. + /// + /// The rectangle inside which buffer contents will be accessed. + /// A BufferCell array with the requested buffer contents. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new System.NotImplementedException(); + } + + /// + /// Scrolls the contents of the console buffer. + /// + /// The source rectangle to scroll. + /// The destination coordinates by which to scroll. + /// The rectangle inside which the scrolling will be clipped. + /// The cell with which the buffer will be filled. + public override void ScrollBufferContents( + Rectangle source, + Coordinates destination, + Rectangle clip, + BufferCell fill) + { + throw new System.NotImplementedException(); + } + + /// + /// Sets the contents of the buffer inside the specified rectangle. + /// + /// The rectangle inside which buffer contents will be filled. + /// The BufferCell which will be used to fill the requested space. + public override void SetBufferContents( + Rectangle rectangle, + BufferCell fill) + { + throw new System.NotImplementedException(); + } + + /// + /// Sets the contents of the buffer at the given coordinate. + /// + /// The coordinate at which the buffer will be changed. + /// The new contents for the buffer at the given coordinate. + public override void SetBufferContents( + Coordinates origin, + BufferCell[,] contents) + { + throw new System.NotImplementedException(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/ConsoleServicePSHostUserInterface.cs b/src/PowerShellEditorServices/Console/ConsoleServicePSHostUserInterface.cs new file mode 100644 index 000000000..354e633a9 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ConsoleServicePSHostUserInterface.cs @@ -0,0 +1,202 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Linq; +using System.Security; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides an implementation of the PSHostUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + internal class ConsoleServicePSHostUserInterface : PSHostUserInterface + { + #region Private Fields + + private IConsoleHost consoleHost; + private ConsoleServicePSHostRawUserInterface rawUserInterface; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// + /// The IConsoleHost that will be used to perform host actions for this class. + /// + public ConsoleServicePSHostUserInterface(IConsoleHost consoleHost) + { + this.consoleHost = consoleHost; + this.rawUserInterface = new ConsoleServicePSHostRawUserInterface(consoleHost); + } + + #endregion + + #region PSHostUserInterface Implementation + + public override Dictionary Prompt( + string caption, + string message, + Collection descriptions) + { + throw new NotImplementedException(); + } + + public override int PromptForChoice( + string promptCaption, + string promptMessage, + Collection choiceDescriptions, + int defaultChoice) + { + Task promptTask = + this.consoleHost + .PromptForChoice( + promptCaption, + promptMessage, + choiceDescriptions.Select(ChoiceDetails.Create), + defaultChoice); + + // This will synchronously block on the async PromptForChoice + // method (which ultimately gets run on another thread) and + // then returns the result of the method. + int choiceResult = promptTask.Result; + + // Check for errors + if (promptTask.Status == TaskStatus.Faulted) + { + // Rethrow the exception + throw new Exception( + "PromptForChoice failed, check inner exception for details", + promptTask.Exception); + } + + // Return the result + return choiceResult; + } + + public override PSCredential PromptForCredential( + string caption, + string message, + string userName, + string targetName, + PSCredentialTypes allowedCredentialTypes, + PSCredentialUIOptions options) + { + throw new NotImplementedException(); + } + + public override PSCredential PromptForCredential( + string caption, + string message, + string userName, + string targetName) + { + throw new NotImplementedException(); + } + + public override PSHostRawUserInterface RawUI + { + get { return this.rawUserInterface; } + } + + public override string ReadLine() + { + throw new NotImplementedException(); + } + + public override SecureString ReadLineAsSecureString() + { + throw new NotImplementedException(); + } + + public override void Write( + ConsoleColor foregroundColor, + ConsoleColor backgroundColor, + string value) + { + this.consoleHost.WriteOutput( + value, + false, + OutputType.Normal, + foregroundColor, + backgroundColor); + } + + public override void Write(string value) + { + this.consoleHost.WriteOutput( + value, + false, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + public override void WriteLine(string value) + { + this.consoleHost.WriteOutput( + value, + true, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + public override void WriteDebugLine(string message) + { + this.consoleHost.WriteOutput( + message, + true, + OutputType.Debug); + } + + public override void WriteVerboseLine(string message) + { + this.consoleHost.WriteOutput( + message, + true, + OutputType.Verbose); + } + + public override void WriteWarningLine(string message) + { + this.consoleHost.WriteOutput( + message, + true, + OutputType.Warning); + } + + public override void WriteErrorLine(string value) + { + this.consoleHost.WriteOutput( + value, + true, + OutputType.Error, + ConsoleColor.Red); + } + + public override void WriteProgress( + long sourceId, + ProgressRecord record) + { + this.consoleHost.UpdateProgress( + sourceId, + ProgressDetails.Create(record)); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Console/IConsoleHost.cs b/src/PowerShellEditorServices/Console/IConsoleHost.cs new file mode 100644 index 000000000..aebf894ea --- /dev/null +++ b/src/PowerShellEditorServices/Console/IConsoleHost.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a simplified interface for implementing a PowerShell + /// host that will be used for an interactive console. + /// + public interface IConsoleHost + { + /// + /// Writes output of the given type to the user interface with + /// the given foreground and background colors. Also includes + /// a newline if requested. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + /// + /// Specifies the type of output to be written. + /// + /// + /// Specifies the foreground color of the output to be written. + /// + /// + /// Specifies the background color of the output to be written. + /// + void WriteOutput( + string outputString, + bool includeNewLine = true, + OutputType outputType = OutputType.Normal, + ConsoleColor foregroundColor = ConsoleColor.White, + ConsoleColor backgroundColor = ConsoleColor.Black); + + /// + /// Prompts the user to make a choice using the provided details. + /// + /// + /// The caption string which will be displayed to the user. + /// + /// + /// The descriptive message which will be displayed to the user. + /// + /// + /// The list of choices from which the user will select. + /// + /// + /// The default choice to highlight for the user. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's choice. + /// + Task PromptForChoice( + string promptCaption, + string promptMessage, + IEnumerable choices, + int defaultChoice); + + // TODO: Get rid of this method! Leaky abstraction. + + /// + /// Sends a user's prompt choice response back to the specified prompt ID. + /// + /// + /// The ID of the prompt to which the user is responding. + /// + /// + /// The index of the choice that the user selected. + /// + void PromptForChoiceResult( + int promptId, + int choiceResult); + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + void UpdateProgress( + long sourceId, + ProgressDetails progressDetails); + + /// + /// Notifies the IConsoleHost implementation that the PowerShell + /// session is exiting. + /// + /// The error code that identifies the session exit result. + void ExitSession(int exitCode); + } +} diff --git a/src/PowerShellEditorServices/Console/OutputType.cs b/src/PowerShellEditorServices/Console/OutputType.cs new file mode 100644 index 000000000..ec6cabc43 --- /dev/null +++ b/src/PowerShellEditorServices/Console/OutputType.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Enumerates the types of output lines that will be sent + /// to an IConsoleHost implementation. + /// + public enum OutputType + { + /// + /// A normal output line, usually written with the or Write-Host or + /// Write-Output cmdlets. + /// + Normal, + + /// + /// A debug output line, written with the Write-Debug cmdlet. + /// + Debug, + + /// + /// A verbose output line, written with the Write-Verbose cmdlet. + /// + Verbose, + + /// + /// A warning output line, written with the Write-Warning cmdlet. + /// + Warning, + + /// + /// An error output line, written with the Write-Error cmdlet or + /// as a result of some error during PowerShell pipeline execution. + /// + Error + } +} diff --git a/src/PowerShellEditorServices/Console/ProgressDetails.cs b/src/PowerShellEditorServices/Console/ProgressDetails.cs new file mode 100644 index 000000000..21b008d13 --- /dev/null +++ b/src/PowerShellEditorServices/Console/ProgressDetails.cs @@ -0,0 +1,27 @@ +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides details about the progress of a particular activity. + /// + public class ProgressDetails + { + /// + /// Gets the percentage of the activity that has been completed. + /// + public int PercentComplete { get; private set; } + + internal static ProgressDetails Create(ProgressRecord progressRecord) + { + //progressRecord.RecordType == ProgressRecordType.Completed; + //progressRecord.Activity; + //progressRecord. + + return new ProgressDetails + { + PercentComplete = progressRecord.PercentComplete + }; + } + } +} diff --git a/src/PowerShellEditorServices/Console/SynchronizingConsoleHostWrapper.cs b/src/PowerShellEditorServices/Console/SynchronizingConsoleHostWrapper.cs new file mode 100644 index 000000000..c4e6b1b5d --- /dev/null +++ b/src/PowerShellEditorServices/Console/SynchronizingConsoleHostWrapper.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Wraps an existing IConsoleHost implementation so that all + /// interface methods are dispatched through the provided + /// SynchronizationContext. This is primarily useful for + /// simplifying UI applications who write their own IConsoleHost + /// implementation. + /// + public class SynchronizingConsoleHostWrapper : IConsoleHost + { + #region Private Fields + + private IConsoleHost wrappedConsoleHost; + private SynchronizationContext syncContext; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the SynchronizingConsoleHostWrapper + /// class that wraps the given IConsoleHost implementation and + /// invokes its calls through the given SynchronizationContext. + /// + /// + /// The IConsoleHost implementation that will be wrapped. + /// + /// + /// The SynchronizationContext which will be used for invoking + /// host operations calls on the proper thread. + /// + public SynchronizingConsoleHostWrapper( + IConsoleHost wrappedConsoleHost, + SynchronizationContext syncContext) + { + Validate.IsNotNull("wrappedConsoleHost", wrappedConsoleHost); + Validate.IsNotNull("syncContext", syncContext); + + this.wrappedConsoleHost = wrappedConsoleHost; + this.syncContext = syncContext; + } + + #endregion + + #region IConsoleHost Implementation + + void IConsoleHost.WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + this.syncContext.Post( + (d) => + { + this.wrappedConsoleHost.WriteOutput( + outputString, + includeNewLine, + outputType, + foregroundColor, + backgroundColor); + }, + null); + } + + Task IConsoleHost.PromptForChoice( + string promptCaption, + string promptMessage, + IEnumerable choices, + int defaultChoice) + { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + + this.syncContext.Post( + (d) => + { + // Now that we're on the host thread, we can invoke + // PromptForChoice synchronously by calling .Result + // on the task. + int choiceResult = + this.wrappedConsoleHost.PromptForChoice( + promptCaption, + promptMessage, + choices, + defaultChoice).Result; + + taskCompletionSource.SetResult(choiceResult); + }, + null); + + return taskCompletionSource.Task; + } + + void IConsoleHost.PromptForChoiceResult(int promptId, int choiceResult) + { + // TODO: Need to remove this method! + throw new NotImplementedException(); + } + + void IConsoleHost.ExitSession(int exitCode) + { + this.syncContext.Post( + (d) => + { + this.wrappedConsoleHost.ExitSession(exitCode); + }, + null); + } + + void IConsoleHost.UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + this.syncContext.Post( + (d) => + { + this.wrappedConsoleHost.UpdateProgress( + sourceId, + progressDetails); + }, + null); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/IHost.cs b/src/PowerShellEditorServices/IHost.cs new file mode 100644 index 000000000..f3e854c88 --- /dev/null +++ b/src/PowerShellEditorServices/IHost.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an interface for starting and identifying a host. + /// + public interface IHost + { + /// + /// Gets the host application's identifying name. + /// + string Name { get; } + + /// + /// Gets the host application's version number. + /// + Version Version { get; } + + /// + /// Starts the host's message pump. + /// + void Start(); + } +} diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs new file mode 100644 index 000000000..62996047d --- /dev/null +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Language +{ + /// + /// Provides common operations for the syntax tree of a parsed script. + /// + internal static class AstOperations + { + /// + /// Gets completions for the symbol found in the Ast at + /// the given file offset. + /// + /// + /// The Ast which will be traversed to find a completable symbol. + /// + /// + /// The array of tokens corresponding to the scriptAst parameter. + /// + /// + /// The 1-based file offset at which a symbol will be located. + /// + /// + /// The Runspace to use for gathering completions. + /// + /// + /// A CommandCompletion instance that contains completions for the + /// symbol at the given offset. + /// + static public CompletionResults GetCompletions( + Ast scriptAst, + Token[] currentTokens, + int fileOffset, + Runspace runspace) + { + var type = scriptAst.Extent.StartScriptPosition.GetType(); + var method = + type.GetMethod( + "CloneWithNewOffset", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(int) }, null); + + IScriptPosition cursorPosition = + (IScriptPosition)method.Invoke( + scriptAst.Extent.StartScriptPosition, + new object[] { fileOffset }); + + CommandCompletion commandCompletion = null; + if (runspace.RunspaceAvailability == RunspaceAvailability.Available) + { + using (System.Management.Automation.PowerShell powerShell = + System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspace; + + commandCompletion = + CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + null, + powerShell); + } + } + + return CompletionResults.Create(commandCompletion); + } + } +} diff --git a/src/PowerShellEditorServices/Language/CompletionResults.cs b/src/PowerShellEditorServices/Language/CompletionResults.cs new file mode 100644 index 000000000..7e9f15d18 --- /dev/null +++ b/src/PowerShellEditorServices/Language/CompletionResults.cs @@ -0,0 +1,277 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text.RegularExpressions; + +namespace Microsoft.PowerShell.EditorServices.Language +{ + /// + /// Provides the results of a single code completion request. + /// + public sealed class CompletionResults + { + #region Properties + + /// + /// Gets the completions that were found during the + /// completion request. + /// + public CompletionDetails[] Completions { get; private set; } + + #endregion + + #region Constructors + + internal static CompletionResults Create( + CommandCompletion commandCompletion) + { + return new CompletionResults + { + Completions = GetCompletionsArray(commandCompletion), + }; + } + + #endregion + + #region Private Methods + + private static CompletionDetails[] GetCompletionsArray( + CommandCompletion commandCompletion) + { + IEnumerable completionList = + commandCompletion.CompletionMatches.Select( + CompletionDetails.Create); + + return completionList.ToArray(); + } + + #endregion + } + + /// + /// Enumerates the completion types that may be returned. + /// + public enum CompletionType + { + /// + /// Completion type is unknown, either through being uninitialized or + /// having been created from an unsupported CompletionResult that was + /// returned by the PowerShell engine. + /// + Unknown = 0, + + /// + /// Identifies a completion for a command. + /// + Command, + + /// + /// Identifies a completion for a .NET method. + /// + Method, + + /// + /// Identifies a completion for a command parameter name. + /// + ParameterName, + + /// + /// Identifies a completion for a command parameter value. + /// + ParameterValue, + + /// + /// Identifies a completion for a variable name. + /// + Variable, + + /// + /// Identifies a completion for a namespace. + /// + Namespace, + + /// + /// Identifies a completion for a .NET type name. + /// + Type, + + /// + /// Identifies a completion for a PowerShell language keyword. + /// + Keyword + } + + /// + /// Provides the details about a single completion result. + /// + public sealed class CompletionDetails + { + #region Properties + + /// + /// Gets the text that will be used to complete the statement + /// at the requested file offset. + /// + public string CompletionText { get; private set; } + + /// + /// Gets the text that can be used to display a tooltip for + /// the statement at the requested file offset. + /// + public string ToolTipText { get; private set; } + + /// + /// Gets the name of the type which this symbol represents. + /// If the symbol doesn't have an inherent type, null will + /// be returned. + /// + public string SymbolTypeName { get; private set; } + + /// + /// Gets the CompletionType which identifies the type of this completion. + /// + public CompletionType CompletionType { get; private set; } + + #endregion + + #region Constructors + + internal static CompletionDetails Create(CompletionResult completionResult) + { + Validate.IsNotNull("completionResult", completionResult); + + // Some tooltips may have newlines or whitespace for unknown reasons + string toolTipText = completionResult.ToolTip; + if (toolTipText != null) + { + toolTipText = toolTipText.Trim(); + } + + return new CompletionDetails + { + CompletionText = completionResult.CompletionText, + ToolTipText = toolTipText, + SymbolTypeName = ExtractSymbolTypeNameFromToolTip(completionResult.ToolTip), + CompletionType = + ConvertCompletionResultType( + completionResult.ResultType) + }; + } + + internal static CompletionDetails Create( + string completionText, + CompletionType completionType, + string toolTipText = null, + string symbolTypeName = null) + { + return new CompletionDetails + { + CompletionText = completionText, + CompletionType = completionType, + ToolTipText = toolTipText, + SymbolTypeName = symbolTypeName + }; + } + + #endregion + + #region Public Methods + + /// + /// Compares two CompletionResults instances for equality. + /// + /// The potential CompletionResults instance to compare. + /// True if the CompletionResults instances have the same details. + public override bool Equals(object obj) + { + CompletionDetails otherDetails = obj as CompletionDetails; + if (otherDetails == null) + { + return false; + } + + return + string.Equals(this.CompletionText, otherDetails.CompletionText) && + this.CompletionType == otherDetails.CompletionType && + string.Equals(this.ToolTipText, otherDetails.ToolTipText) && + string.Equals(this.SymbolTypeName, otherDetails.SymbolTypeName); + } + + /// + /// Returns the hash code for this CompletionResults instance. + /// + /// The hash code for this CompletionResults instance. + public override int GetHashCode() + { + return + string.Format( + "{0}{1}{2}{3}", + this.CompletionText, + this.CompletionType, + this.ToolTipText, + this.SymbolTypeName).GetHashCode(); + } + + #endregion + + #region Private Methods + + private static CompletionType ConvertCompletionResultType( + CompletionResultType completionResultType) + { + switch (completionResultType) + { + case CompletionResultType.Command: + return CompletionType.Command; + + case CompletionResultType.Method: + return CompletionType.Method; + + case CompletionResultType.ParameterName: + return CompletionType.ParameterName; + + case CompletionResultType.ParameterValue: + return CompletionType.ParameterValue; + + case CompletionResultType.Variable: + return CompletionType.Variable; + + case CompletionResultType.Namespace: + return CompletionType.Namespace; + + case CompletionResultType.Type: + return CompletionType.Type; + + case CompletionResultType.Keyword: + return CompletionType.Keyword; + + default: + // TODO: Trace the unsupported CompletionResultType + return CompletionType.Unknown; + } + } + + private static string ExtractSymbolTypeNameFromToolTip(string toolTipText) + { + // Tooltips returned from PowerShell contain the symbol type in + // brackets. Attempt to extract such strings for further processing. + var matches = Regex.Matches(toolTipText, @"^\[(.+)\]"); + + if (matches.Count > 0 && matches[0].Groups.Count > 1) + { + // Return the symbol type name + return matches[0].Groups[1].Value; + } + + return null; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs new file mode 100644 index 000000000..3b4d57125 --- /dev/null +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Language +{ + using Microsoft.PowerShell.EditorServices.Utility; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + /// + /// Provides a high-level service for performing code completion and + /// navigation operations on PowerShell scripts. + /// + public class LanguageService + { + #region Private Fields + + private Runspace runspace; + + #endregion + + #region Constructors + + /// + /// Constructs an instance of the LanguageService class and uses + /// the given Runspace to execute language service operations. + /// + /// + /// The Runspace in which language service operations will be executed. + /// + public LanguageService(Runspace languageServiceRunspace) + { + Validate.IsNotNull("languageServiceRunspace", languageServiceRunspace); + + this.runspace = languageServiceRunspace; + } + + #endregion + + #region Public Methods + + /// + /// Gets completions for a statement contained in the given + /// script file at the specified line and column position. + /// + /// + /// The script file in which completions will be gathered. + /// + /// + /// The 1-based line number at which completions will be gathered. + /// + /// + /// The 1-based column number at which completions will be gathered. + /// + /// + /// A CommandCompletion instance completions for the identified statement. + /// + public CompletionResults GetCompletionsInFile( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + Validate.IsNotNull("scriptFile", scriptFile); + + // Get the offset at the specified position. This method + // will also validate the given position. + int fileOffset = + scriptFile.GetOffsetAtPosition( + lineNumber, + columnNumber); + + return + AstOperations.GetCompletions( + scriptFile.ScriptAst, + scriptFile.ScriptTokens, + fileOffset, + this.runspace); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Language/ScriptExtent.cs b/src/PowerShellEditorServices/Language/ScriptExtent.cs new file mode 100644 index 000000000..f7395b012 --- /dev/null +++ b/src/PowerShellEditorServices/Language/ScriptExtent.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Language +{ + /// + /// Provides a default IScriptExtent implementation + /// containing details about a section of script content + /// in a file. + /// + public class ScriptExtent : IScriptExtent + { + #region Properties + + /// + /// Gets the file path of the script file in which this extent is contained. + /// + public string File + { + get { throw new NotImplementedException(); } + } + + /// + /// Gets or sets the starting column number of the extent. + /// + public int StartColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the starting line number of the extent. + /// + public int StartLineNumber + { + get; + set; + } + + /// + /// Gets or sets the starting file offset of the extent. + /// + public int StartOffset + { + get; + set; + } + + /// + /// Gets or sets the starting script position of the extent. + /// + public IScriptPosition StartScriptPosition + { + get { throw new NotImplementedException(); } + } + /// + /// Gets or sets the text that is contained within the extent. + /// + public string Text + { + get; + set; + } + + /// + /// Gets or sets the ending column number of the extent. + /// + public int EndColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the ending line number of the extent. + /// + public int EndLineNumber + { + get; + set; + } + + /// + /// Gets or sets the ending file offset of the extent. + /// + public int EndOffset + { + get; + set; + } + + /// + /// Gets the ending script position of the extent. + /// + public IScriptPosition EndScriptPosition + { + get { throw new NotImplementedException(); } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj new file mode 100644 index 000000000..e3a2af44f --- /dev/null +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -0,0 +1,108 @@ + + + + + Debug + AnyCPU + {81E8CBCD-6319-49E7-9662-0475BD0791F4} + Library + Properties + Microsoft.PowerShell.EditorServices + Microsoft.PowerShell.EditorServices + v4.5 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Microsoft.PowerShell.EditorServices.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 1591,1573,1572 + bin\Release\Microsoft.PowerShell.EditorServices.XML + + + + ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.dll + True + + + ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Concurrent.dll + True + + + ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Enlightenment.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {f4bde3d0-3eef-4157-8a3e-722df7adef60} + ScriptAnalyzerEngine + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/PowerShellEditorServices/Properties/AssemblyInfo.cs b/src/PowerShellEditorServices/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..91eaeb936 --- /dev/null +++ b/src/PowerShellEditorServices/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Core")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Core")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0a73c5de-d6ee-4c3c-8bf7-3b2ddea22b75")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test.Shared")] diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs new file mode 100644 index 000000000..21161ad40 --- /dev/null +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -0,0 +1,174 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Analysis; +using Microsoft.PowerShell.EditorServices.Console; +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Manages a single session for all editor services. This + /// includes managing all open script files for the session. + /// + public class EditorSession + { + #region Private Fields + + private Runspace languageRunspace; + private Dictionary workspaceFiles = new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets the LanguageService instance for this session. + /// + public LanguageService LanguageService { get; private set; } + + /// + /// Gets the AnalysisService instance for this session. + /// + public AnalysisService AnalysisService { get; private set; } + + /// + /// Gets the ConsoleService instance for this session. + /// + public ConsoleService ConsoleService { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Starts the session using the provided IConsoleHost implementation + /// for the ConsoleService. + /// + /// + /// An IConsoleHost implementation which is used to interact with the + /// host's user interface. + /// + public void StartSession(IConsoleHost consoleHost) + { + InitialSessionState initialSessionState = InitialSessionState.CreateDefault2(); + + // Create a runspace to share between the language and analysis services + this.languageRunspace = RunspaceFactory.CreateRunspace(initialSessionState); + this.languageRunspace.ApartmentState = ApartmentState.STA; + this.languageRunspace.ThreadOptions = PSThreadOptions.ReuseThread; + this.languageRunspace.Open(); + this.languageRunspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + + // Initialize all services + this.LanguageService = new LanguageService(this.languageRunspace); + this.AnalysisService = new AnalysisService(this.languageRunspace); + this.ConsoleService = new ConsoleService(consoleHost, initialSessionState); + } + + /// + /// Opens a script file with the given file path. + /// + /// The file path at which the script resides. + /// + /// is not found. + /// + /// + /// has already been loaded in the session. + /// + /// + /// contains a null or empty string. + /// + public void OpenFile(string filePath) + { + Validate.IsNotNullOrEmptyString("filePath", filePath); + + // Make sure the file isn't already loaded into the session + if (!this.workspaceFiles.ContainsKey(filePath)) + { + // This method allows FileNotFoundException to bubble up + // if the file isn't found. + + using (StreamReader streamReader = new StreamReader(filePath, Encoding.UTF8)) + { + ScriptFile newFile = new ScriptFile(filePath, streamReader); + this.workspaceFiles.Add(filePath, newFile); + } + } + else + { + throw new ArgumentException( + "The specified file has already been loaded: " + filePath, + "filePath"); + } + } + + /// + /// Closes a currently open script file with the given file path. + /// + /// The file path at which the script resides. + public void CloseFile(ScriptFile scriptFile) + { + Validate.IsNotNull("scriptFile", scriptFile); + + this.workspaceFiles.Remove(scriptFile.FilePath); + } + + /// + /// Attempts to get a currently open script file with the given file path. + /// + /// The file path at which the script resides. + /// The output variable in which the ScriptFile will be stored. + /// A ScriptFile instance + public bool TryGetFile(string filePath, out ScriptFile scriptFile) + { + scriptFile = null; + return this.workspaceFiles.TryGetValue(filePath, out scriptFile); + } + + /// + /// Gets all open files in the session. + /// + /// A collection of all open ScriptFiles in the session. + public IEnumerable GetOpenFiles() + { + return this.workspaceFiles.Values; + } + + #endregion + + #region IDisposable Implementation + + /// + /// Disposes of any Runspaces that were created for the + /// services used in this session. + /// + public void Dispose() + { + // Dispose all necessary services + if (this.ConsoleService != null) + { + this.ConsoleService.Dispose(); + } + + // Dispose all runspaces + if (this.languageRunspace != null) + { + this.languageRunspace.Dispose(); + this.languageRunspace = null; + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/FileChange.cs b/src/PowerShellEditorServices/Session/FileChange.cs new file mode 100644 index 000000000..5933ba1f5 --- /dev/null +++ b/src/PowerShellEditorServices/Session/FileChange.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Contains details relating to a content change in an open file. + /// + public class FileChange + { + /// + /// The string which is to be inserted in the file. + /// + public string InsertString { get; set; } + + /// + /// The 1-based line number where the change starts. + /// + public int Line { get; set; } + + /// + /// The 1-based column offset where the change starts. + /// + public int Offset { get; set; } + + /// + /// The 1-based line number where the change ends. + /// + public int EndLine { get; set; } + + /// + /// The 1-based column offset where the change ends. + /// + public int EndOffset { get; set; } + } +} diff --git a/src/PowerShellEditorServices/Session/ScriptFile.cs b/src/PowerShellEditorServices/Session/ScriptFile.cs new file mode 100644 index 000000000..584619c18 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ScriptFile.cs @@ -0,0 +1,267 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Contains the details and contents of an open script file. + /// + public class ScriptFile + { + #region Private Fields + + private Token[] scriptTokens; + + #endregion + + #region Properties + + /// + /// Gets the path at which this file resides. + /// + public string FilePath { get; private set; } + + /// + /// Gets a string containing the full contents of the file. + /// + public string Contents + { + get + { + return string.Join("\r\n", this.FileLines); + } + } + + /// + /// Gets the list of syntax markers found by parsing this + /// file's contents. + /// + public ScriptFileMarker[] SyntaxMarkers + { + get; + private set; + } + + /// + /// Gets the list of strings for each line of the file. + /// + internal IList FileLines + { + get; + private set; + } + + /// + /// Gets the ScriptBlockAst representing the parsed script contents. + /// + public ScriptBlockAst ScriptAst + { + get; + private set; + } + + /// + /// Gets the array of Tokens representing the parsed script contents. + /// + public Token[] ScriptTokens + { + get { return this.scriptTokens; } + } + + #endregion + + #region Constructors + + /// + /// Creates a new ScriptFile instance by reading file contents from + /// the given TextReader. + /// + /// The path at which the script file resides. + /// The TextReader to use for reading the file's contents. + public ScriptFile(string filePath, TextReader textReader) + { + this.FilePath = filePath; + this.ReadFile(textReader); + } + + #endregion + + #region Public Methods + + /// + /// Gets a line from the file's contents. + /// + /// The 1-based line number in the file. + /// The complete line at the given line number. + public string GetLine(int lineNumber) + { + // TODO: Validate range + + return this.FileLines[lineNumber - 1]; + } + + /// + /// Applies the provided FileChange to the file's contents + /// + /// The FileChange to apply to the file's contents. + public void ApplyChange(FileChange fileChange) + { + // TODO: Verify offsets are in range + + // Break up the change lines + string[] changeLines = fileChange.InsertString.Split('\n'); + + // Get the first fragment of the first line + string firstLineFragment = + this.FileLines[fileChange.Line - 1] + .Substring(0, fileChange.Offset - 1); + + // Get the last fragment of the last line + string endLine = this.FileLines[fileChange.EndLine - 1]; + string lastLineFragment = + endLine.Substring( + fileChange.EndOffset - 1, + (this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1); + + // Remove the old lines + for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) + { + this.FileLines.RemoveAt(fileChange.Line - 1); + } + + // Build and insert the new lines + int currentLineNumber = fileChange.Line; + for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++) + { + // Since we split the lines above using \n, make sure to + // trim the ending \r's off as well. + string finalLine = changeLines[changeIndex].TrimEnd('\r'); + + // Should we add first or last line fragments? + if (changeIndex == 0) + { + // Append the first line fragment + finalLine = firstLineFragment + finalLine; + } + if (changeIndex == changeLines.Length - 1) + { + // Append the last line fragment + finalLine = finalLine + lastLineFragment; + } + + this.FileLines.Insert(currentLineNumber - 1, finalLine); + currentLineNumber++; + } + + // Parse the script again to be up-to-date + this.ParseFileContents(); + } + + /// + /// Calculates the zero-based character offset of a given + /// line and column position in the file. + /// + /// The 1-based line number from which the offset is calculated. + /// The 1-based column number from which the offset is calculated. + /// The zero-based offset for the given file position. + public int GetOffsetAtPosition(int lineNumber, int columnNumber) + { + Validate.IsWithinRange("lineNumber", lineNumber, 1, this.FileLines.Count); + Validate.IsGreaterThan("columnNumber", columnNumber, 0); + + int offset = 0; + + for(int i = 0; i < lineNumber; i++) + { + if (i == lineNumber - 1) + { + // Subtract 1 to account for 1-based column numbering + offset += columnNumber - 1; + } + else + { + // Add an offset to account for the current platform's newline characters + offset += this.FileLines[i].Length + Environment.NewLine.Length; + } + } + + return offset; + } + + #endregion + + #region Private Methods + + /// + /// Reads the contents of a file contained in the given TextReader. + /// + /// A TextReader to use for reading file contents. + private void ReadFile(TextReader textReader) + { + this.FileLines = new List(); + + // Read the file contents line by line + string fileLine = null; + do + { + fileLine = textReader.ReadLine(); + if (fileLine != null) + { + FileLines.Add(fileLine); + } + } + while (fileLine != null); + + // Parse the contents to get syntax tree and errors + this.ParseFileContents(); + } + + /// + /// Parses the current file contents to get the AST, tokens, + /// and parse errors. + /// + private void ParseFileContents() + { + ParseError[] parseErrors = null; + + try + { + this.ScriptAst = + Parser.ParseInput( + this.Contents, + out this.scriptTokens, + out parseErrors); + } + catch (RuntimeException ex) + { + var parseError = + new ParseError( + null, + ex.ErrorRecord.FullyQualifiedErrorId, + ex.Message); + + parseErrors = new[] { parseError }; + this.scriptTokens = new Token[0]; + this.ScriptAst = null; + } + + // Translate parse errors into syntax markers + this.SyntaxMarkers = + parseErrors + .Select(ScriptFileMarker.FromParseError) + .ToArray(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/ScriptFileMarker.cs b/src/PowerShellEditorServices/Session/ScriptFileMarker.cs new file mode 100644 index 000000000..b6f6d0ffe --- /dev/null +++ b/src/PowerShellEditorServices/Session/ScriptFileMarker.cs @@ -0,0 +1,110 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Defines the message level of a script file marker. + /// + public enum ScriptFileMarkerLevel + { + /// + /// The marker represents an informational message. + /// + Information = 0, + + /// + /// The marker represents a warning message. + /// + Warning, + + /// + /// The marker represents an error message. + /// + Error + }; + + /// + /// Contains details about a marker that should be displayed + /// for the a script file. The marker information could come + /// from syntax parsing or semantic analysis of the script. + /// + public class ScriptFileMarker + { + #region Properties + + /// + /// Gets or sets the marker's message string. + /// + public string Message { get; set; } + + /// + /// Gets or sets the marker's message level. + /// + public ScriptFileMarkerLevel Level { get; set; } + + /// + /// Gets or sets the ScriptRegion where the marker should appear. + /// + public ScriptRegion ScriptRegion { get; set; } + + #endregion + + #region Public Methods + + internal static ScriptFileMarker FromParseError( + ParseError parseError) + { + Validate.IsNotNull("parseError", parseError); + + return new ScriptFileMarker + { + Message = parseError.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = ScriptRegion.Create(parseError.Extent) + }; + } + + internal static ScriptFileMarker FromDiagnosticRecord( + DiagnosticRecord diagnosticRecord) + { + Validate.IsNotNull("diagnosticRecord", diagnosticRecord); + + return new ScriptFileMarker + { + Message = diagnosticRecord.Message, + Level = GetMarkerLevelFromDiagnosticSeverity(diagnosticRecord.Severity), + ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent) + }; + } + + private static ScriptFileMarkerLevel GetMarkerLevelFromDiagnosticSeverity( + DiagnosticSeverity diagnosticSeverity) + { + switch (diagnosticSeverity) + { + case DiagnosticSeverity.Information: + return ScriptFileMarkerLevel.Information; + case DiagnosticSeverity.Warning: + return ScriptFileMarkerLevel.Warning; + case DiagnosticSeverity.Error: + return ScriptFileMarkerLevel.Error; + default: + throw new ArgumentException( + string.Format( + "The provided DiagnosticSeverity value '{0}' is unknown.", + diagnosticSeverity), + "diagnosticSeverity"); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Session/ScriptRegion.cs b/src/PowerShellEditorServices/Session/ScriptRegion.cs new file mode 100644 index 000000000..fc9dd8da1 --- /dev/null +++ b/src/PowerShellEditorServices/Session/ScriptRegion.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Contains details about a specific region of text in script file. + /// + public sealed class ScriptRegion + { + #region Properties + + /// + /// Gets the file path of the script file in which this region is contained. + /// + public string File { get; set; } + + /// + /// Gets or sets the text that is contained within the region. + /// + public string Text { get; set; } + + /// + /// Gets or sets the starting line number of the region. + /// + public int StartLineNumber { get; set; } + + /// + /// Gets or sets the starting column number of the region. + /// + public int StartColumnNumber { get; set; } + + /// + /// Gets or sets the starting file offset of the region. + /// + public int StartOffset { get; set; } + + /// + /// Gets or sets the ending line number of the region. + /// + public int EndLineNumber { get; set; } + + /// + /// Gets or sets the ending column number of the region. + /// + public int EndColumnNumber { get; set; } + + /// + /// Gets or sets the ending file offset of the region. + /// + public int EndOffset { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ScriptRegion class from an + /// instance of an IScriptExtent implementation. + /// + /// + /// The IScriptExtent to copy into the ScriptRegion. + /// + /// + /// A new ScriptRegion instance with the same details as the IScriptExtent. + /// + public static ScriptRegion Create(IScriptExtent scriptExtent) + { + return new ScriptRegion + { + File = scriptExtent.File, + Text = scriptExtent.Text, + StartLineNumber = scriptExtent.StartLineNumber, + StartColumnNumber = scriptExtent.StartColumnNumber, + StartOffset = scriptExtent.StartOffset, + EndLineNumber = scriptExtent.EndLineNumber, + EndColumnNumber = scriptExtent.EndColumnNumber, + EndOffset = scriptExtent.EndOffset + }; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Utility/Validate.cs b/src/PowerShellEditorServices/Utility/Validate.cs new file mode 100644 index 000000000..19aefe2c7 --- /dev/null +++ b/src/PowerShellEditorServices/Utility/Validate.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides common validation methods to simplify method + /// parameter checks. + /// + public static class Validate + { + /// + /// Throws ArgumentNullException if value is null. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNull(string parameterName, object valueToCheck) + { + if (valueToCheck == null) + { + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is outside + /// of the given lower and upper limits. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should not be less than. + /// The upper limit which the value should not be greater than. + public static void IsWithinRange( + string parameterName, + int valueToCheck, + int lowerLimit, + int upperLimit) + { + // TODO: Debug assert here if lowerLimit >= upperLimit + + if (valueToCheck < lowerLimit || valueToCheck > upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is not between {0} and {1}", + lowerLimit, + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is greater than or equal + /// to the given upper limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The upper limit which the value should be less than. + public static void IsLessThan( + string parameterName, + int valueToCheck, + int upperLimit) + { + if (valueToCheck >= upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is greater than or equal to {0}", + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is less than or equal + /// to the given lower limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should be greater than. + public static void IsGreaterThan( + string parameterName, + int valueToCheck, + int lowerLimit) + { + if (valueToCheck < lowerLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is less than or equal to {0}", + lowerLimit)); + } + } + + /// + /// Throws ArgumentException if the value is equal to the undesired value. + /// + /// The type of value to be validated. + /// The name of the parameter being validated. + /// The value that valueToCheck should not equal. + /// The value of the parameter being validated. + public static void IsNotEqual( + string parameterName, + TValue valueToCheck, + TValue undesiredValue) + { + if (EqualityComparer.Default.Equals(valueToCheck, undesiredValue)) + { + throw new ArgumentException( + string.Format( + "The given value '{0}' should not equal '{1}'", + valueToCheck, + undesiredValue), + parameterName); + } + } + + /// + /// Throws ArgumentException if the value is null, an empty string, + /// or a string containing only whitespace. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) + { + if (string.IsNullOrWhiteSpace(valueToCheck)) + { + throw new ArgumentException( + "Parameter contains a null, empty, or whitespace string.", + parameterName); + } + } + } +} diff --git a/src/PowerShellEditorServices/packages.config b/src/PowerShellEditorServices/packages.config new file mode 100644 index 000000000..3d1bcb8d9 --- /dev/null +++ b/src/PowerShellEditorServices/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/submodules/PSScriptAnalyzer b/submodules/PSScriptAnalyzer new file mode 160000 index 000000000..96652e225 --- /dev/null +++ b/submodules/PSScriptAnalyzer @@ -0,0 +1 @@ +Subproject commit 96652e2259048336a68d93db72a10a10165bd39d diff --git a/test/PowerShellEditorServices.Test.Host/App.config b/test/PowerShellEditorServices.Test.Host/App.config new file mode 100644 index 000000000..9735dc735 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs new file mode 100644 index 000000000..a3e12e052 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using EnvDTE; +using Microsoft.PowerShell.EditorServices.Transport.Stdio; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Host +{ + internal class LanguageServiceManager + { + System.Diagnostics.Process languageServiceProcess; + + public MessageReader MessageReader { get; private set; } + + public MessageWriter MessageWriter { get; private set; } + + public void Start() + { + // If the test is running in the debugger, tell the language + // service to also wait for the debugger + string languageServiceArguments = string.Empty; + if (System.Diagnostics.Debugger.IsAttached) + { + languageServiceArguments = "/waitForDebugger"; + } + + this.languageServiceProcess = new System.Diagnostics.Process + { + StartInfo = new ProcessStartInfo + { + FileName = "Microsoft.PowerShell.EditorServices.Host.exe", + Arguments = languageServiceArguments, + CreateNoWindow = false, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + }, + EnableRaisingEvents = true, + }; + + // Start the process + this.languageServiceProcess.Start(); + + // Attach to the language service process if debugging + if (System.Diagnostics.Debugger.IsAttached) + { + AttachToProcessIfDebugging(this.languageServiceProcess.Id); + } + + // Load up all of the message types from the transport assembly + MessageTypeResolver messageTypeResolver = new MessageTypeResolver(); + messageTypeResolver.ScanForMessageTypes(typeof(StartedEvent).Assembly); + + // Set up the message reader and writer + this.MessageReader = + new MessageReader( + this.languageServiceProcess.StandardOutput, + MessageFormat.WithContentLength, + messageTypeResolver); + + this.MessageWriter = + new MessageWriter( + this.languageServiceProcess.StandardInput, + MessageFormat.WithoutContentLength, + messageTypeResolver); + + // Wait for the 'started' event + MessageBase startedMessage = this.MessageReader.ReadMessage().Result; + Assert.IsType(startedMessage); + } + + public void Stop() + { + if (this.MessageReader != null) + { + this.MessageReader = null; + } + + if (this.MessageWriter != null) + { + this.MessageWriter = null; + } + + if (this.languageServiceProcess != null) + { + this.languageServiceProcess.Kill(); + this.languageServiceProcess = null; + } + } + + private static void AttachToProcessIfDebugging(int processId) + { + if (System.Diagnostics.Debugger.IsAttached) + { + int tryCount = 5; + + while (tryCount-- > 0) + { + try + { + var dte = (DTE)Marshal.GetActiveObject("VisualStudio.DTE.12.0"); + var processes = dte.Debugger.LocalProcesses.OfType(); + var foundProcess = processes.SingleOrDefault(x => x.ProcessID == processId); + + //EnvDTE.Process foundProcess = null; + //for (int i = 0; i < dte.Debugger.LocalProcesses.Count; i++) + //{ + // foundProcess = dte.Debugger.LocalProcesses.Item(i) as EnvDTE.Process; + + // if (foundProcess != null && foundProcess.ProcessID == processId) + // { + // break; + // } + //} + + if (foundProcess != null) + { + foundProcess.Attach(); + break; + } + else + { + throw new InvalidOperationException("Could not find language service process!"); + } + } + catch (COMException) + { + // Wait a bit and try again + System.Threading.Thread.Sleep(1000); + } + } + } + } + } + + [ComImport, Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IOleMessageFilter + { + [PreserveSig] + int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo); + + [PreserveSig] + int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType); + + [PreserveSig] + int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType); + } + + public class MessageFilter : IOleMessageFilter + { + private const int Handled = 0, RetryAllowed = 2, Retry = 99, Cancel = -1, WaitAndDispatch = 2; + + int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo) + { + return Handled; + } + + int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) + { + return dwRejectType == RetryAllowed ? Retry : Cancel; + } + + int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) + { + return WaitAndDispatch; + } + + public static void Register() + { + CoRegisterMessageFilter(new MessageFilter()); + } + + public static void Revoke() + { + CoRegisterMessageFilter(null); + } + + private static void CoRegisterMessageFilter(IOleMessageFilter newFilter) + { + IOleMessageFilter oldFilter; + CoRegisterMessageFilter(newFilter, out oldFilter); + } + + [DllImport("Ole32.dll")] + private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter); + } +} diff --git a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj new file mode 100644 index 000000000..9473a9403 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj @@ -0,0 +1,124 @@ + + + + + + + Debug + AnyCPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028} + Library + Properties + Microsoft.PowerShell.EditorServices.Test.Host + Microsoft.PowerShell.EditorServices.Test.Host + v4.5 + 512 + ace2de1c + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + True + + + {b2f6369a-d737-4afd-8b81-9b094db07da7} + PowerShellEditorServices.Host + + + {f8a0946a-5d25-4651-8079-b8d5776916fb} + PowerShellEditorServices.Transport.Stdio + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + {f4bde3d0-3eef-4157-8a3e-722df7adef60} + ScriptAnalyzerEngine + + + {c33b6b9d-e61c-45a3-9103-895fd82a5c1e} + ScriptAnalyzerBuiltinRules + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b8c4f3f55 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Test.Host")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Test.Host")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a4c6bac2-47ef-4baf-9cdc-fa34f5d1abe2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs new file mode 100644 index 000000000..e722ee159 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs @@ -0,0 +1,160 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Request; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; +using System; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Host +{ + public class ScenarioTests : IDisposable + { + private LanguageServiceManager languageServiceManager = + new LanguageServiceManager(); + + private MessageReader MessageReader + { + get { return this.languageServiceManager.MessageReader; } + } + + private MessageWriter MessageWriter + { + get { return this.languageServiceManager.MessageWriter; } + } + + public ScenarioTests() + { + this.languageServiceManager.Start(); + } + + public void Dispose() + { + this.languageServiceManager.Stop(); + } + + [Fact] + public void ServiceReturnsSyntaxErrors() + { + // Send the 'open' and 'geterr' events + this.SendOpenFileRequest("TestFiles\\SimpleSyntaxError.ps1"); + this.SendErrorRequest("TestFiles\\SimpleSyntaxError.ps1"); + + // Wait for the events + SyntaxDiagnosticEvent syntaxEvent = this.WaitForMessage(); + SemanticDiagnosticEvent semanticEvent = this.WaitForMessage(); + + // Check for the expected event types + Assert.Equal("syntaxDiag", syntaxEvent.EventType); + Assert.Equal("semanticDiag", semanticEvent.EventType); + + // Was there a syntax error? + Assert.NotEqual(0, syntaxEvent.Body.Diagnostics.Length); + Assert.False( + string.IsNullOrEmpty(syntaxEvent.Body.Diagnostics[0].Text)); + } + + [Fact] + public void ServiceCompletesFunctionName() + { + this.SendOpenFileRequest("TestFiles\\CompleteFunctionName.ps1"); + this.MessageWriter.WriteMessage( + new CompletionsRequest + { + Arguments = new CompletionsRequestArgs + { + File = "TestFiles\\CompleteFunctionName.ps1", + Line = 5, + Offset = 4, + Prefix = "" + } + }); + + CompletionsResponse completions = this.WaitForMessage(); + Assert.NotNull(completions); + Assert.NotEqual(completions.Body.Length, 0); + + // TODO: Add more asserts + } + + [Fact] + public void ServiceExecutesReplCommandAndReceivesOutput() + { + this.MessageWriter.WriteMessage( + new ReplExecuteRequest + { + Arguments = new ReplExecuteArgs + { + CommandString = "1 + 2" + } + }); + + ReplWriteOutputEvent replWriteLineEvent = this.WaitForMessage(); + Assert.Equal("3", replWriteLineEvent.Body.LineContents); + } + + [Fact] + public void ServiceExecutesReplCommandAndReceivesChoicePrompt() + { + string choiceScript = + @" + $caption = ""Test Choice""; + $message = ""Make a selection""; + $choiceA = new-Object System.Management.Automation.Host.ChoiceDescription ""&A"",""A""; + $choiceB = new-Object System.Management.Automation.Host.ChoiceDescription ""&B"",""B""; + $choices = [System.Management.Automation.Host.ChoiceDescription[]]($choiceA,$choiceB); + $response = $host.ui.PromptForChoice($caption, $message, $choices, 1) + $response"; + + this.MessageWriter.WriteMessage( + new ReplExecuteRequest + { + Arguments = new ReplExecuteArgs + { + CommandString = choiceScript + } + }); + + // Wait for the choice prompt event and check expected values + ReplPromptChoiceEvent replPromptChoiceEvent = this.WaitForMessage(); + Assert.Equal(1, replPromptChoiceEvent.Body.DefaultChoice); + + // Respond to the prompt event + this.MessageWriter.WriteMessage( + new ReplPromptChoiceResponse + { + Body = new ReplPromptChoiceResponseBody + { + Choice = 0 + } + }); + + // Wait for the selection to appear as output + ReplWriteOutputEvent replWriteLineEvent = this.WaitForMessage(); + Assert.Equal("0", replWriteLineEvent.Body.LineContents); + } + + private void SendOpenFileRequest(string fileName) + { + this.MessageWriter.WriteMessage( + OpenFileRequest.Create(fileName)); + } + + private void SendErrorRequest(params string[] fileNames) + { + this.MessageWriter.WriteMessage( + ErrorRequest.Create(fileNames)); + } + + private TMessage WaitForMessage() where TMessage : MessageBase + { + // TODO: Integrate timeout! + MessageBase receivedMessage = this.MessageReader.ReadMessage().Result; + return Assert.IsType(receivedMessage); + } + } +} diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 new file mode 100644 index 000000000..d7c7e7c8f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 @@ -0,0 +1,5 @@ +function My-Function +{ +} + +My- \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 new file mode 100644 index 000000000..0468c9f27 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 @@ -0,0 +1,10 @@ +function My-Function ($myInput) +{ + My-Function $myInput +} + +$things = 4 + +$things + +My-Function $things diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/MultiLineReplace.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/MultiLineReplace.ps1 new file mode 100644 index 000000000..2589dd3e0 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/MultiLineReplace.ps1 @@ -0,0 +1,3 @@ +first +secoXX +XXfth \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/SimpleSyntaxError.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/SimpleSyntaxError.ps1 new file mode 100644 index 000000000..94092ebc9 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/SimpleSyntaxError.ps1 @@ -0,0 +1,3 @@ +# Should complain about lacking function body +function MyFunc +} \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Host/packages.config b/test/PowerShellEditorServices.Test.Host/packages.config new file mode 100644 index 000000000..6f1fb7f5f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs new file mode 100644 index 000000000..d7e4c765d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion +{ + public class CompleteCommandFromModule + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"Completion\CompletionExamples.ps1", + StartLineNumber = 13, + StartColumnNumber = 11 + }; + + public static readonly CompletionDetails ExpectedCompletion = + CompletionDetails.Create( + "Install-Module", + CompletionType.Command, + "Install-Module"); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs new file mode 100644 index 000000000..572026fbd --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion +{ + public class CompleteCommandInFile + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"Completion\CompletionExamples.ps1", + StartLineNumber = 8, + StartColumnNumber = 7 + }; + + public static readonly CompletionDetails ExpectedCompletion = + CompletionDetails.Create( + "Get-Something", + CompletionType.Command, + "Get-Something"); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs new file mode 100644 index 000000000..66e11e7c3 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion +{ + public class CompleteVariableInFile + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"Completion\CompletionExamples.ps1", + StartLineNumber = 10, + StartColumnNumber = 9 + }; + + public static readonly CompletionDetails ExpectedCompletion = + CompletionDetails.Create( + "$testVar1", + CompletionType.Variable, + "testVar1"); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.ps1 b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.ps1 new file mode 100644 index 000000000..55e48622f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.ps1 @@ -0,0 +1,13 @@ +function Get-Something +{ + $testVar2 = "Shouldn't find this variable" +} + +$testVar1 = "Should find this variable" + +Get-So + +$testVar + +Import-Module PowerShellGet +Install-Mo \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj new file mode 100644 index 000000000..c24baf51b --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA} + Library + Properties + Microsoft.PowerShell.EditorServices.Test.Shared + Microsoft.PowerShell.EditorServices.Test.Shared + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e70561cf8 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Test.Shared")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Test.Shared")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6b95f264-20c3-49c5-9084-6b7b1cac06e3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs b/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs new file mode 100644 index 000000000..66636d1ba --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using System.IO; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Utility +{ + public class ResourceFileLoader + { + private Assembly resourceAssembly; + + public ResourceFileLoader(Assembly resourceAssembly = null) + { + if (resourceAssembly == null) + { + resourceAssembly = Assembly.GetExecutingAssembly(); + } + + this.resourceAssembly = resourceAssembly; + } + + public ScriptFile LoadFile(string fileName) + { + // Convert the filename to the proper format + string resourceName = + string.Format( + "{0}.{1}", + resourceAssembly.GetName().Name, + fileName.Replace('\\', '.')); + + using (Stream stream = resourceAssembly.GetManifestResourceStream(resourceName)) + using (StreamReader streamReader = new StreamReader(stream)) + { + return new ScriptFile(fileName, streamReader); + } + } + } +} diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/App.config b/test/PowerShellEditorServices.Test.Transport.Stdio/App.config new file mode 100644 index 000000000..9735dc735 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageReaderWriterTests.cs b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageReaderWriterTests.cs new file mode 100644 index 000000000..e13ee1183 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageReaderWriterTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Test.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System.IO; +using System.Reflection; +using Xunit; + +namespace PSLanguageService.Test +{ + public class MessageReaderWriterTests + { + const string testEventString = "{\"event\":\"testEvent\",\"body\":null,\"seq\":0,\"type\":\"event\"}\r\n"; + const string testEventWithContentLengthString = "Content-Length: 56\r\n\r\n" + testEventString; + + private MessageTypeResolver messageTypeResolver; + + public MessageReaderWriterTests() + { + this.messageTypeResolver = new MessageTypeResolver(); + this.messageTypeResolver.ScanForMessageTypes(Assembly.GetExecutingAssembly()); + } + + [Fact] + public void WritesMessageWithContentLength() + { + StringWriter stringWriter = new StringWriter(); + MessageWriter messageWriter = + new MessageWriter( + stringWriter, + MessageFormat.WithContentLength, + this.messageTypeResolver); + + messageWriter.WriteMessage( + new TestEvent()); + + string messageOutput = stringWriter.ToString(); + Assert.Equal( + testEventWithContentLengthString, + messageOutput); + } + + [Fact] + public void WritesMessageWithoutContentLength() + { + StringWriter stringWriter = new StringWriter(); + MessageWriter messageWriter = + new MessageWriter( + stringWriter, + MessageFormat.WithoutContentLength, + this.messageTypeResolver); + + messageWriter.WriteMessage( + new TestEvent()); + + string messageOutput = stringWriter.ToString(); + Assert.Equal( + testEventString, + messageOutput); + } + + [Fact] + public void ReadsMessageWithContentLength() + { + MessageReader messageReader = + this.GetMessageReader( + testEventWithContentLengthString, + MessageFormat.WithContentLength); + + MessageBase messageResult = messageReader.ReadMessage().Result; + TestEvent eventResult = Assert.IsType(messageResult); + Assert.Equal("testEvent", eventResult.EventType); + } + + [Fact] + public void ReadsMessageWithoutContentLength() + { + MessageReader messageReader = + this.GetMessageReader( + testEventString, + MessageFormat.WithoutContentLength); + + MessageBase messageResult = messageReader.ReadMessage().Result; + TestEvent eventResult = Assert.IsType(messageResult); + Assert.Equal("testEvent", eventResult.EventType); + } + + private MessageReader GetMessageReader( + string messageString, + MessageFormat messageFormat) + { + return + new MessageReader( + new StringReader( + messageString), + messageFormat, + this.messageTypeResolver); + } + } +} diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageTypeResolverTests.cs b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageTypeResolverTests.cs new file mode 100644 index 000000000..f73b11960 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/MessageTypeResolverTests.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using System; +using System.Reflection; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Transport.Stdio.Message +{ + public class MessageTypeResolverTests + { + private MessageTypeResolver messageTypeResolver; + + public MessageTypeResolverTests() + { + // Load message types in the current assembly + this.messageTypeResolver = new MessageTypeResolver(); + this.messageTypeResolver.ScanForMessageTypes( + Assembly.GetExecutingAssembly()); + } + + [Fact] + public void MessageTypeResolverFindsRequestTypeByName() + { + this.FindMessageTypeByName( + MessageType.Request, + "testRequest"); + } + + [Fact] + public void MessageTypeResolverFindsRequestTypeNameByType() + { + this.FindMessageTypeNameByType( + "testRequest"); + } + + [Fact] + public void MessageTypeResolverFindsResponseTypeByName() + { + this.FindMessageTypeByName( + MessageType.Response, + "testResponse"); + } + + [Fact] + public void MessageTypeResolverFindsResponseTypeNameByType() + { + this.FindMessageTypeNameByType( + "testResponse"); + } + + [Fact] + public void MessageTypeResolverFindsEventTypeByName() + { + this.FindMessageTypeByName( + MessageType.Event, + "testEvent"); + } + + [Fact] + public void MessageTypeResolverFindsEventTypeNameByType() + { + this.FindMessageTypeNameByType( + "testEvent"); + } + + private void FindMessageTypeByName( + MessageType messageType, + string messageTypeName) + { + Type concreteMessageType = null; + + bool isFound = + this.messageTypeResolver.TryGetMessageTypeByName( + messageType, + messageTypeName, + out concreteMessageType); + + Assert.True(isFound); + Assert.NotNull(concreteMessageType); + Assert.Equal(typeof(TMessage), concreteMessageType); + } + + private void FindMessageTypeNameByType( + string expectedTypeName) + { + string returnedTypeName = null; + + bool isFound = + this.messageTypeResolver.TryGetMessageTypeNameByType( + typeof(TMessage), + out returnedTypeName); + + Assert.True(isFound); + Assert.NotNull(expectedTypeName); + Assert.Equal(expectedTypeName, returnedTypeName); + } + } +} diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/Message/TestMessageTypes.cs b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/TestMessageTypes.cs new file mode 100644 index 000000000..510e4b283 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/Message/TestMessageTypes.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Request; +using Microsoft.PowerShell.EditorServices.Transport.Stdio.Response; +using System; + +namespace Microsoft.PowerShell.EditorServices.Test.Transport.Stdio.Message +{ + #region Request Types + + [MessageTypeName("testRequest")] + internal class TestRequest : RequestBase + { + public override void ProcessMessage( + EditorSession editorSession, + MessageWriter messageWriter) + { + throw new NotImplementedException(); + } + } + + internal class TestRequestArguments + { + public string SomeString { get; set; } + } + + #endregion + + #region Response Types + + [MessageTypeName("testResponse")] + internal class TestResponse : ResponseBase + { + } + + internal class TestResponseBody + { + public string SomeString { get; set; } + } + + #endregion + + #region Event Types + + [MessageTypeName("testEvent")] + internal class TestEvent : EventBase + { + } + + internal class TestEventBody + { + public string SomeString { get; set; } + } + + #endregion +} diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/PowerShellEditorServices.Test.Transport.Stdio.csproj b/test/PowerShellEditorServices.Test.Transport.Stdio/PowerShellEditorServices.Test.Transport.Stdio.csproj new file mode 100644 index 000000000..4705f20b1 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/PowerShellEditorServices.Test.Transport.Stdio.csproj @@ -0,0 +1,98 @@ + + + + + + + Debug + AnyCPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4} + Library + Properties + Microsoft.PowerShell.EditorServices.Test.Transport.Stdio + Microsoft.PowerShell.EditorServices.Test.Transport.Stdio + v4.5 + 512 + cf0525dc + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + + + {f8a0946a-5d25-4651-8079-b8d5776916fb} + PowerShellEditorServices.Transport.Stdio + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Transport.Stdio/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..20ebcee88 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Test.Transport.Stdio")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Test.Transport.Stdio")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("48040b0d-82f5-4633-8927-2b191eef678e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/PowerShellEditorServices.Test.Transport.Stdio/packages.config b/test/PowerShellEditorServices.Test.Transport.Stdio/packages.config new file mode 100644 index 000000000..6f1fb7f5f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Transport.Stdio/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/App.config b/test/PowerShellEditorServices.Test/App.config new file mode 100644 index 000000000..9735dc735 --- /dev/null +++ b/test/PowerShellEditorServices.Test/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs b/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs new file mode 100644 index 000000000..9f7808c3a --- /dev/null +++ b/test/PowerShellEditorServices.Test/Console/ConsoleServiceTests.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using System; +using System.Management.Automation.Runspaces; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Console +{ + public class ConsoleServiceTests : IDisposable + { + private TestConsoleHost consoleHost; + private ConsoleService consoleService; + + const string TestOutputString = "This is a test."; + + public ConsoleServiceTests() + { + this.consoleHost = new TestConsoleHost(); + this.consoleService = + new ConsoleService( + consoleHost, + InitialSessionState.CreateDefault2()); + } + + public void Dispose() + { + // After all tests are complete, dispose of the ConsoleService + this.consoleService.Dispose(); + } + + [Fact] + public async Task ReceivesNormalOutput() + { + await this.consoleService.ExecuteCommand( + string.Format( + "\"{0}\"", + TestOutputString)); + + Assert.Equal( + TestOutputString + Environment.NewLine, + this.consoleHost.GetOutputForType(OutputType.Normal)); + } + + [Fact] + public async Task ReceivesErrorOutput() + { + await this.consoleService.ExecuteCommand( + string.Format( + "Write-Error \"{0}\"", + TestOutputString)); + + string errorString = this.consoleHost.GetOutputForType(OutputType.Error).Split('\r')[0]; + + Assert.Equal( + string.Format("Write-Error \"{0}\" : {0}", TestOutputString), + errorString); + } + + [Fact] + public async Task ReceivesVerboseOutput() + { + // Since setting VerbosePreference causes other message to + // be written out when we run our test, run a command preemptively + // to flush out unwanted verbose messages + await this.consoleService.ExecuteCommand("Write-Verbose \"Preloading\""); + + await this.consoleService.ExecuteCommand( + string.Format( + "$VerbosePreference = \"Continue\"; Write-Verbose \"{0}\"", + TestOutputString)); + + Assert.Equal( + TestOutputString + Environment.NewLine, + this.consoleHost.GetOutputForType(OutputType.Verbose)); + } + + [Fact] + public async Task ReceivesDebugOutput() + { + // Since setting VerbosePreference causes other message to + // be written out when we run our test, run a command preemptively + // to flush out unwanted verbose messages + await this.consoleService.ExecuteCommand("Write-Verbose \"Preloading\""); + + await this.consoleService.ExecuteCommand( + string.Format( + "$DebugPreference = \"Continue\"; Write-Debug \"{0}\"", + TestOutputString)); + + Assert.Equal( + TestOutputString + Environment.NewLine, + this.consoleHost.GetOutputForType(OutputType.Debug)); + } + + [Fact] + public async Task ReceivesWarningOutput() + { + await this.consoleService.ExecuteCommand( + string.Format( + "Write-Warning \"{0}\"", + TestOutputString)); + + Assert.Equal( + TestOutputString + Environment.NewLine, + this.consoleHost.GetOutputForType(OutputType.Warning)); + } + + [Fact] + public async Task ReceivesChoicePrompt() + { + string choiceScript = + @" + $caption = ""Test Choice""; + $message = ""Make a selection""; + $choiceA = new-Object System.Management.Automation.Host.ChoiceDescription ""&A"",""A""; + $choiceB = new-Object System.Management.Automation.Host.ChoiceDescription ""&B"",""B""; + $choices = [System.Management.Automation.Host.ChoiceDescription[]]($choiceA,$choiceB); + $response = $host.ui.PromptForChoice($caption, $message, $choices, 1) + $response"; + + await this.consoleService.ExecuteCommand(choiceScript); + + // TODO: Verify prompt info + + // Verify prompt result written to output + Assert.Equal( + "1" + Environment.NewLine, + this.consoleHost.GetOutputForType(OutputType.Normal)); + } + } +} diff --git a/test/PowerShellEditorServices.Test/Console/TestConsoleHost.cs b/test/PowerShellEditorServices.Test/Console/TestConsoleHost.cs new file mode 100644 index 000000000..0a51c9467 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Console/TestConsoleHost.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Console; +using System; +using System.Collections.Generic; +using System.Management.Automation.Host; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Test.Console +{ + public class TestConsoleHost : IConsoleHost + { + private Dictionary outputPerType = + new Dictionary(); + + #region Helper Methods + + public string GetOutputForType(OutputType outputLineType) + { + string outputString = null; + + this.outputPerType.TryGetValue(outputLineType, out outputString); + + return outputString; + } + + #endregion + + #region IConsoleHost Implementation + + void IConsoleHost.WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + string storedOutputString = null; + if (!this.outputPerType.TryGetValue(outputType, out storedOutputString)) + { + this.outputPerType.Add(outputType, null); + } + + if (storedOutputString == null) + { + storedOutputString = outputString; + } + else + { + storedOutputString += outputString; + } + + if (includeNewLine) + { + storedOutputString += Environment.NewLine; + } + + this.outputPerType[outputType] = storedOutputString; + } + + Task IConsoleHost.PromptForChoice( + string caption, + string message, + IEnumerable choices, + int defaultChoice) + { + var taskCompletionSource = new TaskCompletionSource(); + + // Keep prompt options for validation + + // Run a sleep on another thread to simulate user response + Task.Factory.StartNew( + () => + { + // Sleep and then signal the result + Thread.Sleep(500); + taskCompletionSource.SetResult(defaultChoice); + }); + + // Return the task that will be awaited + return taskCompletionSource.Task; + } + + void IConsoleHost.PromptForChoiceResult(int promptId, int choiceResult) + { + // No need to do anything here, task has already completed. + } + + void IConsoleHost.UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + // TODO: Log progress + } + + void IConsoleHost.ExitSession(int exitCode) + { + // TODO: Log exit code + } + + #endregion + + } +} diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs new file mode 100644 index 000000000..2db71a1d1 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Test.Shared.Completion; +using Microsoft.PowerShell.EditorServices.Test.Shared.Utility; +using System; +using System.Management.Automation.Runspaces; +using System.Threading; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Language +{ + public class LanguageServiceTests : IDisposable + { + private ResourceFileLoader fileLoader; + private Runspace languageServiceRunspace; + private LanguageService languageService; + + public LanguageServiceTests() + { + // Load script files from the shared assembly + this.fileLoader = + new ResourceFileLoader( + typeof(CompleteCommandInFile).Assembly); + + this.languageServiceRunspace = RunspaceFactory.CreateRunspace(); + this.languageServiceRunspace.ApartmentState = ApartmentState.STA; + this.languageServiceRunspace.ThreadOptions = PSThreadOptions.ReuseThread; + this.languageServiceRunspace.Open(); + + this.languageService = new LanguageService(this.languageServiceRunspace); + } + + public void Dispose() + { + this.languageServiceRunspace.Dispose(); + } + + [Fact] + public void LanguageServiceCompletesCommandInFile() + { + CompletionResults completionResults = + this.GetCompletionResults( + CompleteCommandInFile.SourceDetails); + + Assert.NotEqual(0, completionResults.Completions.Length); + Assert.Equal( + CompleteCommandInFile.ExpectedCompletion, + completionResults.Completions[0]); + } + + [Fact(Skip = "This test does not run correctly on AppVeyor, need to investigate.")] + public void LanguageServiceCompletesCommandFromModule() + { + CompletionResults completionResults = + this.GetCompletionResults( + CompleteCommandFromModule.SourceDetails); + + Assert.NotEqual(0, completionResults.Completions.Length); + Assert.Equal( + CompleteCommandFromModule.ExpectedCompletion, + completionResults.Completions[0]); + } + + [Fact] + public void LanguageServiceCompletesVariableInFile() + { + CompletionResults completionResults = + this.GetCompletionResults( + CompleteVariableInFile.SourceDetails); + + Assert.Equal(1, completionResults.Completions.Length); + Assert.Equal( + CompleteVariableInFile.ExpectedCompletion, + completionResults.Completions[0]); + } + + private CompletionResults GetCompletionResults(ScriptRegion scriptRegion) + { + ScriptFile scriptFile = + this.fileLoader.LoadFile( + scriptRegion.File); + + // Run the completions request + return + this.languageService.GetCompletionsInFile( + scriptFile, + scriptRegion.StartLineNumber, + scriptRegion.StartColumnNumber); + } + } +} diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj new file mode 100644 index 000000000..1bf55103e --- /dev/null +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -0,0 +1,103 @@ + + + + + + + Debug + AnyCPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3} + Library + Properties + Microsoft.PowerShell.EditorServices.Test + Microsoft.PowerShell.EditorServices.Test + v4.5 + 512 + 8703d398 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + + + + {81e8cbcd-6319-49e7-9662-0475bd0791f4} + PowerShellEditorServices + + + {6a20b9e9-de66-456e-b4f5-acfd1a95c3ca} + PowerShellEditorServices.Test.Shared + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..a7deee989 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShellEditorServices.Test.Core")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerShellEditorServices.Test.Core")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("647f1413-41e5-4de2-9da4-e39409338d04")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/PowerShellEditorServices.Test/Session/FileChangeTests.cs b/test/PowerShellEditorServices.Test/Session/FileChangeTests.cs new file mode 100644 index 000000000..ffef79453 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Session/FileChangeTests.cs @@ -0,0 +1,142 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; +using System.IO; +using Xunit; + +namespace PSLanguageService.Test +{ + public class FileChangeTests + { + [Fact] + public void CanApplySingleLineInsert() + { + this.AssertFileChange( + "This is a test.", + "This is a working test.", + new FileChange + { + Line = 1, + EndLine = 1, + Offset = 10, + EndOffset = 10, + InsertString = " working" + }); + } + + [Fact] + public void CanApplySingleLineReplace() + { + this.AssertFileChange( + "This is a potentially broken test.", + "This is a working test.", + new FileChange + { + Line = 1, + EndLine = 1, + Offset = 11, + EndOffset = 29, + InsertString = "working" + }); + } + + [Fact] + public void CanApplySingleLineDelete() + { + this.AssertFileChange( + "This is a test of the emergency broadcasting system.", + "This is a test.", + new FileChange + { + Line = 1, + EndLine = 1, + Offset = 15, + EndOffset = 52, + InsertString = "" + }); + } + + [Fact] + public void CanApplyMultiLineInsert() + { + this.AssertFileChange( + "first\r\nsecond\r\nfifth", + "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + new FileChange + { + Line = 3, + EndLine = 3, + Offset = 1, + EndOffset = 1, + InsertString = "third\r\nfourth\r\n" + }); + } + + [Fact] + public void CanApplyMultiLineReplace() + { + this.AssertFileChange( + "first\r\nsecoXX\r\nXXfth", + "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + new FileChange + { + Line = 2, + EndLine = 3, + Offset = 5, + EndOffset = 3, + InsertString = "nd\r\nthird\r\nfourth\r\nfi" + }); + } + + [Fact] + public void CanApplyMultiLineReplaceWithRemovedLines() + { + this.AssertFileChange( + "first\r\nsecoXX\r\nREMOVE\r\nTHESE\r\nLINES\r\nXXfth", + "first\r\nsecond\r\nthird\r\nfourth\r\nfifth", + new FileChange + { + Line = 2, + EndLine = 6, + Offset = 5, + EndOffset = 3, + InsertString = "nd\r\nthird\r\nfourth\r\nfi" + }); + } + + [Fact] + public void CanApplyMultiLineDelete() + { + this.AssertFileChange( + "first\r\nsecond\r\nREMOVE\r\nTHESE\r\nLINES\r\nthird", + "first\r\nsecond\r\nthird", + new FileChange + { + Line = 3, + EndLine = 6, + Offset = 1, + EndOffset = 1, + InsertString = "" + }); + } + + private void AssertFileChange( + string initialString, + string expectedString, + FileChange fileChange) + { + using (StringReader stringReader = new StringReader(initialString)) + { + // Create an in-memory file from the StringReader + ScriptFile fileToChange = new ScriptFile("TestFile.ps1", stringReader); + + // Apply the FileChange and assert the resulting contents + fileToChange.ApplyChange(fileChange); + Assert.Equal(expectedString, fileToChange.Contents); + } + } + } +} diff --git a/test/PowerShellEditorServices.Test/packages.config b/test/PowerShellEditorServices.Test/packages.config new file mode 100644 index 000000000..6f1fb7f5f --- /dev/null +++ b/test/PowerShellEditorServices.Test/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file