Skip to content

Add Plaster messages #1014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Extensions;
using Microsoft.PowerShell.EditorServices.Host;
using Microsoft.PowerShell.EditorServices.Templates;
using Serilog;

namespace Microsoft.PowerShell.EditorServices.Engine
Expand Down Expand Up @@ -244,6 +245,7 @@ public void StartLanguageService(
GetFullyInitializedPowerShellContext(
provider.GetService<OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer>(),
profilePaths))
.AddSingleton<TemplateService>()
.AddSingleton<EditorOperationsService>()
.AddSingleton<ExtensionService>(
(provider) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public async Task StartAsync()
.WithHandler<HoverHandler>()
.WithHandler<SignatureHelpHandler>()
.WithHandler<DefinitionHandler>()
.WithHandler<TemplateHandlers>()
.OnInitialize(
async (languageServer, request) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using OmniSharp.Extensions.Embedded.MediatR;
using OmniSharp.Extensions.JsonRpc;

namespace PowerShellEditorServices.Engine.Services.Handlers
{
[Serial, Method("powerShell/getProjectTemplates")]
public interface IGetProjectTemplatesHandler : IJsonRpcRequestHandler<GetProjectTemplatesRequest, GetProjectTemplatesResponse> { }

[Serial, Method("powerShell/newProjectFromTemplate")]
public interface INewProjectFromTemplateHandler : IJsonRpcRequestHandler<NewProjectFromTemplateRequest, NewProjectFromTemplateResponse> { }

public class GetProjectTemplatesRequest : IRequest<GetProjectTemplatesResponse>
{
public bool IncludeInstalledModules { get; set; }
}

public class GetProjectTemplatesResponse
{
public bool NeedsModuleInstall { get; set; }

public TemplateDetails[] Templates { get; set; }
}

/// <summary>
/// Provides details about a file or project template.
/// </summary>
public class TemplateDetails
{
/// <summary>
/// Gets or sets the title of the template.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Gets or sets the author of the template.
/// </summary>
public string Author { get; set; }

/// <summary>
/// Gets or sets the version of the template.
/// </summary>
public string Version { get; set; }

/// <summary>
/// Gets or sets the description of the template.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets the template's comma-delimited string of tags.
/// </summary>
public string Tags { get; set; }

/// <summary>
/// Gets or sets the template's folder path.
/// </summary>
public string TemplatePath { get; set; }
}

public class NewProjectFromTemplateRequest : IRequest<NewProjectFromTemplateResponse>
{
public string DestinationPath { get; set; }

public string TemplatePath { get; set; }
}

public class NewProjectFromTemplateResponse
{
public bool CreationSuccessful { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices;
using Microsoft.PowerShell.EditorServices.Templates;

namespace PowerShellEditorServices.Engine.Services.Handlers
{
public class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler
{
private readonly ILogger<GetVersionHandler> _logger;
private readonly TemplateService _templateService;

public TemplateHandlers(
ILoggerFactory factory,
TemplateService templateService)
{
_logger = factory.CreateLogger<GetVersionHandler>();
_templateService = templateService;
}

public async Task<GetProjectTemplatesResponse> Handle(GetProjectTemplatesRequest request, CancellationToken cancellationToken)
{
bool plasterInstalled = await _templateService.ImportPlasterIfInstalledAsync();

if (plasterInstalled)
{
var availableTemplates =
await _templateService.GetAvailableTemplatesAsync(
request.IncludeInstalledModules);


return new GetProjectTemplatesResponse
{
Templates = availableTemplates
};
}

return new GetProjectTemplatesResponse
{
NeedsModuleInstall = true,
Templates = new TemplateDetails[0]
};
}

public async Task<NewProjectFromTemplateResponse> Handle(NewProjectFromTemplateRequest request, CancellationToken cancellationToken)
{
bool creationSuccessful;
try
{
await _templateService.CreateFromTemplateAsync(request.TemplatePath, request.DestinationPath);
creationSuccessful = true;
}
catch (Exception e)
{
// We don't really care if this worked or not but we report status.
_logger.LogException("New plaster template failed.", e);
creationSuccessful = false;
}

return new NewProjectFromTemplateResponse
{
CreationSuccessful = creationSuccessful
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Utility;
using PowerShellEditorServices.Engine.Services.Handlers;
using System;
using System.Linq;
using System.Management.Automation;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.EditorServices.Templates
{
/// <summary>
/// Provides a service for listing PowerShell project templates and creating
/// new projects from those templates. This service leverages the Plaster
/// module for creating projects from templates.
/// </summary>
public class TemplateService
{
#region Private Fields

private readonly ILogger logger;
private bool isPlasterLoaded;
private bool? isPlasterInstalled;
private readonly PowerShellContextService powerShellContext;

#endregion

#region Constructors

/// <summary>
/// Creates a new instance of the TemplateService class.
/// </summary>
/// <param name="powerShellContext">The PowerShellContext to use for this service.</param>
/// <param name="factory">An ILoggerFactory implementation used for writing log messages.</param>
public TemplateService(PowerShellContextService powerShellContext, ILoggerFactory factory)
{
Validate.IsNotNull(nameof(powerShellContext), powerShellContext);

this.logger = factory.CreateLogger<TemplateService>();
this.powerShellContext = powerShellContext;
}

#endregion

#region Public Methods

/// <summary>
/// Checks if Plaster is installed on the user's machine.
/// </summary>
/// <returns>A Task that can be awaited until the check is complete. The result will be true if Plaster is installed.</returns>
public async Task<bool> ImportPlasterIfInstalledAsync()
{
if (!this.isPlasterInstalled.HasValue)
{
PSCommand psCommand = new PSCommand();

psCommand
.AddCommand("Get-Module")
.AddParameter("ListAvailable")
.AddParameter("Name", "Plaster");

psCommand
.AddCommand("Sort-Object")
.AddParameter("Descending")
.AddParameter("Property", "Version");

psCommand
.AddCommand("Select-Object")
.AddParameter("First", 1);

this.logger.LogTrace("Checking if Plaster is installed...");

var getResult =
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
psCommand, false, false);

PSObject moduleObject = getResult.First();
this.isPlasterInstalled = moduleObject != null;
string installedQualifier =
this.isPlasterInstalled.Value
? string.Empty : "not ";

this.logger.LogTrace($"Plaster is {installedQualifier}installed!");

// Attempt to load plaster
if (this.isPlasterInstalled.Value && this.isPlasterLoaded == false)
{
this.logger.LogTrace("Loading Plaster...");

psCommand = new PSCommand();
psCommand
.AddCommand("Import-Module")
.AddParameter("ModuleInfo", (PSModuleInfo)moduleObject.ImmediateBaseObject)
.AddParameter("PassThru");

var importResult =
await this.powerShellContext.ExecuteCommandAsync<object>(
psCommand, false, false);

this.isPlasterLoaded = importResult.Any();
string loadedQualifier =
this.isPlasterInstalled.Value
? "was" : "could not be";

this.logger.LogTrace($"Plaster {loadedQualifier} loaded successfully!");
}
}

return this.isPlasterInstalled.Value;
}

/// <summary>
/// Gets the available file or project templates on the user's
/// machine.
/// </summary>
/// <param name="includeInstalledModules">
/// If true, searches the user's installed PowerShell modules for
/// included templates.
/// </param>
/// <returns>A Task which can be awaited for the TemplateDetails list to be returned.</returns>
public async Task<TemplateDetails[]> GetAvailableTemplatesAsync(
bool includeInstalledModules)
{
if (!this.isPlasterLoaded)
{
throw new InvalidOperationException("Plaster is not loaded, templates cannot be accessed.");
}

PSCommand psCommand = new PSCommand();
psCommand.AddCommand("Get-PlasterTemplate");

if (includeInstalledModules)
{
psCommand.AddParameter("IncludeModules");
}

var templateObjects =
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
psCommand, false, false);

this.logger.LogTrace($"Found {templateObjects.Count()} Plaster templates");

return
templateObjects
.Select(CreateTemplateDetails)
.ToArray();
}

/// <summary>
/// Creates a new file or project from a specified template and
/// places it in the destination path. This ultimately calls
/// Invoke-Plaster in PowerShell.
/// </summary>
/// <param name="templatePath">The folder path containing the template.</param>
/// <param name="destinationPath">The folder path where the files will be created.</param>
/// <returns>A boolean-returning Task which communicates success or failure.</returns>
public async Task<bool> CreateFromTemplateAsync(
string templatePath,
string destinationPath)
{
this.logger.LogTrace(
$"Invoking Plaster...\n\n TemplatePath: {templatePath}\n DestinationPath: {destinationPath}");

PSCommand command = new PSCommand();
command.AddCommand("Invoke-Plaster");
command.AddParameter("TemplatePath", templatePath);
command.AddParameter("DestinationPath", destinationPath);

var errorString = new System.Text.StringBuilder();
await this.powerShellContext.ExecuteCommandAsync<PSObject>(
command,
errorString,
new ExecutionOptions
{
WriteOutputToHost = false,
WriteErrorsToHost = true,
InterruptCommandPrompt = true
});

// If any errors were written out, creation was not successful
return errorString.Length == 0;
}

#endregion

#region Private Methods

private static TemplateDetails CreateTemplateDetails(PSObject psObject)
{
return new TemplateDetails
{
Title = psObject.Members["Title"].Value as string,
Author = psObject.Members["Author"].Value as string,
Version = psObject.Members["Version"].Value.ToString(),
Description = psObject.Members["Description"].Value as string,
TemplatePath = psObject.Members["TemplatePath"].Value as string,
Tags =
psObject.Members["Tags"].Value is object[] tags
? string.Join(", ", tags)
: string.Empty
};
}

#endregion
}
}
Loading