File: UriExtensions.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Net;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Utilities;
 
namespace Microsoft.CodeAnalysis.Razor;
 
internal static class UriExtensions
{
    /// <summary>
    ///  Converts the specified <see cref="Uri"/> into a file path that matches
    ///  a Roslyn <see cref="TextDocument.FilePath"/>.
    /// </summary>
    public static string GetDocumentFilePath(this Uri uri)
        => RazorUri.GetDocumentFilePathFromUri(uri);
 
    public static Uri GetRequiredParsedUri(this DocumentUri uri)
        => uri.ParsedUri.AssumeNotNull();
 
    public static string GetAbsoluteOrUNCPath(this DocumentUri uri)
        => GetAbsoluteOrUNCPath(uri.GetRequiredParsedUri());
 
    public static string GetAbsoluteOrUNCPath(this Uri uri)
    {
        ArgHelper.ThrowIfNull(uri, nameof(uri));
 
        if (uri.IsUnc)
        {
            // For UNC paths, AbsolutePath doesn't include the host name `//COMPUTERNAME/` part. So we need to use LocalPath instead.
            return uri.LocalPath;
        }
 
        // Absolute paths are usually encoded.
        var absolutePath = uri.AbsolutePath.Contains("%") ? WebUtility.UrlDecode(uri.AbsolutePath) : uri.AbsolutePath;
 
        return EnsureUniquePathForScheme(uri.Scheme, absolutePath);
    }
 
    private static string EnsureUniquePathForScheme(string scheme, string decodedAbsolutePath)
    {
        // File Uris we leave untouched, as they represent the actual files on disk
        if (scheme == Uri.UriSchemeFile)
        {
            return decodedAbsolutePath;
        }
 
        var normalizedPath = FilePathNormalizer.Normalize(decodedAbsolutePath);
        var firstSeparatorIndex = normalizedPath.IndexOf('/');
        if (firstSeparatorIndex < 0)
        {
            // A path without a separator is unlikely, but we can't add our unique-ness marker anyway, so just return as is
            // and hope for the best.
            return decodedAbsolutePath;
        }
 
        // For any non-file Uri, we add a marker to the path to ensure things won't conflict with the real file on disk.
        // This is a somewhat hacky fix to ensure things like a git diff view will work, where the left hand side is a
        // Uri like "git://path/to/file.razor" and the right hand side is "file://path/to/file.razor". If we mapped both
        // of those to the same file path, then one side would be sending line and character positions that don't match
        // our understanding of the document.
        // A true fix would be to move away from using file paths in the first place, but instead use the Uris as provided
        // by the LSP client as the source of truth.
        // See https://github.com/dotnet/razor/issues/9365 and https://github.com/microsoft/vscode-dotnettools/issues/2151
        // for examples.
 
        return normalizedPath.Insert(firstSeparatorIndex + 1, $"_{scheme}_/");
    }
}