diff --git a/Src/OrionSDK.sln b/Src/OrionSDK.sln index 0b50c5b1f..c9c0c58a0 100644 --- a/Src/OrionSDK.sln +++ b/Src/OrionSDK.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InformationService.Contract EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwisPowerShell.Tests", "..\Test\SwisPowerShell.Tests\SwisPowerShell.Tests.csproj", "{164BEA87-FC26-4588-99AA-2F47174AE368}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwqlStudio.Tests", "..\Test\SwqlStudio.Tests\SwqlStudio.Tests.csproj", "{763D0053-96D4-44C8-B68C-C3EE1D9D406F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,18 @@ Global {164BEA87-FC26-4588-99AA-2F47174AE368}.Release|Mixed Platforms.Build.0 = Release|Any CPU {164BEA87-FC26-4588-99AA-2F47174AE368}.Release|x86.ActiveCfg = Release|Any CPU {164BEA87-FC26-4588-99AA-2F47174AE368}.Release|x86.Build.0 = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|x86.ActiveCfg = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Debug|x86.Build.0 = Debug|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|Any CPU.Build.0 = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|x86.ActiveCfg = Release|Any CPU + {763D0053-96D4-44C8-B68C-C3EE1D9D406F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Src/SwqlStudio/Autocomplete/AutocompleteProvider.cs b/Src/SwqlStudio/Autocomplete/AutocompleteProvider.cs index b44618b8a..388eebb21 100644 --- a/Src/SwqlStudio/Autocomplete/AutocompleteProvider.cs +++ b/Src/SwqlStudio/Autocomplete/AutocompleteProvider.cs @@ -7,7 +7,7 @@ namespace SwqlStudio.Autocomplete // we are not reusing full swis grammar, we can do more 'educated guess' here. internal class AutocompleteProvider { - private static readonly HashSet _keyWords = new HashSet(Grammar.General); + private static readonly HashSet _keyWords = new HashSet(Grammar.General, StringComparer.OrdinalIgnoreCase); private readonly string _text; public AutocompleteProvider(string text) @@ -15,7 +15,7 @@ public AutocompleteProvider(string text) _text = text; } - private enum LastInterestingElement + internal enum LastInterestingElement { Nothing, // nothing interesting Dot, // last thing was dot, so when we find identifier, we append @@ -29,30 +29,30 @@ public ExpectedCaretPosition ParseFor(int caretPosition) var rv = DoTheParsing(caretPosition, aliasList); string possibleAlias; - switch (rv.Item2) + switch (rv.LastElement) { case LastInterestingElement.Nothing: return new ExpectedCaretPosition(ExpectedCaretPositionType.Entity | ExpectedCaretPositionType.Keyword, null); case LastInterestingElement.Dot: - if (!aliasList.TryGetValue(rv.Item1, out possibleAlias)) + if (!aliasList.TryGetValue(rv.CurrentIdentifier, out possibleAlias)) { // expand alias for navigation property // n.blahblah becomes node.blahblah (if we have alias node n) - if (rv.Item1.Contains('.')) + if (rv.CurrentIdentifier.Contains('.')) { - var firstPortion = rv.Item1.Substring(0, rv.Item1.IndexOf('.')); + var firstPortion = rv.CurrentIdentifier.Substring(0, rv.CurrentIdentifier.IndexOf('.')); if (aliasList.TryGetValue(firstPortion, out possibleAlias)) { - possibleAlias = possibleAlias + rv.Item1.Substring(rv.Item1.IndexOf('.')); + possibleAlias = possibleAlias + rv.CurrentIdentifier.Substring(rv.CurrentIdentifier.IndexOf('.')); } else { - possibleAlias = rv.Item1; + possibleAlias = rv.CurrentIdentifier; } } else { - possibleAlias = rv.Item1; + possibleAlias = rv.CurrentIdentifier; } } @@ -60,8 +60,8 @@ public ExpectedCaretPosition ParseFor(int caretPosition) return new ExpectedCaretPosition(ExpectedCaretPositionType.Column, possibleAlias); case LastInterestingElement.As: - if (!aliasList.TryGetValue(rv.Item1, out possibleAlias)) - possibleAlias = rv.Item1; + if (!aliasList.TryGetValue(rv.CurrentIdentifier, out possibleAlias)) + possibleAlias = rv.CurrentIdentifier; return new ExpectedCaretPosition(ExpectedCaretPositionType.Keyword | ExpectedCaretPositionType.Column, possibleAlias); default: @@ -69,7 +69,7 @@ public ExpectedCaretPosition ParseFor(int caretPosition) } } - private Tuple DoTheParsing(int caretPosition, + internal (string CurrentIdentifier, LastInterestingElement LastElement) DoTheParsing(int caretPosition, IDictionary aliasList) { string lastIdentifier = ""; @@ -80,11 +80,11 @@ private Tuple DoTheParsing(int caretPosition, bool detected = false; - foreach (var tok in new AutocompleteTokenizer(_text)) + foreach ((int position, int length, var token) in new AutocompleteTokenizer(_text)) { - if (tok.Item3 == AutocompleteTokenizer.Token.Special) + if (token == AutocompleteTokenizer.Token.Special) { - if (_text[tok.Item1] == '.') + if (_text[position] == '.') { lastInterestingElement = LastInterestingElement.Dot; } @@ -94,7 +94,7 @@ private Tuple DoTheParsing(int caretPosition, } } - if (!detected && tok.Item1 <= caretPosition && (tok.Item1 + tok.Item2) >= caretPosition) + if (!detected && position <= caretPosition && (position + length) >= caretPosition) { // here we are. what do we see right now? detected = true; @@ -102,11 +102,11 @@ private Tuple DoTheParsing(int caretPosition, underCaretInterestingElement = lastInterestingElement; } - switch (tok.Item3) + switch (token) { case AutocompleteTokenizer.Token.Identifier: - var value = _text.Substring(tok.Item1, tok.Item2); - if (value == "as") + var value = _text.Substring(position, length); + if (string.Equals(value, "as", StringComparison.OrdinalIgnoreCase)) // alias. only interesting keyword for us. however, ignore, since Table X and Table as X are equivalent. // this may mean someone writing SELECT A B FROM D - A B are aliases - but, whatever. Full scan would be much worse. { @@ -149,7 +149,7 @@ private Tuple DoTheParsing(int caretPosition, } - return Tuple.Create(underCaretIdentifier, underCaretInterestingElement); + return (underCaretIdentifier, underCaretInterestingElement); } } } diff --git a/Src/SwqlStudio/Autocomplete/AutocompleteTokenizer.cs b/Src/SwqlStudio/Autocomplete/AutocompleteTokenizer.cs index 562df3e89..f877b66fd 100644 --- a/Src/SwqlStudio/Autocomplete/AutocompleteTokenizer.cs +++ b/Src/SwqlStudio/Autocomplete/AutocompleteTokenizer.cs @@ -18,6 +18,7 @@ public enum Token private readonly string _input; private static readonly IEnumerable _ignoredRegexes; private static readonly IEnumerable> _regexes; + static AutocompleteTokenizer() { _ignoredRegexes = new[] @@ -41,7 +42,7 @@ public AutocompleteTokenizer(string input) _input = input; } - public IEnumerator> GetEnumerator() + public IEnumerator<(int Position, int Length, Token Token)> GetEnumerator() { int position = 0; while (position < _input.Length) @@ -61,18 +62,18 @@ public IEnumerator> GetEnumerator() var m = rx.Item1.Match(_input, position); if (m.Success) { - yield return Tuple.Create(position, m.Groups[0].Length, rx.Item2); + yield return (position, m.Groups[0].Length, rx.Item2); position += m.Groups[0].Length; goto end; } } - yield return Tuple.Create(position, 1, Token.Special); + yield return (position, 1, Token.Special); position++; end: ; } - yield return Tuple.Create(position, 0, Token.EOF); + yield return (position, 0, Token.EOF); } } } diff --git a/Src/SwqlStudio/Properties/AssemblyInfo.cs b/Src/SwqlStudio/Properties/AssemblyInfo.cs index 60e9f1ce9..b482dd89f 100644 --- a/Src/SwqlStudio/Properties/AssemblyInfo.cs +++ b/Src/SwqlStudio/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -18,3 +19,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("85e57129-3aa6-4c0c-9532-c1167a4195cc")] +[assembly: InternalsVisibleTo("SwqlStudio.Tests")] diff --git a/Test/SwqlStudio.Tests/AutocompleteProviderTest.cs b/Test/SwqlStudio.Tests/AutocompleteProviderTest.cs new file mode 100644 index 000000000..d31c7c669 --- /dev/null +++ b/Test/SwqlStudio.Tests/AutocompleteProviderTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using SwqlStudio.Autocomplete; +using Xunit; + +namespace SwqlStudio.Tests +{ + public class AutocompleteProviderTest + { + [Theory] + [MemberData(nameof(DoTheParsing_TestCases))] + public void DoTheParsing_IdentifiesAliases(string query, Dictionary expected) + { + var provider = new AutocompleteProvider(query); + var aliases = new Dictionary(); + provider.DoTheParsing(9, aliases); + + aliases.Should().BeEquivalentTo(expected); + } + + public static IEnumerable DoTheParsing_TestCases() + { + yield return new object[] + { + "select n. from Orion.Nodes n inner join Orion.Interfaces as i", + new Dictionary {["n"] = "Orion.Nodes", ["i"] = "Orion.Interfaces"} + }; + + yield return new object[] + { + "SELECT n. FROM Orion.Nodes n INNER JOIN Orion.Interfaces AS i", + new Dictionary {["n"] = "Orion.Nodes", ["i"] = "Orion.Interfaces"} + }; + + yield return new object[] + { + "select n. from Orion.Nodes n", + new Dictionary {["n"] = "Orion.Nodes"} + }; + + } + } +} diff --git a/Test/SwqlStudio.Tests/AutocompleteTokenizerTest.cs b/Test/SwqlStudio.Tests/AutocompleteTokenizerTest.cs new file mode 100644 index 000000000..e822c945c --- /dev/null +++ b/Test/SwqlStudio.Tests/AutocompleteTokenizerTest.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using SwqlStudio.Autocomplete; +using Xunit; + +namespace SwqlStudio.Tests +{ + public class AutocompleteTokenizerTest + { + [Fact] + public void TokenizerTestSuite() + { + var tokenizer = new AutocompleteTokenizer( + "SELECT n. Orion.Nodes 123 \"abc\""); + + IEnumerable<(int Position, int Length, AutocompleteTokenizer.Token Token)> Enumerate() + { + foreach(var t in tokenizer) + yield return t; + } + + Enumerate().Should().BeEquivalentTo(new [] + { + (0, 6, AutocompleteTokenizer.Token.Identifier), + (7, 1, AutocompleteTokenizer.Token.Identifier), + (8, 1, AutocompleteTokenizer.Token.Special), + (10, 5, AutocompleteTokenizer.Token.Identifier), + (15, 1, AutocompleteTokenizer.Token.Special), + (16, 5, AutocompleteTokenizer.Token.Identifier), + (22, 4, AutocompleteTokenizer.Token.Number), + (26, 5, AutocompleteTokenizer.Token.String), + (31, 0, AutocompleteTokenizer.Token.EOF), + }); + + } + } +} diff --git a/Test/SwqlStudio.Tests/SwqlStudio.Tests.csproj b/Test/SwqlStudio.Tests/SwqlStudio.Tests.csproj new file mode 100644 index 000000000..2c4b3e19b --- /dev/null +++ b/Test/SwqlStudio.Tests/SwqlStudio.Tests.csproj @@ -0,0 +1,27 @@ + + + + net48 + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +