Skip to content

Commit 029a6cc

Browse files
rkeithhillTylerLeonhardt
authored andcommitted
Cherry pick PR 1750 merge commit to legacy/v1.x, has additional fixes (#887)
1 parent 40cacf6 commit 029a6cc

File tree

3 files changed

+42
-34
lines changed

3 files changed

+42
-34
lines changed

src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -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/Workspace/Workspace.cs

+33-33
Original file line numberDiff line numberDiff line change
@@ -637,55 +637,55 @@ private static string UnescapeDriveColon(string fileUri)
637637
/// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified.
638638
/// </param>
639639
/// <returns>The file system path encoded as a DocumentUri.</returns>
640-
internal static string ConvertPathToDocumentUri(string path)
640+
public static string ConvertPathToDocumentUri(string path)
641641
{
642642
const string fileUriPrefix = "file:///";
643+
const string untitledUriPrefix = "untitled:";
643644

644-
if (path.StartsWith("untitled:", StringComparison.Ordinal))
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))
645648
{
646649
return path;
647650
}
648651

649-
if (path.StartsWith(fileUriPrefix, StringComparison.Ordinal))
650-
{
651-
return path;
652-
}
652+
string escapedPath = Uri.EscapeDataString(path);
653+
var docUriStrBld = new StringBuilder(escapedPath);
653654

654-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
655+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
655656
{
656-
// On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt
657-
string absoluteUri = new Uri(path).AbsoluteUri;
658-
659-
// First colon is part of the protocol scheme, see if there are other colons in the path
660-
int firstColonIndex = absoluteUri.IndexOf(':');
661-
if (absoluteUri.IndexOf(':', firstColonIndex + 1) > firstColonIndex)
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(':'))
662661
{
663-
absoluteUri = new StringBuilder(absoluteUri)
664-
.Replace(
665-
oldValue: ":",
666-
newValue: "%3A",
667-
startIndex: firstColonIndex + 1,
668-
count: absoluteUri.Length - firstColonIndex - 1)
669-
.ToString();
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+
}
670674
}
671675

672-
return absoluteUri;
676+
// Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \.
677+
docUriStrBld.Replace("%5C", "/");
673678
}
674-
675-
// VSCode file URIs on Windows need the drive letter lowercase, and the colon
676-
// URI encoded. System.Uri won't do that, so we manually create the URI.
677-
var newUri = new StringBuilder(System.Web.HttpUtility.UrlPathEncode(path));
678-
int colonIndex = path.IndexOf(':');
679-
if (colonIndex > 0)
679+
else
680680
{
681-
int driveLetterIndex = colonIndex - 1;
682-
char driveLetter = char.ToLowerInvariant(path[driveLetterIndex]);
683-
newUri
684-
.Replace(path[driveLetterIndex], driveLetter, driveLetterIndex, 1)
685-
.Replace(":", "%3A", colonIndex, 1);
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", "/");
686685
}
687686

688-
return newUri.Replace('\\', '/').Insert(0, fileUriPrefix).ToString();
687+
// ' is not always encoded. I've seen this in Windows PowerShell.
688+
return docUriStrBld.Replace("'", "%27").Insert(0, fileUriPrefix).ToString();
689689
}
690690

691691
#endregion

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+8
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ public void DocumentUriRetunsCorrectStringForAbsolutePath()
594594
path = @"C:\Users\AmosBurton\projects\Rocinate\ProtoMolecule.ps1";
595595
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
596596
Assert.Equal("file:///c%3A/Users/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
597+
598+
path = @"c:\Users\BobbyDraper\projects\Rocinate\foo's_~#-[@] +,;=%.ps1";
599+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
600+
Assert.Equal("file:///c%3A/Users/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
597601
}
598602
else
599603
{
@@ -602,6 +606,10 @@ public void DocumentUriRetunsCorrectStringForAbsolutePath()
602606
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
603607
Assert.Equal("file:///home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri);
604608

609+
path = "/home/BobbyDraper/projects/Rocinate/foo's_~#-[@] +,;=%.ps1";
610+
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
611+
Assert.Equal("file:///home/BobbyDraper/projects/Rocinate/foo%27s_~%23-%5B%40%5D%20%2B%2C%3B%3D%25.ps1", scriptFile.DocumentUri);
612+
605613
path = "/home/NaomiNagata/projects/Rocinate/Proto:Mole:cule.ps1";
606614
scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion);
607615
Assert.Equal("file:///home/NaomiNagata/projects/Rocinate/Proto%3AMole%3Acule.ps1", scriptFile.DocumentUri);

0 commit comments

Comments
 (0)