Skip to content

Commit 73e8e6e

Browse files
rkeithhillTylerLeonhardt
authored andcommitted
Cherry pick PR 1750 to legacy/1.x branch, fix more issues (PowerShell#880)
* Cherry pick commit for PowerShell#1750 fix * Fix docUri issue with Reference CodeLens provider Encode more chars like %, &, ' , etc * Address PR feedback * Update src/PowerShellEditorServices/Workspace/Workspace.cs Co-Authored-By: rkeithhill <[email protected]>
1 parent 257d26c commit 73e8e6e

File tree

7 files changed

+125
-9
lines changed

7 files changed

+125
-9
lines changed

src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private async Task HandleCodeLensRequest(
126126
codeLensResponse[i] = codeLensResults[i].ToProtocolCodeLens(
127127
new CodeLensData
128128
{
129-
Uri = codeLensResults[i].File.ClientFilePath,
129+
Uri = codeLensResults[i].File.DocumentUri,
130130
ProviderId = codeLensResults[i].Provider.ProviderId
131131
},
132132
_jsonSerializer);

src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public async Task<CodeLens> ResolveCodeLensAsync(
118118
GetReferenceCountHeader(referenceLocations.Length),
119119
new object[]
120120
{
121-
codeLens.File.ClientFilePath,
121+
codeLens.File.DocumentUri,
122122
codeLens.ScriptExtent.ToRange().Start,
123123
referenceLocations,
124124
}
@@ -151,7 +151,7 @@ private static string GetFileUri(string filePath)
151151
// If the file isn't untitled, return a URI-style path
152152
return
153153
!filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory")
154-
? new Uri("file://" + filePath).AbsoluteUri
154+
? Workspace.ConvertPathToDocumentUri(filePath)
155155
: filePath;
156156
}
157157

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1789,15 +1789,15 @@ private static async Task PublishScriptDiagnostics(
17891789
diagnostics.Add(markerDiagnostic);
17901790
}
17911791

1792-
correctionIndex[scriptFile.ClientFilePath] = fileCorrections;
1792+
correctionIndex[scriptFile.DocumentUri] = fileCorrections;
17931793

17941794
// Always send syntax and semantic errors. We want to
17951795
// make sure no out-of-date markers are being displayed.
17961796
await eventSender(
17971797
PublishDiagnosticsNotification.Type,
17981798
new PublishDiagnosticsNotification
17991799
{
1800-
Uri = scriptFile.ClientFilePath,
1800+
Uri = scriptFile.DocumentUri,
18011801
Diagnostics = diagnostics.ToArray()
18021802
});
18031803
}

src/PowerShellEditorServices/Workspace/ScriptFile.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using Microsoft.PowerShell.EditorServices.Utility;
76
using System;
87
using System.Collections.Generic;
98
using System.IO;
109
using System.Linq;
1110
using System.Management.Automation;
1211
using System.Management.Automation.Language;
12+
using System.Runtime.InteropServices;
13+
using Microsoft.PowerShell.EditorServices.Utility;
1314

1415
namespace Microsoft.PowerShell.EditorServices
1516
{
@@ -52,6 +53,19 @@ public string Id
5253
/// </summary>
5354
public string ClientFilePath { get; private set; }
5455

56+
/// <summary>
57+
/// Gets the file path in LSP DocumentUri form. The ClientPath property must not be null.
58+
/// </summary>
59+
public string DocumentUri
60+
{
61+
get
62+
{
63+
return this.ClientFilePath == null
64+
? string.Empty
65+
: Workspace.ConvertPathToDocumentUri(this.ClientFilePath);
66+
}
67+
}
68+
5569
/// <summary>
5670
/// Gets or sets a boolean that determines whether
5771
/// semantic analysis should be enabled for this file.

src/PowerShellEditorServices/Workspace/Workspace.cs

+65-2
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List<string> found
351351
this.logger.WriteHandledException(
352352
$"Could not enumerate files in the path '{folderPath}' due to an exception",
353353
e);
354-
354+
355355
continue;
356356
}
357357

@@ -400,7 +400,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List<string> found
400400
this.logger.WriteHandledException(
401401
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
402402
e);
403-
403+
404404
return;
405405
}
406406

@@ -625,6 +625,69 @@ private static string UnescapeDriveColon(string fileUri)
625625
return sb.ToString();
626626
}
627627

