File: ProtocolConversionsTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
using Range = Roslyn.LanguageServer.Protocol.Range;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests
{
    public class ProtocolConversionsTests : AbstractLanguageServerProtocolTests
    {
        public ProtocolConversionsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
        {
        }
 
        [Fact]
        public void CreateAbsoluteUri_LocalPaths_AllAscii()
        {
            var invalidFileNameChars = Path.GetInvalidFileNameChars();
            var unescaped = "!$&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~";
 
            for (var c = '\0'; c < '\u0080'; c++)
            {
                if (invalidFileNameChars.Contains(c))
                {
                    // no need to validate escaping for characters that can't appear in a file/directory name
                    continue;
                }
 
                var filePath = PathUtilities.IsUnixLikePlatform ? $"/_{c}/" : $"C:\\_{c}\\";
                var uriPrefix = PathUtilities.IsUnixLikePlatform ? "" : "C:/_";
 
                var expectedAbsoluteUri = "file:///" + uriPrefix + (unescaped.Contains(c) ? c : "%" + ((int)c).ToString("X2")) + "/";
 
                Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath));
 
                var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
                Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri);
                Assert.Equal(filePath, uri.LocalPath);
            }
        }
 
        [ConditionalTheory(typeof(WindowsOnly))]
        [InlineData("C:/", "file:///C:/")]
        [InlineData("C:\\", "file:///C:/")]
        [InlineData("C:\\a\\b", "file:///C:/a/b")]
        [InlineData("C:\\a\\\\b", "file:///C:/a//b")]
        [InlineData("C:\\%25\ue25b/a\\b", "file:///C:/%2525%EE%89%9B/a/b")]
        [InlineData("C:\\%25\ue25b/a\\\\b", "file:///C:/%2525%EE%89%9B/a//b")]
        [InlineData("C:\\\u0089\uC7BD", "file:///C:/%C2%89%EC%9E%BD")]
        [InlineData("/\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")]
        [InlineData("\\\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")]
        [InlineData("C:\\ !$&'()+,-;=@[]_~#", "file:///C:/%20!$&'()+,-;=@[]_~%23")]
        [InlineData("C:\\ !$&'()+,-;=@[]_~#\ue25b", "file:///C:/%20!$&'()+,-;=@[]_~%23%EE%89%9B")]
        [InlineData("C:\\\u0073\u0323\u0307", "file:///C:/s%CC%A3%CC%87")] // combining marks
        [InlineData("A:/\\\u200e//", "file:///A://%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487
        [InlineData("B:\\/\u200e", "file:///B://%E2%80%8E")]
        [InlineData("C:/\\\\-Ā\r", "file:///C:///-%C4%80%0D")]
        [InlineData("D:\\\\\\\\\\\u200e", "file:///D://///%E2%80%8E")]
        public void CreateAbsoluteUri_LocalPaths_Windows(string filePath, string expectedAbsoluteUri)
        {
            Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath));
 
            var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
            Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri);
            Assert.Equal(filePath.Replace('/', '\\'), uri.LocalPath);
        }
 
        [ConditionalTheory(typeof(WindowsOnly))]
        [InlineData("C:\\a\\.\\b", "file:///C:/a/./b", "file:///C:/a/b")]
        [InlineData("C:\\a\\..\\b", "file:///C:/a/../b", "file:///C:/b")]
        [InlineData("C:\\\ue25b\\.\\\ue25c", "file:///C:/%EE%89%9B/./%EE%89%9C", "file:///C:/%EE%89%9B/%EE%89%9C")]
        [InlineData("C:\\\ue25b\\..\\\ue25c", "file:///C:/%EE%89%9B/../%EE%89%9C", "file:///C:/%EE%89%9C")]
        public void CreateAbsoluteUri_LocalPaths_Normalized_Windows(string filePath, string expectedRawUri, string expectedNormalizedUri)
        {
            Assert.Equal(expectedRawUri, ProtocolConversions.GetAbsoluteUriString(filePath));
 
            var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
            Assert.Equal(expectedNormalizedUri, uri.AbsoluteUri);
            Assert.Equal(Path.GetFullPath(filePath).Replace('/', '\\'), uri.LocalPath);
        }
 
        [ConditionalTheory(typeof(UnixLikeOnly))]
        [InlineData("/", "file:///")]
        [InlineData("/u", "file:///u")]
        [InlineData("/unix/path", "file:///unix/path")]
        [InlineData("/%25\ue25b/\u0089\uC7BD", "file:///%2525%EE%89%9B/%C2%89%EC%9E%BD")]
        [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23")]
        [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23%EE%89%9B")]
        [InlineData("/\\\u200e//", "file:////%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487
        [InlineData("\\/\u200e", "file:////%E2%80%8E")]
        [InlineData("/\\\\-Ā\r", "file://///-%C4%80%0D")]
        [InlineData("\\\\\\\\\\\u200e", "file:///////%E2%80%8E")]
        public void CreateAbsoluteUri_LocalPaths_Unix(string filePath, string expectedAbsoluteUri)
        {
            Assert.Equal(expectedAbsoluteUri, ProtocolConversions.GetAbsoluteUriString(filePath));
 
            var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
            Assert.Equal(expectedAbsoluteUri, uri.AbsoluteUri);
            Assert.Equal(filePath, uri.LocalPath);
        }
 
        [ConditionalTheory(typeof(WindowsOnly))]
        [InlineData("C:\\a\\b", "file:///C:/a/b")]
        [InlineData("C:\\a\\b\\", "file:///C:/a/b")]
        [InlineData("C:\\a\\\\b", "file:///C:/a//b")]
        [InlineData("C:\\%25\ue25b/a\\b", "file:///C:/%2525%EE%89%9B/a/b")]
        [InlineData("C:\\%25\ue25b/a\\\\b", "file:///C:/%2525%EE%89%9B/a//b")]
        [InlineData("C:\\\u0089\uC7BD", "file:///C:/%C2%89%EC%9E%BD")]
        [InlineData("/\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")]
        [InlineData("\\\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")]
        [InlineData("\\\\server\ue25b\\%25\ue25b\\b\\", "file://server/%2525%EE%89%9B/b")]
        [InlineData("C:\\ !$&'()+,-;=@[]_~#", "file:///C:/%20!$&'()+,-;=@[]_~%23")]
        [InlineData("C:\\ !$&'()+,-;=@[]_~#\ue25b", "file:///C:/%20!$&'()+,-;=@[]_~%23%EE%89%9B")]
        [InlineData("C:\\\u0073\u0323\u0307", "file:///C:/s%CC%A3%CC%87")] // combining marks
        [InlineData("A:/\\\u200e//", "file:///A://%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487
        [InlineData("B:\\/\u200e", "file:///B://%E2%80%8E")]
        [InlineData("C:/\\\\-Ā\r", "file:///C:///-%C4%80%0D")]
        [InlineData("D:\\\\\\\\\\\u200e", "file:///D://///%E2%80%8E")]
        public void CreateRelativePatternBaseUri_LocalPaths_Windows(string filePath, string expectedUri)
        {
            var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath);
            Assert.Equal(expectedUri, uri.AbsoluteUri);
        }
 
        [ConditionalTheory(typeof(UnixLikeOnly))]
        [InlineData("/", "file://")]
        [InlineData("/u", "file:///u")]
        [InlineData("/unix/", "file:///unix")]
        [InlineData("/unix/path", "file:///unix/path")]
        [InlineData("/%25\ue25b/\u0089\uC7BD", "file:///%2525%EE%89%9B/%C2%89%EC%9E%BD")]
        [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23")]
        [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23%EE%89%9B")]
        [InlineData("/\\\u200e//", "file:////%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487
        [InlineData("\\/\u200e", "file:////%E2%80%8E")]
        [InlineData("/\\\\-Ā\r", "file://///-%C4%80%0D")]
        [InlineData("\\\\\\\\\\\u200e", "file:///////%E2%80%8E")]
        public void CreateRelativePatternBaseUri_LocalPaths_Unix(string filePath, string expectedRelativeUri)
        {
            var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath);
            Assert.Equal(expectedRelativeUri, uri.AbsoluteUri);
        }
 
        [ConditionalTheory(typeof(UnixLikeOnly))]
        [InlineData("/a/./b", "file:///a/./b", "file:///a/b")]
        [InlineData("/a/../b", "file:///a/../b", "file:///b")]
        [InlineData("/\ue25b/./\ue25c", "file:///%EE%89%9B/./%EE%89%9C", "file:///%EE%89%9B/%EE%89%9C")]
        [InlineData("/\ue25b/../\ue25c", "file:///%EE%89%9B/../%EE%89%9C", "file:///%EE%89%9C")]
        public void CreateAbsoluteUri_LocalPaths_Normalized_Unix(string filePath, string expectedRawUri, string expectedNormalizedUri)
        {
            Assert.Equal(expectedRawUri, ProtocolConversions.GetAbsoluteUriString(filePath));
 
            var uri = ProtocolConversions.CreateAbsoluteUri(filePath);
            Assert.Equal(expectedNormalizedUri, uri.AbsoluteUri);
            Assert.Equal(filePath, uri.LocalPath);
        }
 
        [Theory]
        [InlineData("git:/x:/%2525%EE%89%9B/%C2%89%EC%9E%BD?abc")]
        [InlineData("git://host/%2525%EE%89%9B/%C2%89%EC%9E%BD")]
        [InlineData("xy://host/%2525%EE%89%9B/%C2%89%EC%9E%BD")]
        public void CreateAbsoluteUri_Urls(string url)
        {
            Assert.Equal(url, ProtocolConversions.CreateAbsoluteUri(url).AbsoluteUri);
        }
 
        [Fact]
        public void CompletionItemKind_DoNotUseMethodAndFunction()
        {
            var map = ProtocolConversions.RoslynTagToCompletionItemKinds;
            var containsMethod = map.Values.Any(c => c.Contains(CompletionItemKind.Method));
            var containsFunction = map.Values.Any(c => c.Contains(CompletionItemKind.Function));
 
            Assert.False(containsFunction && containsMethod, "Don't use Method and Function completion item kinds as it causes user confusion.");
        }
 
        [Fact]
        public void RangeToTextSpanStartWithNextLine()
        {
            var markup = GetTestMarkup();
 
            var sourceText = SourceText.From(markup);
            var range = new Range() { Start = new Position(0, 0), End = new Position(1, 0) };
            var textSpan = ProtocolConversions.RangeToTextSpan(range, sourceText);
 
            // End should be start of the second line
            Assert.Equal(0, textSpan.Start);
            Assert.Equal(10, textSpan.End);
        }
 
        [Fact]
        public void RangeToTextSpanMidLine()
        {
            var markup = GetTestMarkup();
            var sourceText = SourceText.From(markup);
 
            // Take just "x = 5"
            var range = new Range() { Start = new Position(2, 8), End = new Position(2, 12) };
            var textSpan = ProtocolConversions.RangeToTextSpan(range, sourceText);
 
            Assert.Equal(21, textSpan.Start);
            Assert.Equal(25, textSpan.End);
        }
 
        [Fact]
        public void RangeToTextSpanLineEndOfDocument()
        {
            var markup = GetTestMarkup();
            var sourceText = SourceText.From(markup);
 
            var range = new Range() { Start = new Position(0, 0), End = new Position(3, 1) };
            var textSpan = ProtocolConversions.RangeToTextSpan(range, sourceText);
 
            Assert.Equal(0, textSpan.Start);
            Assert.Equal(30, textSpan.End);
        }
 
        [Fact]
        public void RangeToTextSpanLineEndOfDocumentWithEndOfLineChars()
        {
            var markup =
@"void M()
{
    var x = 5;
}
"; // add additional end line 
 
            var sourceText = SourceText.From(markup);
 
            var range = new Range() { Start = new Position(0, 0), End = new Position(4, 0) };
            var textSpan = ProtocolConversions.RangeToTextSpan(range, sourceText);
 
            // Result now includes end of line characters for line 3
            Assert.Equal(0, textSpan.Start);
            Assert.Equal(32, textSpan.End);
        }
 
        [Fact]
        public void RangeToTextSpanLineOutOfRangeError()
        {
            var markup = GetTestMarkup();
            var sourceText = SourceText.From(markup);
 
            var range = new Range() { Start = new Position(0, 0), End = new Position(sourceText.Lines.Count, 0) };
            Assert.Throws<ArgumentException>(() => ProtocolConversions.RangeToTextSpan(range, sourceText));
        }
 
        [Fact]
        public void RangeToTextSpanEndAfterStartError()
        {
            var markup = GetTestMarkup();
            var sourceText = SourceText.From(markup);
 
            // This start position will be beyond the end position
            var range = new Range() { Start = new Position(2, 20), End = new Position(3, 0) };
            Assert.Throws<ArgumentException>(() => ProtocolConversions.RangeToTextSpan(range, sourceText));
        }
 
        private static string GetTestMarkup()
        {
            // Markup is 31 characters long. Line break (\n) is 2 characters 
            /*
            void M()        [Line = 0; Start = 0; End = 8; End including line break = 10]
            {               [Line = 1; Start = 10; End = 11; End including line break = 13]
                var x = 5;  [Line = 2; Start = 13; End = 27; End including line break = 29]
            }               [Line = 3; Start = 29; End = 30; End including line break = 30]
             */
 
            var markup =
@"void M()
{
    var x = 5;
}";
            return markup;
        }
 
        [Theory, CombinatorialData]
        public async Task ProjectToProjectContext_HostWorkspace(bool mutatingLspWorkspace)
        {
            var source = """
                class {|caret:A|}
                {
                    void M()
                    {
                    }
                }
                """;
 
            // Create a server with an existing file.
            await using var testLspServer = await CreateTestLspServerAsync(source, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
            var caret = testLspServer.GetLocations("caret").Single();
 
            var document = await GetTextDocumentAsync(testLspServer, caret.Uri);
            Assert.NotNull(document);
 
            var projectContext = ProtocolConversions.ProjectToProjectContext(document.Project);
 
            Assert.False(projectContext.IsMiscellaneous);
        }
 
        [Theory, CombinatorialData]
        public async Task ProjectToProjectContext_MiscellaneousFilesWorkspace(bool mutatingLspWorkspace)
        {
            var source = """
                class A
                {
                    void M()
                    {
                    }
                }
                """;
 
            // Create a server that supports LSP misc files.
            await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
 
            // Open an empty loose file.
            var looseFileUri = ProtocolConversions.CreateAbsoluteUri(@"C:\SomeFile.cs");
            await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false);
 
            var document = await GetTextDocumentAsync(testLspServer, looseFileUri);
            Assert.NotNull(document);
 
            var projectContext = ProtocolConversions.ProjectToProjectContext(document.Project);
 
            Assert.True(projectContext.IsMiscellaneous);
        }
 
        internal static async Task<TextDocument?> GetTextDocumentAsync(TestLspServer testLspServer, Uri uri)
        {
            var (_, _, textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new TextDocumentIdentifier { Uri = uri }, CancellationToken.None);
            return textDocument;
        }
    }
}