Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing autocomplete #278

Merged
merged 6 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes it so much easier to read. Thanks!

{
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>