Skip to content

Commit 00c3f99

Browse files
Add Plaster messages
1 parent 576718c commit 00c3f99

File tree

6 files changed

+384
-0
lines changed

6 files changed

+384
-0
lines changed

src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.Extensions.Logging;
2020
using Microsoft.PowerShell.EditorServices.Extensions;
2121
using Microsoft.PowerShell.EditorServices.Host;
22+
using Microsoft.PowerShell.EditorServices.Templates;
2223
using Serilog;
2324

2425
namespace Microsoft.PowerShell.EditorServices.Engine
@@ -244,6 +245,7 @@ public void StartLanguageService(
244245
GetFullyInitializedPowerShellContext(
245246
provider.GetService<OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer>(),
246247
profilePaths))
248+
.AddSingleton<TemplateService>()
247249
.AddSingleton<EditorOperationsService>()
248250
.AddSingleton<ExtensionService>(
249251
(provider) =>

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public async Task StartAsync()
114114
.WithHandler<HoverHandler>()
115115
.WithHandler<SignatureHelpHandler>()
116116
.WithHandler<DefinitionHandler>()
117+
.WithHandler<TemplateHandlers>()
117118
.OnInitialize(
118119
async (languageServer, request) =>
119120
{
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using OmniSharp.Extensions.Embedded.MediatR;
7+
using OmniSharp.Extensions.JsonRpc;
8+
9+
namespace PowerShellEditorServices.Engine.Services.Handlers
10+
{
11+
[Serial, Method("powerShell/getProjectTemplates")]
12+
public interface IGetProjectTemplatesHandler : IJsonRpcRequestHandler<GetProjectTemplatesRequest, GetProjectTemplatesResponse> { }
13+
14+
[Serial, Method("powerShell/newProjectFromTemplate")]
15+
public interface INewProjectFromTemplateHandler : IJsonRpcRequestHandler<NewProjectFromTemplateRequest, NewProjectFromTemplateResponse> { }
16+
17+
public class GetProjectTemplatesRequest : IRequest<GetProjectTemplatesResponse>
18+
{
19+
public bool IncludeInstalledModules { get; set; }
20+
}
21+
22+
public class GetProjectTemplatesResponse
23+
{
24+
public bool NeedsModuleInstall { get; set; }
25+
26+
public TemplateDetails[] Templates { get; set; }
27+
}
28+
29+
/// <summary>
30+
/// Provides details about a file or project template.
31+
/// </summary>
32+
public class TemplateDetails
33+
{
34+
/// <summary>
35+
/// Gets or sets the title of the template.
36+
/// </summary>
37+
public string Title { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets the author of the template.
41+
/// </summary>
42+
public string Author { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the version of the template.
46+
/// </summary>
47+
public string Version { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the description of the template.
51+
/// </summary>
52+
public string Description { get; set; }
53+
54+
/// <summary>
55+
/// Gets or sets the template's comma-delimited string of tags.
56+
/// </summary>
57+
public string Tags { get; set; }
58+
59+
/// <summary>
60+
/// Gets or sets the template's folder path.
61+
/// </summary>
62+
public string TemplatePath { get; set; }
63+
}
64+
65+
public class NewProjectFromTemplateRequest : IRequest<NewProjectFromTemplateResponse>
66+
{
67+
public string DestinationPath { get; set; }
68+
69+
public string TemplatePath { get; set; }
70+
}
71+
72+
public class NewProjectFromTemplateResponse
73+
{
74+
public bool CreationSuccessful { get; set; }
75+
}
76+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.PowerShell.EditorServices;
11+
using Microsoft.PowerShell.EditorServices.Templates;
12+
13+
namespace PowerShellEditorServices.Engine.Services.Handlers
14+
{
15+
public class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler
16+
{
17+
private readonly ILogger<GetVersionHandler> _logger;
18+
private readonly TemplateService _templateService;
19+
20+
public TemplateHandlers(
21+
ILoggerFactory factory,
22+
TemplateService templateService)
23+
{
24+
_logger = factory.CreateLogger<GetVersionHandler>();
25+
_templateService = templateService;
26+
}
27+
28+
public async Task<GetProjectTemplatesResponse> Handle(GetProjectTemplatesRequest request, CancellationToken cancellationToken)
29+
{
30+
bool plasterInstalled = await _templateService.ImportPlasterIfInstalledAsync();
31+
32+
if (plasterInstalled)
33+
{
34+
var availableTemplates =
35+
await _templateService.GetAvailableTemplatesAsync(
36+
request.IncludeInstalledModules);
37+
38+
39+
return new GetProjectTemplatesResponse
40+
{
41+
Templates = availableTemplates
42+
};
43+
}
44+
45+
return new GetProjectTemplatesResponse
46+
{
47+
NeedsModuleInstall = true,
48+
Templates = new TemplateDetails[0]
49+
};
50+
}
51+
52+
public async Task<NewProjectFromTemplateResponse> Handle(NewProjectFromTemplateRequest request, CancellationToken cancellationToken)
53+
{
54+
bool creationSuccessful;
55+
try
56+
{
57+
await _templateService.CreateFromTemplateAsync(request.TemplatePath, request.DestinationPath);
58+
creationSuccessful = true;
59+
}
60+
catch (Exception e)
61+
{
62+
// We don't really care if this worked or not but we report status.
63+
_logger.LogException("New plaster template failed.", e);
64+
creationSuccessful = false;
65+
}
66+
67+
return new NewProjectFromTemplateResponse
68+
{
69+
CreationSuccessful = creationSuccessful
70+
};
71+
}
72+
}
73+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.PowerShell.EditorServices.Utility;
8+
using PowerShellEditorServices.Engine.Services.Handlers;
9+
using System;
10+
using System.Linq;
11+
using System.Management.Automation;
12+
using System.Threading.Tasks;
13+
14+
namespace Microsoft.PowerShell.EditorServices.Templates
15+
{
16+
/// <summary>
17+
/// Provides a service for listing PowerShell project templates and creating
18+
/// new projects from those templates. This service leverages the Plaster
19+
/// module for creating projects from templates.
20+
/// </summary>
21+
public class TemplateService
22+
{
23+
#region Private Fields
24+
25+
private readonly ILogger logger;
26+
private bool isPlasterLoaded;
27+
private bool? isPlasterInstalled;
28+
private readonly PowerShellContextService powerShellContext;
29+
30+
#endregion
31+
32+
#region Constructors
33+
34+
/// <summary>
35+
/// Creates a new instance of the TemplateService class.
36+
/// </summary>
37+
/// <param name="powerShellContext">The PowerShellContext to use for this service.</param>
38+
/// <param name="factory">An ILoggerFactory implementation used for writing log messages.</param>
39+
public TemplateService(PowerShellContextService powerShellContext, ILoggerFactory factory)
40+
{
41+
Validate.IsNotNull(nameof(powerShellContext), powerShellContext);
42+
43+
this.logger = factory.CreateLogger<TemplateService>();
44+
this.powerShellContext = powerShellContext;
45+
}
46+
47+
#endregion
48+
49+
#region Public Methods
50+
51+
/// <summary>
52+
/// Checks if Plaster is installed on the user's machine.
53+
/// </summary>
54+
/// <returns>A Task that can be awaited until the check is complete. The result will be true if Plaster is installed.</returns>
55+
public async Task<bool> ImportPlasterIfInstalledAsync()
56+
{
57+
if (!this.isPlasterInstalled.HasValue)
58+
{
59+
PSCommand psCommand = new PSCommand();
60+
61+
psCommand
62+
.AddCommand("Get-Module")
63+
.AddParameter("ListAvailable")
64+
.AddParameter("Name", "Plaster");
65+
66+
psCommand
67+
.AddCommand("Sort-Object")
68+
.AddParameter("Descending")
69+
.AddParameter("Property", "Version");
70+
71+
psCommand
72+
.AddCommand("Select-Object")
73+
.AddParameter("First", 1);
74+
75+
this.logger.LogTrace("Checking if Plaster is installed...");
76+
77+
var getResult =
78+
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
79+
psCommand, false, false);
80+
81+
PSObject moduleObject = getResult.First();
82+
this.isPlasterInstalled = moduleObject != null;
83+
string installedQualifier =
84+
this.isPlasterInstalled.Value
85+
? string.Empty : "not ";
86+
87+
this.logger.LogTrace($"Plaster is {installedQualifier}installed!");
88+
89+
// Attempt to load plaster
90+
if (this.isPlasterInstalled.Value && this.isPlasterLoaded == false)
91+
{
92+
this.logger.LogTrace("Loading Plaster...");
93+
94+
psCommand = new PSCommand();
95+
psCommand
96+
.AddCommand("Import-Module")
97+
.AddParameter("ModuleInfo", (PSModuleInfo)moduleObject.ImmediateBaseObject)
98+
.AddParameter("PassThru");
99+
100+
var importResult =
101+
await this.powerShellContext.ExecuteCommandAsync<object>(
102+
psCommand, false, false);
103+
104+
this.isPlasterLoaded = importResult.Any();
105+
string loadedQualifier =
106+
this.isPlasterInstalled.Value
107+
? "was" : "could not be";
108+
109+
this.logger.LogTrace($"Plaster {loadedQualifier} loaded successfully!");
110+
}
111+
}
112+
113+
return this.isPlasterInstalled.Value;
114+
}
115+
116+
/// <summary>
117+
/// Gets the available file or project templates on the user's
118+
/// machine.
119+
/// </summary>
120+
/// <param name="includeInstalledModules">
121+
/// If true, searches the user's installed PowerShell modules for
122+
/// included templates.
123+
/// </param>
124+
/// <returns>A Task which can be awaited for the TemplateDetails list to be returned.</returns>
125+
public async Task<TemplateDetails[]> GetAvailableTemplatesAsync(
126+
bool includeInstalledModules)
127+
{
128+
if (!this.isPlasterLoaded)
129+
{
130+
throw new InvalidOperationException("Plaster is not loaded, templates cannot be accessed.");
131+
}
132+
133+
PSCommand psCommand = new PSCommand();
134+
psCommand.AddCommand("Get-PlasterTemplate");
135+
136+
if (includeInstalledModules)
137+
{
138+
psCommand.AddParameter("IncludeModules");
139+
}
140+
141+
var templateObjects =
142+
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
143+
psCommand, false, false);
144+
145+
this.logger.LogTrace($"Found {templateObjects.Count()} Plaster templates");
146+
147+
return
148+
templateObjects
149+
.Select(CreateTemplateDetails)
150+
.ToArray();
151+
}
152+
153+
/// <summary>
154+
/// Creates a new file or project from a specified template and
155+
/// places it in the destination path. This ultimately calls
156+
/// Invoke-Plaster in PowerShell.
157+
/// </summary>
158+
/// <param name="templatePath">The folder path containing the template.</param>
159+
/// <param name="destinationPath">The folder path where the files will be created.</param>
160+
/// <returns>A boolean-returning Task which communicates success or failure.</returns>
161+
public async Task<bool> CreateFromTemplateAsync(
162+
string templatePath,
163+
string destinationPath)
164+
{
165+
this.logger.LogTrace(
166+
$"Invoking Plaster...\n\n TemplatePath: {templatePath}\n DestinationPath: {destinationPath}");
167+
168+
PSCommand command = new PSCommand();
169+
command.AddCommand("Invoke-Plaster");
170+
command.AddParameter("TemplatePath", templatePath);
171+
command.AddParameter("DestinationPath", destinationPath);
172+
173+
var errorString = new System.Text.StringBuilder();
174+
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
175+
command,
176+
errorString,
177+
new ExecutionOptions
178+
{
179+
WriteOutputToHost = false,
180+
WriteErrorsToHost = true,
181+
InterruptCommandPrompt = true
182+
});
183+
184+
// If any errors were written out, creation was not successful
185+
return errorString.Length == 0;
186+
}
187+
188+
#endregion
189+
190+
#region Private Methods
191+
192+
private static TemplateDetails CreateTemplateDetails(PSObject psObject)
193+
{
194+
return new TemplateDetails
195+
{
196+
Title = psObject.Members["Title"].Value as string,
197+
Author = psObject.Members["Author"].Value as string,
198+
Version = psObject.Members["Version"].Value.ToString(),
199+
Description = psObject.Members["Description"].Value as string,
200+
TemplatePath = psObject.Members["TemplatePath"].Value as string,
201+
Tags =
202+
psObject.Members["Tags"].Value is object[] tags
203+
? string.Join(", ", tags)
204+
: string.Empty
205+
};
206+
}
207+
208+
#endregion
209+
}
210+
}

0 commit comments

Comments
 (0)