Skip to content

Commit

Permalink
Fixing autocomplete (#278)
Browse files Browse the repository at this point in the history
* Refactoring - adding some sense into tuples

* Refactoring - adding unit tests project

* Test for AutocompleteTokenizer

* Added unit test for broken behavior

* Fixing the case sensitivity in autocomplete

* PR fixes
  • Loading branch information
Víťa Tauer authored Apr 27, 2021
1 parent 0a83acc commit 94be65b
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 24 deletions.
14 changes: 14 additions & 0 deletions Src/OrionSDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 20 additions & 20 deletions Src/SwqlStudio/Autocomplete/AutocompleteProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ namespace SwqlStudio.Autocomplete
// we are not reusing full swis grammar, we can do more 'educated guess' here.
internal class AutocompleteProvider
{
private static readonly HashSet<string> _keyWords = new HashSet<string>(Grammar.General);
private static readonly HashSet<string> _keyWords = new HashSet<string>(Grammar.General, StringComparer.OrdinalIgnoreCase);
private readonly string _text;

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
Expand All @@ -29,47 +29,47 @@ 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;
}
}


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:
throw new ArgumentOutOfRangeException();
}
}

private Tuple<string, LastInterestingElement> DoTheParsing(int caretPosition,
internal (string CurrentIdentifier, LastInterestingElement LastElement) DoTheParsing(int caretPosition,
IDictionary<string, string> aliasList)
{
string lastIdentifier = "";
Expand All @@ -80,11 +80,11 @@ private Tuple<string, LastInterestingElement> 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;
}
Expand All @@ -94,19 +94,19 @@ private Tuple<string, LastInterestingElement> 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;
underCaretIdentifier = lastIdentifier;
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.
{
Expand Down Expand Up @@ -149,7 +149,7 @@ private Tuple<string, LastInterestingElement> DoTheParsing(int caretPosition,
}


return Tuple.Create(underCaretIdentifier, underCaretInterestingElement);
return (underCaretIdentifier, underCaretInterestingElement);
}
}
}
9 changes: 5 additions & 4 deletions Src/SwqlStudio/Autocomplete/AutocompleteTokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum Token
private readonly string _input;
private static readonly IEnumerable<Regex> _ignoredRegexes;
private static readonly IEnumerable<Tuple<Regex, Token>> _regexes;

static AutocompleteTokenizer()
{
_ignoredRegexes = new[]
Expand All @@ -41,7 +42,7 @@ public AutocompleteTokenizer(string input)
_input = input;
}

public IEnumerator<Tuple<int, int, Token>> GetEnumerator()
public IEnumerator<(int Position, int Length, Token Token)> GetEnumerator()
{
int position = 0;
while (position < _input.Length)
Expand All @@ -61,18 +62,18 @@ public IEnumerator<Tuple<int, int, Token>> 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);
}
}
}
2 changes: 2 additions & 0 deletions Src/SwqlStudio/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")]
47 changes: 47 additions & 0 deletions Test/SwqlStudio.Tests/AutocompleteProviderTest.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> expected)
{
var provider = new AutocompleteProvider(query);
var aliases = new Dictionary<string, string>();
provider.DoTheParsing(9, aliases);

aliases.Should().BeEquivalentTo(expected);
}

public static IEnumerable<object[]> DoTheParsing_TestCases()
{
yield return new object[]
{
"select n. from Orion.Nodes n inner join Orion.Interfaces as i",
new Dictionary<string, string> {["n"] = "Orion.Nodes", ["i"] = "Orion.Interfaces"}
};

yield return new object[]
{
"SELECT n. FROM Orion.Nodes n INNER JOIN Orion.Interfaces AS i",
new Dictionary<string, string> {["n"] = "Orion.Nodes", ["i"] = "Orion.Interfaces"}
};

yield return new object[]
{
"select n. from Orion.Nodes n",
new Dictionary<string, string> {["n"] = "Orion.Nodes"}
};

}
}
}
41 changes: 41 additions & 0 deletions Test/SwqlStudio.Tests/AutocompleteTokenizerTest.cs
Original file line number Diff line number Diff line change
@@ -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),
});

}
}
}
27 changes: 27 additions & 0 deletions Test/SwqlStudio.Tests/SwqlStudio.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net48</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Src\SwqlStudio\SwqlStudio.csproj" />
</ItemGroup>

</Project>

0 comments on commit 94be65b

Please sign in to comment.