|
// 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.IO;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Testing;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor;
public class LanguageConfigurationTest(ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Theory]
[InlineData("""<ValidationMessage>""", true)]
[InlineData("""<ValidationMessage Attr="Value">""", true)]
[InlineData("""<ValidationMessage For="() => Input.Username" class="text-danger">""", true)]
[InlineData("""<ValidationMessage />""", false)]
[InlineData("""<ValidationMessage Attr="Value" />""", false)]
[InlineData("""<ValidationMessage For="() => Input.Username" class="text-danger" />""", false)]
[InlineData("""<div dir="@(1 > 2 ? "ltr" : "rtl")">""", true)]
[InlineData("""<table title="@(1 > 2 ? "re\"d" : "blue")">""", true)]
[InlineData("""<div dir="@(1 > 2 ? "ltr" : "rtl")"/>""", false)]
[InlineData("""<table title="@(1 > 2 ? "re\"d" : "blue")"/>""", false)]
// Lines with closing tag on same line should NOT increase indent
[InlineData("""<button stuff></button>""", false)]
[InlineData("""<div></div>""", false)]
[InlineData("""<div class="hello"></div>""", false)]
[InlineData("""<ValidationMessage For="() => Input.Username" class="text-danger"></ValidationMessage>""", false)]
// Void elements should NOT increase indent
[InlineData("""<br>""", false)]
[InlineData("""<hr>""", false)]
[InlineData("""<input>""", false)]
[InlineData("""<img src="foo.png">""", false)]
public void ShouldIncreaseIndentation(string input, bool expected)
{
var langConfig = GetLanguageConfigurationJson();
var rules = langConfig["indentationRules"];
Assert.NotNull(rules);
var pattern = rules.Value<string>("increaseIndentPattern");
Assert.NotNull(pattern);
var isMatch = IsMatch(input, pattern);
Assert.Equal(expected, isMatch);
}
[Theory]
[InlineData("""<div>$$""")]
[InlineData("""<div>$$</div>""")]
[InlineData("""<div class="hello">$$""")]
[InlineData("""<div class="hello">$$</div>""")]
[InlineData("""<div class="@(() => true)">$$""")]
[InlineData("""<div class="@(() => true)">$$</div>""")]
[InlineData("""<PropertyColumn Value="() => true" >$$""")]
[InlineData("""<PropertyColumn Value="() => true" >$$</PropertyColumn>""")]
public void OnEnter_WillIndent(string input)
{
TestFileMarkupParser.GetPosition(input, out input, out var position);
Assert.True(WillIndent(input, position));
}
[Theory]
[InlineData("""<input>$$""")]
[InlineData("""<input />$$""")]
[InlineData("""<PropertyColumn Value="() => true" />$$""")]
[InlineData("""<PropertyColumn />$$""")]
public void OnEnter_WontIndent(string input)
{
TestFileMarkupParser.GetPosition(input, out input, out var position);
Assert.False(WillIndent(input, position));
}
public bool WillIndent(string input, int position)
{
var langConfig = GetLanguageConfigurationJson();
var onEnterRules = langConfig["onEnterRules"]!;
foreach (var rule in onEnterRules)
{
var beforePattern = rule.Value<string>("beforeText");
var afterPattern = rule.Value<string>("afterText");
var before = input.Substring(0, position);
var after = input.Substring(position);
Assert.NotNull(beforePattern);
if (IsMatch(before, beforePattern))
{
_output.WriteLine("Matched beforeText pattern: " + beforePattern);
if (afterPattern is null)
{
_output.WriteLine("No afterText pattern found. Match!");
return true;
}
else if (IsMatch(after, afterPattern))
{
_output.WriteLine("Matched afterText pattern: " + afterPattern);
_output.WriteLine("Match!");
return true;
}
_output.WriteLine("No match on afterText pattern.");
}
}
_output.WriteLine("No match on any pattern.");
return false;
}
private static bool IsMatch(string input, string pattern)
{
// Matches VS behaviour when reading our language-configuration.json
// https://devdiv.visualstudio.com/DevDiv/_git/VSEditor?path=/src/Productivity/TextMate/Core/LanguageConfiguration/Impl/FastRegexConverter.cs&version=GBmain&line=27&lineEnd=28&lineStartColumn=1&lineEndColumn=1&lineStyle=plain&_a=contents
return Regex.IsMatch(input, pattern, RegexOptions.Compiled | RegexOptions.ECMAScript, TimeSpan.FromMilliseconds(1000));
}
private static JObject GetLanguageConfigurationJson()
{
var langConfigFile = GetLanguageConfigurationJsonPath();
return JObject.Parse(File.ReadAllText(langConfigFile));
}
private static string GetLanguageConfigurationJsonPath()
{
var appContextBaseDirectory = AppContext.BaseDirectory;
var currentDirectory = Environment.CurrentDirectory;
var langConfigFile = TryFindLanguageConfigurationFile(appContextBaseDirectory)
?? (string.Equals(currentDirectory, appContextBaseDirectory, StringComparison.OrdinalIgnoreCase)
? null
: TryFindLanguageConfigurationFile(currentDirectory));
if (langConfigFile is null)
{
throw new InvalidOperationException($"Could not locate language-configuration.json from '{appContextBaseDirectory}' or '{currentDirectory}'.");
}
return langConfigFile;
}
private static string? TryFindLanguageConfigurationFile(string baseDirectory)
{
if (string.IsNullOrWhiteSpace(baseDirectory) || !Directory.Exists(baseDirectory))
{
return null;
}
var outputLocalPath = Path.Combine(baseDirectory, "language-configuration.json");
if (File.Exists(outputLocalPath))
{
return outputLocalPath;
}
var repoRoot = SearchUp(baseDirectory, "global.json");
if (repoRoot is not null)
{
var razorRepoRoot = Directory.Exists(Path.Combine(repoRoot, "src", "Razor", "src"))
? Path.Combine(repoRoot, "src", "Razor")
: repoRoot;
var repoPath = Path.Combine(razorRepoRoot, "src", "Microsoft.VisualStudio.RazorExtension", "language-configuration.json");
if (File.Exists(repoPath))
{
return repoPath;
}
}
foreach (var candidatePath in Directory.EnumerateFiles(baseDirectory, "language-configuration.json", SearchOption.AllDirectories))
{
return candidatePath;
}
return null;
}
private static string? SearchUp(string baseDirectory, string fileName)
{
for (var current = new DirectoryInfo(baseDirectory); current is not null; current = current.Parent)
{
if (File.Exists(Path.Combine(current.FullName, fileName)))
{
return current.FullName;
}
}
return null;
}
}
|