1304 lines
46 KiB
C#
1304 lines
46 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.PowerShell.EditorServices.Handlers;
|
|
using Microsoft.PowerShell.EditorServices.Services.Configuration;
|
|
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
|
|
using Newtonsoft.Json.Linq;
|
|
using OmniSharp.Extensions.LanguageServer.Protocol;
|
|
using OmniSharp.Extensions.LanguageServer.Protocol.Client;
|
|
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
|
|
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
|
using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
|
|
|
|
namespace PowerShellEditorServices.Test.E2E
|
|
{
|
|
[Trait("Category", "LSP")]
|
|
public class LanguageServerProtocolMessageTests : IClassFixture<LSPTestsFixture>, IDisposable
|
|
{
|
|
private static readonly string s_binDir =
|
|
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
|
private const string testCommand = "Expand-Archive";
|
|
private const string testDescription = "Extracts files from a specified archive (zipped) file.";
|
|
|
|
private readonly ILanguageClient PsesLanguageClient;
|
|
private readonly List<LogMessageParams> Messages;
|
|
private readonly List<Diagnostic> Diagnostics;
|
|
private readonly string PwshExe;
|
|
|
|
public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixture data)
|
|
{
|
|
data.Output = output;
|
|
PsesLanguageClient = data.PsesLanguageClient;
|
|
Messages = data.Messages;
|
|
Messages.Clear();
|
|
Diagnostics = data.Diagnostics;
|
|
Diagnostics.Clear();
|
|
PwshExe = PsesStdioLanguageServerProcessHost.PwshExe;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Diagnostics.Clear();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private string NewTestFile(string script, bool isPester = false, string languageId = "powershell")
|
|
{
|
|
string fileExt = isPester ? ".Tests.ps1" : ".ps1";
|
|
string filePath = Path.Combine(s_binDir, Path.GetRandomFileName() + fileExt);
|
|
File.WriteAllText(filePath, script);
|
|
|
|
PsesLanguageClient.SendNotification("textDocument/didOpen", new DidOpenTextDocumentParams
|
|
{
|
|
TextDocument = new TextDocumentItem
|
|
{
|
|
LanguageId = languageId,
|
|
Version = 0,
|
|
Text = script,
|
|
Uri = new Uri(filePath)
|
|
}
|
|
});
|
|
|
|
// Give PSES a chance to run what it needs to run.
|
|
Thread.Sleep(2000);
|
|
|
|
return filePath;
|
|
}
|
|
|
|
private async Task WaitForDiagnosticsAsync()
|
|
{
|
|
// Wait for PSSA to finish.
|
|
for (int i = 0; Diagnostics.Count == 0; i++)
|
|
{
|
|
if (i >= 120)
|
|
{
|
|
throw new InvalidDataException("No diagnostics showed up after 2 minutes.");
|
|
}
|
|
|
|
await Task.Delay(1000);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendPowerShellGetVersionRequestAsync()
|
|
{
|
|
PowerShellVersion details
|
|
= await PsesLanguageClient
|
|
.SendRequest("powerShell/getVersion", new GetVersionParams())
|
|
.Returning<PowerShellVersion>(CancellationToken.None);
|
|
|
|
if (PwshExe == "powershell")
|
|
{
|
|
Assert.Equal("Desktop", details.Edition);
|
|
Assert.StartsWith("5", details.Version);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal("Core", details.Edition);
|
|
Assert.StartsWith("7", details.Version);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendWorkspaceSymbolRequestAsync()
|
|
{
|
|
NewTestFile(@"
|
|
function CanSendWorkspaceSymbolRequest {
|
|
Write-Host 'hello'
|
|
}
|
|
");
|
|
|
|
Container<WorkspaceSymbol> symbols = await PsesLanguageClient
|
|
.SendRequest(
|
|
"workspace/symbol",
|
|
new WorkspaceSymbolParams
|
|
{
|
|
Query = "CanSendWorkspaceSymbolRequest"
|
|
})
|
|
.Returning<Container<WorkspaceSymbol>>(CancellationToken.None);
|
|
|
|
WorkspaceSymbol symbol = Assert.Single(symbols);
|
|
Assert.Equal("function CanSendWorkspaceSymbolRequest ()", symbol.Name);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanReceiveDiagnosticsFromFileOpenAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
NewTestFile("$a = 4");
|
|
await WaitForDiagnosticsAsync();
|
|
|
|
Diagnostic diagnostic = Assert.Single(Diagnostics);
|
|
Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync()
|
|
{
|
|
NewTestFile("$a = 4", languageId: "plaintext");
|
|
await Task.Delay(2000);
|
|
|
|
Assert.Empty(Diagnostics);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanReceiveDiagnosticsFromFileChangedAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
string filePath = NewTestFile("$a = 4");
|
|
await WaitForDiagnosticsAsync();
|
|
Diagnostics.Clear();
|
|
|
|
PsesLanguageClient.SendNotification("textDocument/didChange", new DidChangeTextDocumentParams
|
|
{
|
|
// Include several content changes to test against duplicate Diagnostics showing up.
|
|
ContentChanges = new Container<TextDocumentContentChangeEvent>(new[]
|
|
{
|
|
new TextDocumentContentChangeEvent
|
|
{
|
|
Text = "$a = 5"
|
|
},
|
|
new TextDocumentContentChangeEvent
|
|
{
|
|
Text = "$a = 6"
|
|
},
|
|
new TextDocumentContentChangeEvent
|
|
{
|
|
Text = "$a = 7"
|
|
}
|
|
}),
|
|
TextDocument = new OptionalVersionedTextDocumentIdentifier
|
|
{
|
|
Version = 4,
|
|
Uri = new Uri(filePath)
|
|
}
|
|
});
|
|
|
|
await WaitForDiagnosticsAsync();
|
|
if (Diagnostics.Count > 1)
|
|
{
|
|
StringBuilder errorBuilder = new StringBuilder().AppendLine("Multiple diagnostics found when there should be only 1:");
|
|
foreach (Diagnostic diag in Diagnostics)
|
|
{
|
|
errorBuilder.AppendLine(diag.Message);
|
|
}
|
|
|
|
Assert.True(Diagnostics.Count == 1, errorBuilder.ToString());
|
|
}
|
|
|
|
Diagnostic diagnostic = Assert.Single(Diagnostics);
|
|
Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
PsesLanguageClient.SendNotification("workspace/didChangeConfiguration",
|
|
new DidChangeConfigurationParams
|
|
{
|
|
Settings = JToken.FromObject(new LanguageServerSettingsWrapper
|
|
{
|
|
Files = new EditorFileSettings(),
|
|
Search = new EditorSearchSettings(),
|
|
Powershell = new LanguageServerSettings
|
|
{
|
|
ScriptAnalysis = new ScriptAnalysisSettings
|
|
{
|
|
Enable = false
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
string filePath = NewTestFile("$a = 4");
|
|
|
|
// Wait a bit to make sure no diagnostics came through
|
|
await Task.Delay(2000);
|
|
Assert.Empty(Diagnostics);
|
|
|
|
// Restore default configuration
|
|
PsesLanguageClient.SendNotification("workspace/didChangeConfiguration",
|
|
new DidChangeConfigurationParams
|
|
{
|
|
Settings = JToken.FromObject(new LanguageServerSettingsWrapper
|
|
{
|
|
Files = new EditorFileSettings(),
|
|
Search = new EditorSearchSettings(),
|
|
Powershell = new LanguageServerSettings()
|
|
})
|
|
});
|
|
|
|
// That notification does not trigger re-analyzing open files. For that we have to send
|
|
// a textDocument/didChange notification.
|
|
PsesLanguageClient.SendNotification("textDocument/didChange", new DidChangeTextDocumentParams
|
|
{
|
|
ContentChanges = new Container<TextDocumentContentChangeEvent>(),
|
|
TextDocument = new OptionalVersionedTextDocumentIdentifier
|
|
{
|
|
Version = 4,
|
|
Uri = new Uri(filePath)
|
|
}
|
|
});
|
|
|
|
await WaitForDiagnosticsAsync();
|
|
|
|
Diagnostic diagnostic = Assert.Single(Diagnostics);
|
|
Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendFoldingRangeRequestAsync()
|
|
{
|
|
string scriptPath = NewTestFile(@"gci | % {
|
|
$_
|
|
|
|
@""
|
|
$_
|
|
""@
|
|
}");
|
|
|
|
Container<FoldingRange> foldingRanges =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/foldingRange",
|
|
new FoldingRangeRequestParam
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
}
|
|
})
|
|
.Returning<Container<FoldingRange>>(CancellationToken.None);
|
|
|
|
Assert.Collection(foldingRanges.OrderBy(f => f.StartLine),
|
|
range1 =>
|
|
{
|
|
Assert.Equal(0, range1.StartLine);
|
|
Assert.Equal(8, range1.StartCharacter);
|
|
Assert.Equal(5, range1.EndLine);
|
|
Assert.Equal(1, range1.EndCharacter);
|
|
},
|
|
range2 =>
|
|
{
|
|
Assert.Equal(3, range2.StartLine);
|
|
Assert.Equal(0, range2.StartCharacter);
|
|
Assert.Equal(4, range2.EndLine);
|
|
Assert.Equal(2, range2.EndCharacter);
|
|
});
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanSendFormattingRequestAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
string scriptPath = NewTestFile(@"
|
|
gci | % {
|
|
Get-Process
|
|
}
|
|
|
|
");
|
|
|
|
TextEditContainer textEdits = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/formatting",
|
|
new DocumentFormattingParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
},
|
|
Options = new FormattingOptions
|
|
{
|
|
TabSize = 4,
|
|
InsertSpaces = false
|
|
}
|
|
})
|
|
.Returning<TextEditContainer>(CancellationToken.None);
|
|
|
|
TextEdit textEdit = Assert.Single(textEdits);
|
|
|
|
// If we have a tab, formatting ran.
|
|
Assert.Contains("\t", textEdit.NewText);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanSendRangeFormattingRequestAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
string scriptPath = NewTestFile(@"
|
|
gci | % {
|
|
Get-Process
|
|
}
|
|
|
|
");
|
|
|
|
TextEditContainer textEdits = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/formatting",
|
|
new DocumentRangeFormattingParams
|
|
{
|
|
Range = new Range
|
|
{
|
|
Start = new Position
|
|
{
|
|
Line = 2,
|
|
Character = 0
|
|
},
|
|
End = new Position
|
|
{
|
|
Line = 3,
|
|
Character = 0
|
|
}
|
|
},
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
},
|
|
Options = new FormattingOptions
|
|
{
|
|
TabSize = 4,
|
|
InsertSpaces = false
|
|
}
|
|
})
|
|
.Returning<TextEditContainer>(CancellationToken.None);
|
|
|
|
TextEdit textEdit = Assert.Single(textEdits);
|
|
|
|
// If we have a tab, formatting ran.
|
|
Assert.Contains("\t", textEdit.NewText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendDocumentSymbolRequestAsync()
|
|
{
|
|
string scriptPath = NewTestFile(@"
|
|
function CanSendDocumentSymbolRequest {
|
|
|
|
}
|
|
|
|
CanSendDocumentSymbolRequest
|
|
");
|
|
|
|
SymbolInformationOrDocumentSymbolContainer symbolInformationOrDocumentSymbols =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/documentSymbol",
|
|
new DocumentSymbolParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
}
|
|
})
|
|
.Returning<SymbolInformationOrDocumentSymbolContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(symbolInformationOrDocumentSymbols,
|
|
symInfoOrDocSym =>
|
|
{
|
|
Assert.True(symInfoOrDocSym.IsDocumentSymbol);
|
|
Assert.NotNull(symInfoOrDocSym.DocumentSymbol);
|
|
DocumentSymbol symbol = symInfoOrDocSym.DocumentSymbol;
|
|
|
|
Assert.Equal("function CanSendDocumentSymbolRequest ()", symbol.Name);
|
|
Assert.Equal(SymbolKind.Function, symbol.Kind);
|
|
|
|
Assert.Equal(1, symbol.Range.Start.Line);
|
|
Assert.Equal(0, symbol.Range.Start.Character);
|
|
Assert.Equal(3, symbol.Range.End.Line);
|
|
Assert.Equal(1, symbol.Range.End.Character);
|
|
|
|
Assert.Equal(1, symbol.SelectionRange.Start.Line);
|
|
Assert.Equal(9, symbol.SelectionRange.Start.Character);
|
|
Assert.Equal(1, symbol.SelectionRange.End.Line);
|
|
Assert.Equal(37, symbol.SelectionRange.End.Character);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendReferencesRequestAsync()
|
|
{
|
|
string scriptPath = NewTestFile(@"
|
|
function CanSendReferencesRequest {
|
|
|
|
}
|
|
|
|
CanSendReferencesRequest
|
|
");
|
|
|
|
LocationContainer locations = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/references",
|
|
new ReferenceParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
},
|
|
Position = new Position
|
|
{
|
|
Line = 5,
|
|
Character = 0
|
|
},
|
|
Context = new ReferenceContext
|
|
{
|
|
IncludeDeclaration = false
|
|
}
|
|
})
|
|
.Returning<LocationContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(locations,
|
|
location =>
|
|
{
|
|
Range range = location.Range;
|
|
Assert.Equal(5, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(5, range.End.Line);
|
|
Assert.Equal(24, range.End.Character);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendDocumentHighlightRequestAsync()
|
|
{
|
|
string scriptPath = NewTestFile(@"
|
|
Write-Host 'Hello!'
|
|
|
|
Write-Host 'Goodbye'
|
|
");
|
|
|
|
DocumentHighlightContainer documentHighlights =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/documentHighlight",
|
|
new DocumentHighlightParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
},
|
|
Position = new Position
|
|
{
|
|
Line = 3,
|
|
Character = 1
|
|
}
|
|
})
|
|
.Returning<DocumentHighlightContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(documentHighlights.OrderBy(i => i.Range.Start.Line),
|
|
documentHighlight1 =>
|
|
{
|
|
Range range = documentHighlight1.Range;
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(1, range.End.Line);
|
|
Assert.Equal(10, range.End.Character);
|
|
},
|
|
documentHighlight2 =>
|
|
{
|
|
Range range = documentHighlight2.Range;
|
|
Assert.Equal(3, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(3, range.End.Line);
|
|
Assert.Equal(10, range.End.Character);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendPowerShellGetPSHostProcessesRequestAsync()
|
|
{
|
|
Process process = new();
|
|
process.StartInfo.FileName = PwshExe;
|
|
process.StartInfo.ArgumentList.Add("-NoProfile");
|
|
process.StartInfo.ArgumentList.Add("-NoLogo");
|
|
process.StartInfo.ArgumentList.Add("-NoExit");
|
|
|
|
process.StartInfo.CreateNoWindow = true;
|
|
process.StartInfo.UseShellExecute = false;
|
|
|
|
process.StartInfo.RedirectStandardInput = true;
|
|
process.StartInfo.RedirectStandardOutput = true;
|
|
process.StartInfo.RedirectStandardError = true;
|
|
|
|
process.Start();
|
|
|
|
// Wait for the process to start.
|
|
Thread.Sleep(1000);
|
|
|
|
PSHostProcessResponse[] pSHostProcessResponses = null;
|
|
|
|
try
|
|
{
|
|
pSHostProcessResponses =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"powerShell/getPSHostProcesses",
|
|
new GetPSHostProcessesParams())
|
|
.Returning<PSHostProcessResponse[]>(CancellationToken.None);
|
|
}
|
|
finally
|
|
{
|
|
process.Kill();
|
|
process.Dispose();
|
|
}
|
|
|
|
Assert.NotEmpty(pSHostProcessResponses);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendPowerShellGetRunspaceRequestAsync()
|
|
{
|
|
Process process = new();
|
|
process.StartInfo.FileName = PwshExe;
|
|
process.StartInfo.ArgumentList.Add("-NoProfile");
|
|
process.StartInfo.ArgumentList.Add("-NoLogo");
|
|
process.StartInfo.ArgumentList.Add("-NoExit");
|
|
|
|
process.StartInfo.CreateNoWindow = true;
|
|
process.StartInfo.UseShellExecute = false;
|
|
|
|
process.StartInfo.RedirectStandardInput = true;
|
|
process.StartInfo.RedirectStandardOutput = true;
|
|
process.StartInfo.RedirectStandardError = true;
|
|
|
|
process.Start();
|
|
|
|
// Wait for the process to start.
|
|
Thread.Sleep(1000);
|
|
|
|
RunspaceResponse[] runspaceResponses = null;
|
|
try
|
|
{
|
|
runspaceResponses =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"powerShell/getRunspace",
|
|
new GetRunspaceParams
|
|
{
|
|
ProcessId = process.Id
|
|
})
|
|
.Returning<RunspaceResponse[]>(CancellationToken.None);
|
|
}
|
|
finally
|
|
{
|
|
process.Kill();
|
|
process.Dispose();
|
|
}
|
|
|
|
Assert.NotEmpty(runspaceResponses);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendPesterLegacyCodeLensRequestAsync()
|
|
{
|
|
// Make sure LegacyCodeLens is enabled because we'll need it in this test.
|
|
PsesLanguageClient.Workspace.DidChangeConfiguration(
|
|
new DidChangeConfigurationParams
|
|
{
|
|
Settings = JObject.Parse(@"
|
|
{
|
|
""powershell"": {
|
|
""pester"": {
|
|
""useLegacyCodeLens"": true,
|
|
""codeLens"": true
|
|
}
|
|
}
|
|
}
|
|
")
|
|
});
|
|
|
|
string filePath = NewTestFile(@"
|
|
Describe 'DescribeName' {
|
|
Context 'ContextName' {
|
|
It 'ItName' {
|
|
1 | Should - Be 1
|
|
}
|
|
}
|
|
}
|
|
", isPester: true);
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(codeLenses,
|
|
codeLens1 =>
|
|
{
|
|
Range range = codeLens1.Range;
|
|
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(7, range.End.Line);
|
|
Assert.Equal(1, range.End.Character);
|
|
|
|
Assert.Equal("Run tests", codeLens1.Command.Title);
|
|
},
|
|
codeLens2 =>
|
|
{
|
|
Range range = codeLens2.Range;
|
|
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(7, range.End.Line);
|
|
Assert.Equal(1, range.End.Character);
|
|
|
|
Assert.Equal("Debug tests", codeLens2.Command.Title);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendPesterCodeLensRequestAsync()
|
|
{
|
|
// Make sure Pester legacy CodeLens is disabled because we'll need it in this test.
|
|
PsesLanguageClient.Workspace.DidChangeConfiguration(
|
|
new DidChangeConfigurationParams
|
|
{
|
|
Settings = JObject.Parse(@"
|
|
{
|
|
""powershell"": {
|
|
""pester"": {
|
|
""useLegacyCodeLens"": false,
|
|
""codeLens"": true
|
|
}
|
|
}
|
|
}
|
|
")
|
|
});
|
|
|
|
string filePath = NewTestFile(@"
|
|
Describe 'DescribeName' {
|
|
Context 'ContextName' {
|
|
It 'ItName' {
|
|
1 | Should - Be 1
|
|
}
|
|
}
|
|
}
|
|
", isPester: true);
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(codeLenses,
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(7, range.End.Line);
|
|
Assert.Equal(1, range.End.Character);
|
|
|
|
Assert.Equal("Run tests", codeLens.Command.Title);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(0, range.Start.Character);
|
|
Assert.Equal(7, range.End.Line);
|
|
Assert.Equal(1, range.End.Character);
|
|
|
|
Assert.Equal("Debug tests", codeLens.Command.Title);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(2, range.Start.Line);
|
|
Assert.Equal(4, range.Start.Character);
|
|
Assert.Equal(6, range.End.Line);
|
|
Assert.Equal(5, range.End.Character);
|
|
|
|
Assert.Equal("Run tests", codeLens.Command.Title);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(2, range.Start.Line);
|
|
Assert.Equal(4, range.Start.Character);
|
|
Assert.Equal(6, range.End.Line);
|
|
Assert.Equal(5, range.End.Character);
|
|
|
|
Assert.Equal("Debug tests", codeLens.Command.Title);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(3, range.Start.Line);
|
|
Assert.Equal(8, range.Start.Character);
|
|
Assert.Equal(5, range.End.Line);
|
|
Assert.Equal(9, range.End.Character);
|
|
|
|
Assert.Equal("Run test", codeLens.Command.Title);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
|
|
Assert.Equal(3, range.Start.Line);
|
|
Assert.Equal(8, range.Start.Character);
|
|
Assert.Equal(5, range.End.Line);
|
|
Assert.Equal(9, range.End.Character);
|
|
|
|
Assert.Equal("Debug test", codeLens.Command.Title);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoMessageIfPesterCodeLensDisabled()
|
|
{
|
|
// Make sure Pester legacy CodeLens is disabled because we'll need it in this test.
|
|
PsesLanguageClient.Workspace.DidChangeConfiguration(
|
|
new DidChangeConfigurationParams
|
|
{
|
|
Settings = JObject.Parse(@"
|
|
{
|
|
""powershell"": {
|
|
""pester"": {
|
|
""codeLens"": false
|
|
}
|
|
}
|
|
}
|
|
")
|
|
});
|
|
|
|
string filePath = NewTestFile(@"
|
|
Describe 'DescribeName' {
|
|
Context 'ContextName' {
|
|
It 'ItName' {
|
|
1 | Should - Be 1
|
|
}
|
|
}
|
|
}
|
|
", isPester: true);
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
Assert.Empty(codeLenses);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendFunctionReferencesCodeLensRequestAsync()
|
|
{
|
|
string filePath = NewTestFile(@"
|
|
function CanSendReferencesCodeLensRequest {
|
|
|
|
}
|
|
|
|
CanSendReferencesCodeLensRequest
|
|
");
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
CodeLens codeLens = Assert.Single(codeLenses);
|
|
|
|
Range range = codeLens.Range;
|
|
Assert.Equal(1, range.Start.Line);
|
|
Assert.Equal(9, range.Start.Character);
|
|
Assert.Equal(1, range.End.Line);
|
|
Assert.Equal(41, range.End.Character);
|
|
|
|
CodeLens codeLensResolveResult = await PsesLanguageClient
|
|
.SendRequest("codeLens/resolve", codeLens)
|
|
.Returning<CodeLens>(CancellationToken.None);
|
|
|
|
Assert.Equal("1 reference", codeLensResolveResult.Command.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendClassReferencesCodeLensRequestAsync()
|
|
{
|
|
string filePath = NewTestFile(@"
|
|
param(
|
|
[MyBaseClass]$enumValue
|
|
)
|
|
|
|
class MyBaseClass {
|
|
|
|
}
|
|
|
|
class ChildClass : MyBaseClass, System.IDisposable {
|
|
|
|
}
|
|
|
|
$o = [MyBaseClass]::new()
|
|
$o -is [MyBaseClass]
|
|
");
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(codeLenses.OrderBy(i => i.Range.Start.Line),
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
Assert.Equal(5, range.Start.Line);
|
|
Assert.Equal(6, range.Start.Character);
|
|
Assert.Equal(5, range.End.Line);
|
|
Assert.Equal(17, range.End.Character);
|
|
},
|
|
codeLens =>
|
|
{
|
|
Range range = codeLens.Range;
|
|
Assert.Equal(9, range.Start.Line);
|
|
Assert.Equal(6, range.Start.Character);
|
|
Assert.Equal(9, range.End.Line);
|
|
Assert.Equal(16, range.End.Character);
|
|
}
|
|
);
|
|
|
|
CodeLens baseClassCodeLens = codeLenses.OrderBy(i => i.Range.Start.Line).First();
|
|
CodeLens codeLensResolveResult = await PsesLanguageClient
|
|
.SendRequest("codeLens/resolve", baseClassCodeLens)
|
|
.Returning<CodeLens>(CancellationToken.None);
|
|
|
|
Assert.Equal("4 references", codeLensResolveResult.Command.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendEnumReferencesCodeLensRequestAsync()
|
|
{
|
|
string filePath = NewTestFile(@"
|
|
param(
|
|
[MyEnum]$enumValue
|
|
)
|
|
|
|
enum MyEnum {
|
|
First = 1
|
|
Second
|
|
Third
|
|
}
|
|
|
|
[MyEnum]::First
|
|
'First' -is [MyEnum]
|
|
");
|
|
|
|
CodeLensContainer codeLenses = await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeLens",
|
|
new CodeLensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
}
|
|
})
|
|
.Returning<CodeLensContainer>(CancellationToken.None);
|
|
|
|
CodeLens codeLens = Assert.Single(codeLenses);
|
|
|
|
Range range = codeLens.Range;
|
|
Assert.Equal(5, range.Start.Line);
|
|
Assert.Equal(5, range.Start.Character);
|
|
Assert.Equal(5, range.End.Line);
|
|
Assert.Equal(11, range.End.Character);
|
|
|
|
CodeLens codeLensResolveResult = await PsesLanguageClient
|
|
.SendRequest("codeLens/resolve", codeLens)
|
|
.Returning<CodeLens>(CancellationToken.None);
|
|
|
|
Assert.Equal("3 references", codeLensResolveResult.Command.Title);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanSendCodeActionRequestAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
string filePath = NewTestFile("gci");
|
|
await WaitForDiagnosticsAsync();
|
|
|
|
CommandOrCodeActionContainer commandOrCodeActions =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/codeAction",
|
|
new CodeActionParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier(
|
|
new Uri(filePath, UriKind.Absolute)),
|
|
Range = new Range
|
|
{
|
|
Start = new Position
|
|
{
|
|
Line = 0,
|
|
Character = 0
|
|
},
|
|
End = new Position
|
|
{
|
|
Line = 0,
|
|
Character = 3
|
|
}
|
|
},
|
|
Context = new CodeActionContext
|
|
{
|
|
Diagnostics = new Container<Diagnostic>(Diagnostics)
|
|
}
|
|
})
|
|
.Returning<CommandOrCodeActionContainer>(CancellationToken.None);
|
|
|
|
Assert.Collection(commandOrCodeActions,
|
|
command =>
|
|
{
|
|
Assert.Equal(
|
|
"Replace gci with Get-ChildItem",
|
|
command.CodeAction.Title);
|
|
Assert.Equal(
|
|
CodeActionKind.QuickFix,
|
|
command.CodeAction.Kind);
|
|
Assert.Single(command.CodeAction.Edit.DocumentChanges);
|
|
},
|
|
command =>
|
|
{
|
|
Assert.Equal(
|
|
"PowerShell.ShowCodeActionDocumentation",
|
|
command.CodeAction.Command.Name);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendCompletionAndCompletionResolveRequestAsync()
|
|
{
|
|
CompletionList completionItems = await PsesLanguageClient.TextDocument.RequestCompletion(
|
|
new CompletionParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = DocumentUri.FromFileSystemPath(NewTestFile(testCommand))
|
|
},
|
|
Position = new Position(line: 0, character: 7)
|
|
});
|
|
|
|
CompletionItem completionItem = Assert.Single(completionItems,
|
|
completionItem1 => completionItem1.FilterText == testCommand);
|
|
|
|
CompletionItem updatedCompletionItem = await PsesLanguageClient
|
|
.SendRequest("completionItem/resolve", completionItem)
|
|
.Returning<CompletionItem>(CancellationToken.None);
|
|
|
|
Assert.Contains(testDescription, updatedCompletionItem.Documentation.String);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendCompletionResolveWithModulePrefixRequestAsync()
|
|
{
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"evaluate",
|
|
new EvaluateRequestArguments
|
|
{
|
|
Expression = "Import-Module Microsoft.PowerShell.Archive -Prefix Test"
|
|
})
|
|
.ReturningVoid(CancellationToken.None);
|
|
|
|
try
|
|
{
|
|
const string command = "Expand-TestArchive";
|
|
|
|
CompletionList completionItems = await PsesLanguageClient.TextDocument.RequestCompletion(
|
|
new CompletionParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = DocumentUri.FromFileSystemPath(NewTestFile(command))
|
|
},
|
|
Position = new Position(line: 0, character: 12)
|
|
});
|
|
|
|
CompletionItem completionItem = Assert.Single(completionItems,
|
|
completionItem1 => completionItem1.Label == command);
|
|
|
|
CompletionItem updatedCompletionItem = await PsesLanguageClient.ResolveCompletion(completionItem);
|
|
|
|
Assert.Contains(testDescription, updatedCompletionItem.Documentation.String);
|
|
}
|
|
finally
|
|
{
|
|
// Reset the Archive module to the non-prefixed version
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"evaluate",
|
|
new EvaluateRequestArguments
|
|
{
|
|
Expression = "Remove-Module Microsoft.PowerShell.Archive;Import-Module Microsoft.PowerShell.Archive -Force"
|
|
})
|
|
.ReturningVoid(CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendHoverRequestAsync()
|
|
{
|
|
string filePath = NewTestFile(testCommand);
|
|
|
|
Hover hover = await PsesLanguageClient.TextDocument.RequestHover(
|
|
new HoverParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = DocumentUri.FromFileSystemPath(filePath)
|
|
},
|
|
Position = new Position(line: 0, character: 1)
|
|
});
|
|
|
|
Assert.True(hover.Contents.HasMarkedStrings);
|
|
Assert.Collection(hover.Contents.MarkedStrings,
|
|
str1 => Assert.Equal(testCommand, str1.Value),
|
|
str2 =>
|
|
{
|
|
Assert.Equal("markdown", str2.Language);
|
|
Assert.Equal(testDescription, str2.Value);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendSignatureHelpRequestAsync()
|
|
{
|
|
string filePath = NewTestFile($"{testCommand} -");
|
|
|
|
SignatureHelp signatureHelp = await PsesLanguageClient.RequestSignatureHelp
|
|
(
|
|
new SignatureHelpParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(filePath)
|
|
},
|
|
Position = new Position
|
|
{
|
|
Line = 0,
|
|
Character = 10
|
|
}
|
|
}
|
|
);
|
|
|
|
Assert.Contains(testCommand, signatureHelp.Signatures.First().Label);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendDefinitionRequestAsync()
|
|
{
|
|
string scriptPath = NewTestFile(@"
|
|
function CanSendDefinitionRequest {
|
|
|
|
}
|
|
|
|
CanSendDefinitionRequest
|
|
");
|
|
|
|
LocationOrLocationLinks locationOrLocationLinks =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/definition",
|
|
new DefinitionParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier { Uri = new Uri(scriptPath) },
|
|
Position = new Position { Line = 5, Character = 2 }
|
|
})
|
|
.Returning<LocationOrLocationLinks>(CancellationToken.None);
|
|
|
|
LocationOrLocationLink locationOrLocationLink =
|
|
Assert.Single(locationOrLocationLinks);
|
|
|
|
Assert.Equal(1, locationOrLocationLink.Location.Range.Start.Line);
|
|
Assert.Equal(9, locationOrLocationLink.Location.Range.Start.Character);
|
|
Assert.Equal(1, locationOrLocationLink.Location.Range.End.Line);
|
|
Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanSendGetCommentHelpRequestAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode && PsesStdioLanguageServerProcessHost.IsWindowsPowerShell,
|
|
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
|
|
|
|
string scriptPath = NewTestFile(@"
|
|
function CanSendGetCommentHelpRequest {
|
|
param(
|
|
$myParam,
|
|
$myOtherParam,
|
|
$yetAnotherParam
|
|
)
|
|
|
|
# Include other problematic code to make sure this still works
|
|
gci
|
|
}
|
|
");
|
|
|
|
CommentHelpRequestResult commentHelpRequestResult =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"powerShell/getCommentHelp",
|
|
new CommentHelpRequestParams
|
|
{
|
|
DocumentUri = new Uri(scriptPath).ToString(),
|
|
BlockComment = false,
|
|
TriggerPosition = new Position
|
|
{
|
|
Line = 0,
|
|
Character = 0
|
|
}
|
|
})
|
|
.Returning<CommentHelpRequestResult>(CancellationToken.None);
|
|
|
|
Assert.NotEmpty(commentHelpRequestResult.Content);
|
|
Assert.Contains("myParam", commentHelpRequestResult.Content[7]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendEvaluateRequestAsync()
|
|
{
|
|
EvaluateResponseBody evaluateResponseBody =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"evaluate",
|
|
new EvaluateRequestArguments
|
|
{
|
|
Expression = "Get-ChildItem"
|
|
})
|
|
.Returning<EvaluateResponseBody>(CancellationToken.None);
|
|
|
|
// These always gets returned so this test really just makes sure we get _any_ response.
|
|
Assert.Equal("", evaluateResponseBody.Result);
|
|
Assert.Equal(0, evaluateResponseBody.VariablesReference);
|
|
}
|
|
|
|
// getCommand gets all the commands in the system, and is not optimized and can take forever on CI systems
|
|
[SkippableFact(Timeout = 120000)]
|
|
public async Task CanSendGetCommandRequestAsync()
|
|
{
|
|
Skip.If(Environment.GetEnvironmentVariable("TF_BUILD") is not null,
|
|
"This test is too slow in CI.");
|
|
|
|
List<object> pSCommandMessages =
|
|
await PsesLanguageClient
|
|
.SendRequest("powerShell/getCommand", new GetCommandParams())
|
|
.Returning<List<object>>(CancellationToken.None);
|
|
|
|
Assert.NotEmpty(pSCommandMessages);
|
|
// There should be at least 20 commands or so.
|
|
Assert.True(pSCommandMessages.Count > 20);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task CanSendExpandAliasRequestAsync()
|
|
{
|
|
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
|
|
"The expand alias request doesn't work in Constrained Language Mode.");
|
|
|
|
ExpandAliasResult expandAliasResult =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"powerShell/expandAlias",
|
|
new ExpandAliasParams
|
|
{
|
|
Text = "gci"
|
|
})
|
|
.Returning<ExpandAliasResult>(CancellationToken.None);
|
|
|
|
Assert.Equal("Get-ChildItem", expandAliasResult.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSendSemanticTokenRequestAsync()
|
|
{
|
|
const string scriptContent = "function";
|
|
string scriptPath = NewTestFile(scriptContent);
|
|
|
|
SemanticTokens result =
|
|
await PsesLanguageClient
|
|
.SendRequest(
|
|
"textDocument/semanticTokens/full",
|
|
new SemanticTokensParams
|
|
{
|
|
TextDocument = new TextDocumentIdentifier
|
|
{
|
|
Uri = new Uri(scriptPath)
|
|
}
|
|
})
|
|
.Returning<SemanticTokens>(CancellationToken.None);
|
|
|
|
// More information about how this data is generated can be found at
|
|
// https://github.com/microsoft/vscode-extension-samples/blob/5ae1f7787122812dcc84e37427ca90af5ee09f14/semantic-tokens-sample/vscode.proposed.d.ts#L71
|
|
int[] expectedArr = new int[5]
|
|
{
|
|
// line, index, token length, token type, token modifiers
|
|
0, 0, scriptContent.Length, 1, 0 //function token: line 0, index 0, length of script, type 1 = keyword, no modifiers
|
|
};
|
|
|
|
Assert.Equal(expectedArr, result.Data.ToArray());
|
|
}
|
|
}
|
|
}
|