From c09fa73820e3c9c01c79b7632524f7c888e7e05d Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 27 Feb 2019 23:22:39 -0700 Subject: [PATCH 01/11] Fix unable to open files in problems/peek windows issue This is due to PSES not properly storing a language client path in the ClientFilePath property of ScriptFile. This change ensures that a ClientFilePath is always stored in the text document Uri that a LSP client expects. This code has mostly been provided by @seeminglyScience. Thanks for the reference to your code. This fixes issue 1732 in the vscode-powershell repo. --- .../Workspace/ScriptFile.cs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index f4dd3de26..0c8cadb66 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -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 { @@ -27,6 +28,7 @@ public class ScriptFile }; private Version powerShellVersion; + private string _clientPath; #endregion @@ -50,7 +52,20 @@ public string Id /// /// Gets the path which the editor client uses to identify this file. /// - public string ClientFilePath { get; private set; } + public string ClientFilePath + { + get { return _clientPath; } + + private set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _clientPath = GetPathAsClientPath(value); + } + } /// /// Gets or sets a boolean that determines whether @@ -564,6 +579,43 @@ public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset) #region Private Methods + private static string GetPathAsClientPath(string path) + { + const string fileUriPrefix = "file:///"; + + if (path.StartsWith("untitled:", StringComparison.Ordinal)) + { + return path; + } + + if (path.StartsWith("file:///", StringComparison.Ordinal)) + { + return path; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new Uri(path).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 = System.Web.HttpUtility.UrlPathEncode(path); + int colonIndex = path.IndexOf(":"); + for (var i = colonIndex - 1; i >= 0; i--) + { + newUri.Remove(i, 1); + newUri.Insert(i, char.ToLowerInvariant(path[i]).ToString()); + } + + return newUri + .Remove(colonIndex, 1) + .Insert(colonIndex, "%3A") + .Replace("\\", "/") + .Insert(0, fileUriPrefix) + .ToString(); + } + private void SetFileContents(string fileContents) { // Split the file contents into lines and trim From e9595a5e5b26163ce2e8568d0808664081a7bfe7 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sat, 2 Mar 2019 18:31:32 -0700 Subject: [PATCH 02/11] Play it safe by adding a new property instead of changing ClientPath --- .../CodeLens/CodeLensFeature.cs | 2 +- .../CodeLens/PesterCodeLensProvider.cs | 10 ++-- .../CodeLens/ReferencesCodeLensProvider.cs | 2 +- .../Server/LanguageServer.cs | 4 +- .../Workspace/ScriptFile.cs | 21 ++++--- .../Workspace/Workspace.cs | 57 ++++++++++++++++++- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs index e7e2f6910..d90d77333 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs @@ -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); diff --git a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs index e814918ab..cc4a706b0 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs @@ -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 { @@ -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 })), @@ -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 })), diff --git a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs index 6a5312a93..3aff19990 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs @@ -118,7 +118,7 @@ public async Task ResolveCodeLensAsync( GetReferenceCountHeader(referenceLocations.Length), new object[] { - codeLens.File.ClientFilePath, + codeLens.File.DocumentUri, codeLens.ScriptExtent.ToRange().Start, referenceLocations, } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index d325b2d65..20a32c3d0 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1757,7 +1757,7 @@ 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. @@ -1765,7 +1765,7 @@ await eventSender( PublishDiagnosticsNotification.Type, new PublishDiagnosticsNotification { - Uri = scriptFile.ClientFilePath, + Uri = scriptFile.DocumentUri, Diagnostics = diagnostics.ToArray() }); } diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index 0c8cadb66..c26724633 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -28,7 +28,6 @@ public class ScriptFile }; private Version powerShellVersion; - private string _clientPath; #endregion @@ -52,18 +51,18 @@ public string Id /// /// Gets the path which the editor client uses to identify this file. /// - public string ClientFilePath - { - get { return _clientPath; } + public string ClientFilePath { get; private set; } - private set + /// + /// Gets the file path in LSP DocumentUri form. The ClientPath property must not be null. + /// + public string DocumentUri + { + get { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _clientPath = GetPathAsClientPath(value); + return (this.ClientFilePath == null ) + ? string.Empty + : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); } } diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index d81f9f5e7..58fae5f9f 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -351,7 +351,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List found this.logger.WriteHandledException( $"Could not enumerate files in the path '{folderPath}' due to an exception", e); - + continue; } @@ -400,7 +400,7 @@ private void RecursivelyEnumerateFiles(string folderPath, ref List found this.logger.WriteHandledException( $"Could not enumerate directories in the path '{folderPath}' due to an exception", e); - + return; } @@ -625,6 +625,59 @@ private static string UnescapeDriveColon(string fileUri) return sb.ToString(); } + /// + /// Converts a file system path into a DocumentUri required by Language Server Protocol. + /// + /// + /// 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. + /// + /// + /// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified. + /// + /// The file system path encoded as a DocumentUri. + 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)) + { + return new Uri(path).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 = System.Web.HttpUtility.UrlPathEncode(path); + int colonIndex = path.IndexOf(':'); + for (var i = colonIndex - 1; i >= 0; i--) + { + newUri.Remove(i, 1); + newUri.Insert(i, char.ToLowerInvariant(path[i]).ToString()); + } + + // On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt + if (colonIndex >= 0) + { + newUri = newUri.Replace(":", "%3A"); + } + + return newUri + .Replace('\\', '/') + .Insert(0, fileUriPrefix) + .ToString(); + } + #endregion } } From b4be9ba38db3dcf4de084e4e31ff56f801771d79 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sat, 2 Mar 2019 18:54:44 -0700 Subject: [PATCH 03/11] Address Codacy issues --- .../Workspace/ScriptFile.cs | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index c26724633..b31dd8cba 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -578,43 +578,6 @@ public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset) #region Private Methods - private static string GetPathAsClientPath(string path) - { - const string fileUriPrefix = "file:///"; - - if (path.StartsWith("untitled:", StringComparison.Ordinal)) - { - return path; - } - - if (path.StartsWith("file:///", StringComparison.Ordinal)) - { - return path; - } - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return new Uri(path).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 = System.Web.HttpUtility.UrlPathEncode(path); - int colonIndex = path.IndexOf(":"); - for (var i = colonIndex - 1; i >= 0; i--) - { - newUri.Remove(i, 1); - newUri.Insert(i, char.ToLowerInvariant(path[i]).ToString()); - } - - return newUri - .Remove(colonIndex, 1) - .Insert(colonIndex, "%3A") - .Replace("\\", "/") - .Insert(0, fileUriPrefix) - .ToString(); - } - private void SetFileContents(string fileContents) { // Split the file contents into lines and trim From 92d2cd0765f837e365f6190931925965e16fa673 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Thu, 7 Mar 2019 22:57:38 -0700 Subject: [PATCH 04/11] Add tests for DocumentUri and fix colon issue on Linux/macOS --- .../Workspace/Workspace.cs | 17 ++++++-------- .../Debugging/DebugServiceTests.cs | 2 +- .../Session/ScriptFileTests.cs | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 58fae5f9f..991a1d1ad 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -653,23 +653,20 @@ internal static string ConvertPathToDocumentUri(string path) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new Uri(path).AbsoluteUri; + // On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt + return new Uri(path).AbsoluteUri.Replace(":", "%3A"); } // 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 = System.Web.HttpUtility.UrlPathEncode(path); int colonIndex = path.IndexOf(':'); - for (var i = colonIndex - 1; i >= 0; i--) + if (colonIndex > 0) { - newUri.Remove(i, 1); - newUri.Insert(i, char.ToLowerInvariant(path[i]).ToString()); - } - - // On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt - if (colonIndex >= 0) - { - newUri = newUri.Replace(":", "%3A"); + int driveLetterIndex = colonIndex - 1; + var driveLetter = char.ToLowerInvariant(path[driveLetterIndex]).ToString(); + newUri = newUri.Remove(driveLetterIndex, 2); + newUri = newUri.Insert(driveLetterIndex, driveLetter + "%3A"); } return newUri diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 0d6e0b30e..9b430cd1a 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -101,7 +101,7 @@ public static IEnumerable 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 diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 2a664f2f3..fbd31c59e 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -547,6 +547,7 @@ public void PropertiesInitializedCorrectlyForFile() Assert.Equal(path, scriptFile.FilePath); Assert.Equal(path, scriptFile.ClientFilePath); + Assert.Equal("file:///TestFile.ps1", scriptFile.DocumentUri); Assert.True(scriptFile.IsAnalysisEnabled); Assert.False(scriptFile.IsInMemory); Assert.Empty(scriptFile.ReferencedFiles); @@ -572,6 +573,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); @@ -580,5 +582,25 @@ public void PropertiesInitializedCorrectlyForUntitled() Assert.Equal(3, scriptFile.FileLines.Count); } } + + [Fact] + public void DocumentUriRetunsCorrectStringForAbsolutePath() + { + string path; + ScriptFile scriptFile; + var emptyStringReader = new StringReader(""); + + 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); + + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + // Test the following only on Linux and macOS. + 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); + } + } } } From 056007b424d2f319c73697a9aad6bc47dfcc164e Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Thu, 7 Mar 2019 23:29:15 -0700 Subject: [PATCH 05/11] Attempt to fix where we replace colons on Linux --- .../Workspace/Workspace.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 991a1d1ad..0c2837a86 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -640,6 +640,7 @@ private static string UnescapeDriveColon(string fileUri) internal static string ConvertPathToDocumentUri(string path) { const string fileUriPrefix = "file:///"; + int colonIndex; if (path.StartsWith("untitled:", StringComparison.Ordinal)) { @@ -654,13 +655,24 @@ internal static string ConvertPathToDocumentUri(string path) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // On a Linux filesystem, you can have multiple colons in a filename e.g. foo:bar:baz.txt - return new Uri(path).AbsoluteUri.Replace(":", "%3A"); + 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 = + absoluteUri.Substring(0, firstColonIndex + 1) + + absoluteUri.Substring(firstColonIndex + 1).Replace(":", "%3A"); + } + + 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 = System.Web.HttpUtility.UrlPathEncode(path); - int colonIndex = path.IndexOf(':'); + colonIndex = path.IndexOf(':'); if (colonIndex > 0) { int driveLetterIndex = colonIndex - 1; From 5f2454dd40de828834154ae2560374c84388d8c6 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Fri, 8 Mar 2019 07:56:23 -0700 Subject: [PATCH 06/11] Try to fix tests for Linux --- .../Workspace/Workspace.cs | 10 +++------- .../Session/ScriptFileTests.cs | 17 +++++++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 0c2837a86..510db9869 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -640,7 +640,6 @@ private static string UnescapeDriveColon(string fileUri) internal static string ConvertPathToDocumentUri(string path) { const string fileUriPrefix = "file:///"; - int colonIndex; if (path.StartsWith("untitled:", StringComparison.Ordinal)) { @@ -671,8 +670,8 @@ internal static string ConvertPathToDocumentUri(string path) // 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 = System.Web.HttpUtility.UrlPathEncode(path); - colonIndex = path.IndexOf(':'); + string newUri = System.Web.HttpUtility.UrlPathEncode(path); + int colonIndex = path.IndexOf(':'); if (colonIndex > 0) { int driveLetterIndex = colonIndex - 1; @@ -681,10 +680,7 @@ internal static string ConvertPathToDocumentUri(string path) newUri = newUri.Insert(driveLetterIndex, driveLetter + "%3A"); } - return newUri - .Replace('\\', '/') - .Insert(0, fileUriPrefix) - .ToString(); + return newUri.Replace('\\', '/').Insert(0, fileUriPrefix); } #endregion diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index fbd31c59e..6d448cca7 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -547,7 +547,6 @@ public void PropertiesInitializedCorrectlyForFile() Assert.Equal(path, scriptFile.FilePath); Assert.Equal(path, scriptFile.ClientFilePath); - Assert.Equal("file:///TestFile.ps1", scriptFile.DocumentUri); Assert.True(scriptFile.IsAnalysisEnabled); Assert.False(scriptFile.IsInMemory); Assert.Empty(scriptFile.ReferencedFiles); @@ -590,13 +589,19 @@ public void DocumentUriRetunsCorrectStringForAbsolutePath() ScriptFile scriptFile; var emptyStringReader = new StringReader(""); - 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); - - if (Environment.OSVersion.Platform == PlatformID.Unix) + 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); From 262a2fa0c2889fac7832ae4ce6f64dea4f992900 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 10 Mar 2019 12:07:29 -0600 Subject: [PATCH 07/11] Address PR feedback --- .../Workspace/ScriptFile.cs | 2 +- .../Workspace/Workspace.cs | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index b31dd8cba..2c223b891 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -60,7 +60,7 @@ public string DocumentUri { get { - return (this.ClientFilePath == null ) + return (this.ClientFilePath == null) ? string.Empty : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); } diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 510db9869..9d6f4011c 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -660,9 +660,11 @@ internal static string ConvertPathToDocumentUri(string path) int firstColonIndex = absoluteUri.IndexOf(':'); if (absoluteUri.IndexOf(':', firstColonIndex + 1) >= 0) { - absoluteUri = - absoluteUri.Substring(0, firstColonIndex + 1) + - absoluteUri.Substring(firstColonIndex + 1).Replace(":", "%3A"); + absoluteUri = new StringBuilder() + .Append(absoluteUri, firstColonIndex + 1, absoluteUri.Length - firstColonIndex - 1) + .Replace(":", "%3A") + .Insert(0, absoluteUri.ToCharArray(0, firstColonIndex + 1)) + .ToString(); } return absoluteUri; @@ -670,17 +672,19 @@ internal static string ConvertPathToDocumentUri(string path) // 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. - string newUri = System.Web.HttpUtility.UrlPathEncode(path); + var newUri = new StringBuilder(System.Web.HttpUtility.UrlPathEncode(path)); int colonIndex = path.IndexOf(':'); if (colonIndex > 0) { int driveLetterIndex = colonIndex - 1; - var driveLetter = char.ToLowerInvariant(path[driveLetterIndex]).ToString(); - newUri = newUri.Remove(driveLetterIndex, 2); - newUri = newUri.Insert(driveLetterIndex, driveLetter + "%3A"); + char driveLetter = char.ToLowerInvariant(path[driveLetterIndex]); + newUri + .Remove(driveLetterIndex, 2) + .Insert(driveLetterIndex, driveLetter) + .Insert(driveLetterIndex + 1, "%3A"); } - return newUri.Replace('\\', '/').Insert(0, fileUriPrefix); + return newUri.Replace('\\', '/').Insert(0, fileUriPrefix).ToString(); } #endregion From 7be41a6b96a55fb37f6c30ba0012537ca63cf7d1 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 10 Mar 2019 12:24:37 -0600 Subject: [PATCH 08/11] Kick the build --- src/PowerShellEditorServices/Workspace/ScriptFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs index 2c223b891..c2806a7fb 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs @@ -60,7 +60,7 @@ public string DocumentUri { get { - return (this.ClientFilePath == null) + return this.ClientFilePath == null ? string.Empty : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); } From 6ced439e5a3525a790bba9f17287b4e7f9732021 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 10 Mar 2019 20:31:09 -0600 Subject: [PATCH 09/11] Address PR feedback, use StringBuilder.Replace() --- .../Workspace/Workspace.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 9d6f4011c..8f39372c1 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -660,10 +660,12 @@ internal static string ConvertPathToDocumentUri(string 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)) + absoluteUri = new StringBuilder(absoluteUri) + .Replace( + oldValue: ":", + newValue: "%3A", + startIndex: firstColonIndex + 1, + count: absoluteUri.Length - firstColonIndex - 1) .ToString(); } @@ -679,9 +681,8 @@ internal static string ConvertPathToDocumentUri(string path) int driveLetterIndex = colonIndex - 1; char driveLetter = char.ToLowerInvariant(path[driveLetterIndex]); newUri - .Remove(driveLetterIndex, 2) - .Insert(driveLetterIndex, driveLetter) - .Insert(driveLetterIndex + 1, "%3A"); + .Replace(path[driveLetterIndex], driveLetter, driveLetterIndex, 1) + .Replace(":", "%3A", colonIndex, 1); } return newUri.Replace('\\', '/').Insert(0, fileUriPrefix).ToString(); From ed9e57a08dc470fa3a54c1cce6988240ad3248fb Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Mon, 11 Mar 2019 11:43:58 -0600 Subject: [PATCH 10/11] Add test for backslash char on Linux/macOS. --- .../Session/ScriptFileTests.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 6d448cca7..24ca67104 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -598,13 +598,17 @@ public void DocumentUriRetunsCorrectStringForAbsolutePath() else { // Test the following only on Linux and macOS. - path = "/home/AmosBurton/projects/Rocinate/ProtoMolecule.ps1"; + path = "/home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1"; scriptFile = new ScriptFile(path, path, emptyStringReader, PowerShellVersion); - Assert.Equal("file:///home/AmosBurton/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri); + Assert.Equal("file:///home/AlexKamal/projects/Rocinate/ProtoMolecule.ps1", scriptFile.DocumentUri); - path = "/home/AmosBurton/projects/Rocinate/Proto:Mole:cule.ps1"; + path = "/home/NaomiNagata/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); + 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); } } } From b41ec6fe823275f5dd0145ba0c5f99ea241ecf1e Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 11 Mar 2019 14:53:13 -0600 Subject: [PATCH 11/11] Update src/PowerShellEditorServices/Workspace/Workspace.cs Co-Authored-By: rkeithhill --- src/PowerShellEditorServices/Workspace/Workspace.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 8f39372c1..571ea5c0e 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -658,7 +658,7 @@ internal static string ConvertPathToDocumentUri(string path) // 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) + if (absoluteUri.IndexOf(':', firstColonIndex + 1) > firstColonIndex) { absoluteUri = new StringBuilder(absoluteUri) .Replace(