Skip to content

Cherry pick PR 1750 to legacy/1.x branch, fix more issues #880

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 @@ -126,7 +126,7 @@ private async Task HandleCodeLensRequest(
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 @@ -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 Expand Up @@ -151,7 +151,7 @@ private static string GetFileUri(string filePath)
// If the file isn't untitled, return a URI-style path
return
!filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory")
? new Uri("file://" + filePath).AbsoluteUri
? Workspace.ConvertPathToDocumentUri(filePath)
: filePath;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1738,15 +1738,15 @@ private static async Task PublishScriptDiagnostics(
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
67 changes: 65 additions & 2 deletions src/PowerShellEditorServices/Workspace/Workspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,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 @@ -399,7 +399,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 @@ -624,6 +624,69 @@ 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>
public static string ConvertPathToDocumentUri(string path)
{
const string fileUriPrefix = "file:///";
const string untitledUriPrefix = "untitled:";

// If path is already in document uri form, there is nothing to convert.
if (path.StartsWith(untitledUriPrefix, StringComparison.Ordinal) ||
path.StartsWith(fileUriPrefix, StringComparison.Ordinal))
{
return path;
}

string escapedPath = Uri.EscapeDataString(path);
var docUriStrBld = new StringBuilder(escapedPath);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// VSCode file URIs on Windows need the drive letter lowercase.
// Search original path for colon since a char search (no string culture involved)
// is faster than a string search.
if (path.Contains(':'))
{
// Start at index 1 to avoid an index out of range check when accessing index - 1.
// Also, if the colon is at index 0 there is no drive letter before it to lower case.
for (int i = 1; i < docUriStrBld.Length - 2; i++)
{
if ((docUriStrBld[i] == '%') && (docUriStrBld[i + 1] == '3') && (docUriStrBld[i + 2] == 'A'))
{
int driveLetterIndex = i - 1;
char driveLetter = char.ToLowerInvariant(docUriStrBld[driveLetterIndex]);
docUriStrBld.Replace(path[driveLetterIndex], driveLetter, driveLetterIndex, 1);
break;
}
}
}

// Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \.
docUriStrBld.Replace("%5C", "/");
}
else
{
// Because we will prefix later with file:///, remove the initial encoded / if this is an absolute path.
// See https://docs.microsoft.com/en-us/dotnet/api/system.uri?view=netframework-4.7.2#implicit-file-path-support
// Uri.EscapeDataString goes a bit far, encoding / chars.
docUriStrBld.Replace("%2F", string.Empty, 0, 3).Replace("%2F", "/");
}

// ' is not always encoded. I've seen this in Windows PowerShell.
return docUriStrBld.Replace("'", "%27").Insert(0, fileUriPrefix).ToString();
}

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,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
39 changes: 39 additions & 0 deletions test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,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 @@ -578,5 +579,43 @@ 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);

path = @"c:\Users\BobbyDraper\projects\Rocinate\foo's_~#-[@] +,;=%.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///c%3A/Users/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
}
else
{
// Test the following only on Linux and macOS.
path = "/home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);

path = "/home/BobbyDraper/projects/Rocinate/foo's_~#-[@] +,;=%.ps1";
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
Assert.Equal("file:///home/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);

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

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