628+
/// <summary>
629+
/// Converts a file system path into a DocumentUri required by Language Server Protocol.
630+
/// </summary>
631+
/// <remarks>
632+
/// When sending a document path to a LSP client, the path must be provided as a
633+
/// DocumentUri in order to features like the Problems window or peek definition
634+
/// to be able to open the specified file.
635+
/// </remarks>
636+
/// <param name="path">
637+
/// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified.
638+
/// </param>
639+
/// <returns>The file system path encoded as a DocumentUri.</returns>
640+
public static string ConvertPathToDocumentUri(string path)
641+
{
642+
const string fileUriPrefix = "file:///";
643+
const string untitledUriPrefix = "untitled:";
644+
645+
// If path is already in document uri form, there is nothing to convert.
646+
if (path.StartsWith(untitledUriPrefix, StringComparison.Ordinal) ||
647+
path.StartsWith(fileUriPrefix, StringComparison.Ordinal))
648+
{
649+
return path;
650+
}
651+
652+
string escapedPath = Uri.EscapeDataString(path);
653+
var docUriStrBld = new StringBuilder(escapedPath);
654+
655+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
656+
{
657+
// VSCode file URIs on Windows need the drive letter lowercase.
658+
// Search original path for colon since a char search (no string culture involved)
659+
// is faster than a string search.
660+
if (path.Contains(':'))
661+
{
662+
// Start at index 1 to avoid an index out of range check when accessing index - 1.
663+
// Also, if the colon is at index 0 there is no drive letter before it to lower case.
664+
for (int i = 1; i < docUriStrBld.Length - 2; i++)
665+
{
666+
if ((docUriStrBld[i] == '%') && (docUriStrBld[i + 1] == '3') && (docUriStrBld[i + 2] == 'A'))
667+
{
668+
int driveLetterIndex = i - 1;
669+
char driveLetter = char.ToLowerInvariant(docUriStrBld[driveLetterIndex]);
670+
docUriStrBld.Replace(path[driveLetterIndex], driveLetter, driveLetterIndex, 1);
671+
break;
672+
}
673+
}
674+
}
675+
676+
// Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \.
677+
docUriStrBld.Replace("%5C", "/");
678+
}
679+
else
680+
{
681+
// Because we will prefix later with file:///, remove the initial encoded / if this is an absolute path.
682+
// See https://docs.microsoft.com/en-us/dotnet/api/system.uri?view=netframework-4.7.2#implicit-file-path-support
683+
// Uri.EscapeDataString goes a bit far, encoding / chars.
684+
docUriStrBld.Replace("%2F", string.Empty, 0, 3).Replace("%2F", "/");
685+
}
686+
687+
// ' is not always encoded. I've seen this in Windows PowerShell.
688+
return docUriStrBld.Replace("'", "%27").Insert(0, fileUriPrefix).ToString();
689+
}
690+
628691
#endregion
629692
}
630693
}

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public static IEnumerable<object[]> DebuggerAcceptsScriptArgsTestData
9797
}
9898

9999
[Theory]
100-
[MemberData("DebuggerAcceptsScriptArgsTestData")]
100+
[MemberData(nameof(DebuggerAcceptsScriptArgsTestData))]
101101
public async Task DebuggerAcceptsScriptArgs(string[] args)
102102
{
103103
// The path is intentionally odd (some escaped chars but not all) because we are testing

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+39
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ public void PropertiesInitializedCorrectlyForUntitled()
570570

571571
Assert.Equal(path, scriptFile.FilePath);
572572
Assert.Equal(path, scriptFile.ClientFilePath);
573+
Assert.Equal(path, scriptFile.DocumentUri);
573574
Assert.True(scriptFile.IsAnalysisEnabled);
574575
Assert.True(scriptFile.IsInMemory);
575576
Assert.Empty(scriptFile.ReferencedFiles);
@@ -578,5 +579,43 @@ public void PropertiesInitializedCorrectlyForUntitled()
578579
Assert.Equal(3, scriptFile.FileLines.Count);
579580
}
580581
}
582+
583+
[Fact]
584+
public void DocumentUriRetunsCorrectStringForAbsolutePath()
585+
{
586+
string path;
587+
ScriptFile scriptFile;
588+
var emptyStringReader = new StringReader("");
589+
590+
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
591+
{
592+
path = @"C:\Users\AmosBurton\projects\Rocinate\ProtoMolecule.ps1";
593+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
594+
Assert.Equal("file:///c%3A/Users/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
595+
596+
path = @"c:\Users\BobbyDraper\projects\Rocinate\foo's_~#-[@] +,;=%.ps1";
597+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
598+
Assert.Equal("file:///c%3A/Users/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
599+
}
600+
else
601+
{
602+
// Test the following only on Linux and macOS.
603+
path = "/home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1";
604+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
605+
Assert.Equal("file:///home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
606+
607+
path = "/home/BobbyDraper/projects/Rocinate/foo's_~#-[@] +,;=%.ps1";
608+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
609+
Assert.Equal("file:///home/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
610+
611+
path = "/home/NaomiNagata/projects/Rocinate/Proto:Mole:cule.ps1";
612+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
613+
Assert.Equal("file:///home/NaomiNagata/projects/Rocinate/Proto%3AMole%3Acule.ps1", scriptFile.DocumentUri);
614+
615+
path = "/home/JamesHolden/projects/Rocinate/Proto:Mole\\cule.ps1";
616+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
617+
Assert.Equal("file:///home/JamesHolden/projects/Rocinate/Proto%3AMole%5Ccule.ps1", scriptFile.DocumentUri);
618+
}
619+
}
581620
}
582621
}

0 commit comments

Comments
 (0)