Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private async Task HandleCodeLensRequestAsync(
codeLensResponse[i] = codeLensResults[i].ToProtocolCodeLens(
new CodeLensData
{
Uri = codeLensResults[i].File.ClientFilePath,
Uri = codeLensResults[i].File.DocumentUri,
ProviderId = codeLensResults[i].Provider.ProviderId
},
_jsonSerializer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.PowerShell.EditorServices.Commands;
using Microsoft.PowerShell.EditorServices.Symbols;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Commands;
using Microsoft.PowerShell.EditorServices.Symbols;

namespace Microsoft.PowerShell.EditorServices.CodeLenses
{
Expand Down Expand Up @@ -53,7 +51,7 @@ private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile
"PowerShell.RunPesterTests",
"Run tests",
new object[] {
scriptFile.ClientFilePath,
scriptFile.DocumentUri,
false /* No debug */,
pesterSymbol.TestName,
pesterSymbol.ScriptRegion?.StartLineNumber })),
Expand All @@ -66,7 +64,7 @@ private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile
"PowerShell.RunPesterTests",
"Debug tests",
new object[] {
scriptFile.ClientFilePath,
scriptFile.DocumentUri,
true /* Run in the debugger */,
pesterSymbol.TestName,
pesterSymbol.ScriptRegion?.StartLineNumber })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task<CodeLens> ResolveCodeLensAsync(
GetReferenceCountHeader(referenceLocations.Length),
new object[]
{
codeLens.File.ClientFilePath,
codeLens.File.DocumentUri,
codeLens.ScriptExtent.ToRange().Start,
referenceLocations,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1757,15 +1757,15 @@ private static async Task PublishScriptDiagnosticsAsync(
diagnostics.Add(markerDiagnostic);
}

correctionIndex[scriptFile.ClientFilePath] = fileCorrections;
correctionIndex[scriptFile.DocumentUri] = fileCorrections;

// Always send syntax and semantic errors. We want to
// make sure no out-of-date markers are being displayed.
await eventSender(
PublishDiagnosticsNotification.Type,
new PublishDiagnosticsNotification
{
Uri = scriptFile.ClientFilePath,
Uri = scriptFile.DocumentUri,
Diagnostics = diagnostics.ToArray()
});
}
Expand Down
16 changes: 15 additions & 1 deletion src/PowerShellEditorServices/Workspace/ScriptFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
// 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;
using System.Runtime.InteropServices;
using Microsoft.PowerShell.EditorServices.Utility;

namespace Microsoft.PowerShell.EditorServices
{
Expand Down Expand Up @@ -52,6 +53,19 @@ public string Id
/// </summary>
public string ClientFilePath { get; private set; }

/// <summary>
/// Gets the file path in LSP DocumentUri form. The ClientPath property must not be null.
/// </summary>
public string DocumentUri
{
get
{
return this.ClientFilePath == null
? string.Empty
: Workspace.ConvertPathToDocumentUri(this.ClientFilePath);
}
}

/// <summary>
/// Gets or sets a boolean that determines whether
/// semantic analysis should be enabled for this file.
Expand Down
66 changes: 64 additions & 2 deletions src/PowerShellEditorServices/Workspace/Workspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List<string> found
this.logger.WriteHandledException(
$"Could not enumerate files in the path '{folderPath}' due to an exception",
e);

continue;
}

Expand Down Expand Up @@ -400,7 +400,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List<string> found
this.logger.WriteHandledException(
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
e);

return;
}

Expand Down Expand Up @@ -625,6 +625,68 @@ private static string UnescapeDriveColon(string fileUri)
return sb.ToString();
}

/// <summary>
/// Converts a file system path into a DocumentUri required by Language Server Protocol.
/// </summary>
/// <remarks>
/// When sending a document path to a LSP client, the path must be provided as a
/// DocumentUri in order to features like the Problems window or peek definition
/// to be able to open the specified file.
/// </remarks>
/// <param name="path">
/// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified.
/// </param>
/// <returns>The file system path encoded as a DocumentUri.</returns>
internal static string ConvertPathToDocumentUri(string path)
{
const string fileUriPrefix = "file:///";

if (path.StartsWith("untitled:", StringComparison.Ordinal))
{
return path;
}

if (path.StartsWith(fileUriPrefix, StringComparison.Ordinal))
{
return path;
}

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt
string absoluteUri = new Uri(path).AbsoluteUri;

// First colon is part of the protocol scheme, see if there are other colons in the path
int firstColonIndex = absoluteUri.IndexOf(':');
if (absoluteUri.IndexOf(':', firstColonIndex + 1) >= 0)
{
absoluteUri = new StringBuilder()
.Append(absoluteUri, firstColonIndex + 1, absoluteUri.Length - firstColonIndex - 1)
.Replace(":", "%3A")
.Insert(0, absoluteUri.ToCharArray(0, firstColonIndex + 1))
.ToString();
}

return absoluteUri;
}

// VSCode file URIs on Windows need the drive letter lowercase, and the colon
// URI encoded. System.Uri won't do that, so we manually create the URI.
var newUri = new StringBuilder(System.Web.HttpUtility.UrlPathEncode(path));
int colonIndex = path.IndexOf(':');
if (colonIndex > 0)
{
int driveLetterIndex = colonIndex - 1;
char driveLetter = char.ToLowerInvariant(path[driveLetterIndex]);
newUri
.Remove(driveLetterIndex, 2)
.Insert(driveLetterIndex, driveLetter)
.Insert(driveLetterIndex + 1, "%3A");
}

return newUri.Replace('\\', '/').Insert(0, fileUriPrefix).ToString();
}

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public static IEnumerable<object[]> DebuggerAcceptsScriptArgsTestData
}

[Theory]
[MemberData("DebuggerAcceptsScriptArgsTestData")]
[MemberData(nameof(DebuggerAcceptsScriptArgsTestData))]
public async Task DebuggerAcceptsScriptArgs(string[] args)
{
// The path is intentionally odd (some escaped chars but not all) because we are testing
Expand Down
27 changes: 27 additions & 0 deletions test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ public void PropertiesInitializedCorrectlyForUntitled()

Assert.Equal(path, scriptFile.FilePath);
Assert.Equal(path, scriptFile.ClientFilePath);
Assert.Equal(path, scriptFile.DocumentUri);
Assert.True(scriptFile.IsAnalysisEnabled);
Assert.True(scriptFile.IsInMemory);
Assert.Empty(scriptFile.ReferencedFiles);
Expand All @@ -580,5 +581,31 @@ public void PropertiesInitializedCorrectlyForUntitled()
Assert.Equal(3, scriptFile.FileLines.Count);
}
}

[Fact]
public void DocumentUriRetunsCorrectStringForAbsolutePath()
{
string path;
ScriptFile scriptFile;
var emptyStringReader = new StringReader("");

if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
path = @"C:\Users\AmosBurton\projects\Rocinate\ProtoMolecule.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///c%3A/Users/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
}
else
{
// Test the following only on Linux and macOS.
path = "/home/AmosBurton/projects/Rocinate/ProtoMolecule.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///home/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);

path = "/home/AmosBurton/projects/Rocinate/Proto:Mole:cule.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///home/AmosBurton/projects/Rocinate/Proto%3AMole%3Acule.ps1", scriptFile.DocumentUri);
}
}
}
}