diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs index 32b8301e..93136013 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs @@ -27,6 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting /// prior to the process shutting down. /// private const int ShutdownTimeoutInSeconds = 120; + public static readonly string[] CompletionTriggerCharacters = new string[] { ".", "-", ":", "\\", "[", "\"" }; #region Singleton Instance Code @@ -135,7 +136,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting /// /// /// - private async Task HandleInitializeRequest(InitializeRequest initializeParams, RequestContext requestContext) + internal async Task HandleInitializeRequest(InitializeRequest initializeParams, RequestContext requestContext) { // Call all tasks that registered on the initialize request var initializeTasks = initializeCallbacks.Select(t => t(initializeParams, requestContext)); @@ -157,7 +158,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting CompletionProvider = new CompletionOptions { ResolveProvider = true, - TriggerCharacters = new string[] { ".", "-", ":", "\\", "[" } + TriggerCharacters = CompletionTriggerCharacters }, SignatureHelpProvider = new SignatureHelpOptions { diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs index 49e3fd44..59a25c05 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs @@ -3,8 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; @@ -19,6 +19,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion public class SqlCompletionItem { private static Regex ValidSqlNameRegex = new Regex(@"^[\p{L}_@][\p{L}\p{N}@$#_]{0,127}$"); + private static DelimitedIdentifier BracketedIdentifiers = new DelimitedIdentifier { Start = "[", End = "]"}; + private static DelimitedIdentifier[] DelimitedIdentifiers = + new DelimitedIdentifier[] { BracketedIdentifiers, new DelimitedIdentifier {Start = "\"", End = "\"" } }; /// /// Create new instance given the SQL parser declaration @@ -44,12 +47,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion private void Init() { - InsertText = GetCompletionItemInsertName(); + InsertText = DeclarationTitle; + DelimitedIdentifier delimitedIdentifier = GetDelimitedIdentifier(TokenText); Label = DeclarationTitle; - if (StartsWithBracket(TokenText) || AutoCompleteHelper.IsReservedWord(InsertText)) + + if (delimitedIdentifier == null && !string.IsNullOrEmpty(DeclarationTitle) && + (!ValidSqlNameRegex.IsMatch(DeclarationTitle) || AutoCompleteHelper.IsReservedWord(InsertText))) { - Label = WithBracket(Label); - InsertText = WithBracket(InsertText); + InsertText = WithDelimitedIdentifier(BracketedIdentifiers, DeclarationTitle); + } + if (delimitedIdentifier != null) + { + Label = WithDelimitedIdentifier(delimitedIdentifier, Label); + InsertText = WithDelimitedIdentifier(delimitedIdentifier, InsertText); } Detail = Label; Kind = CreateCompletionItemKind(); @@ -143,7 +153,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion int row, int startColumn, int endColumn) - { + { CompletionItem item = new CompletionItem() { Label = label, @@ -172,31 +182,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion return item; } - private string GetCompletionItemInsertName() + private bool HasDelimitedIdentifier(DelimitedIdentifier delimiteIidentifier, string text) { - string insertText = DeclarationTitle; - if (!string.IsNullOrEmpty(DeclarationTitle) && !ValidSqlNameRegex.IsMatch(DeclarationTitle)) + return text != null && delimiteIidentifier != null && text.StartsWith(delimiteIidentifier.Start) + && text.EndsWith(delimiteIidentifier.End); + } + + private DelimitedIdentifier GetDelimitedIdentifier(string text) + { + return text != null ? DelimitedIdentifiers.FirstOrDefault(x => text.StartsWith(x.Start)) : null; + } + + private string WithDelimitedIdentifier(DelimitedIdentifier delimiteIidentifier, string text) + { + if (!HasDelimitedIdentifier(delimiteIidentifier, text)) { - insertText = WithBracket(DeclarationTitle); - } - return insertText; - } - - private bool HasBrackets(string text) - { - return text != null && text.StartsWith("[") && text.EndsWith("]"); - } - - private bool StartsWithBracket(string text) - { - return text != null && text.StartsWith("["); - } - - private string WithBracket(string text) - { - if (!HasBrackets(text)) - { - return string.Format(CultureInfo.InvariantCulture, "[{0}]", text); + return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", delimiteIidentifier.Start, text, delimiteIidentifier.End); } else { @@ -204,4 +205,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion } } } + + internal class DelimitedIdentifier + { + public string Start { get; set; } + public string End { get; set; } + } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index 35f697d1..793280ba 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -161,17 +161,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer internal class TestScriptDocumentInfo : ScriptDocumentInfo { - public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo) + public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo, + string tokenText = null) :base(textDocumentPosition, scriptFile, scriptParseInfo) { - + this.tokenText = string.IsNullOrEmpty(tokenText) ? "doesntmatchanythingintheintellisensedefaultlist" : tokenText; } + + private string tokenText; public override string TokenText { get { - return "doesntmatchanythingintheintellisensedefaultlist"; + return this.tokenText; } } } @@ -194,5 +197,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false); } + + [Fact] + public void GetDefaultCompletionListWithMatchesTest() + { + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents("koko wants a bananas"); + + ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = false }; + + var scriptDocumentInfo = new TestScriptDocumentInfo( + new TextDocumentPosition() + { + TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri }, + Position = new Position() { Line = 0, Character = 0 } + }, scriptFile, scriptInfo, "all"); + + CompletionItem[] result = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false); + Assert.Equal(result.Length, 1); + + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs index a1030578..f5f3486e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs @@ -3,8 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using Microsoft.SqlServer.Management.SqlParser.Intellisense; -using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Xunit; @@ -325,6 +325,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer Assert.True(completionItem.InsertText.StartsWith("[") && completionItem.InsertText.EndsWith("]")); } + [Fact] + public void ConstructorShouldThrowExceptionGivenEmptyDeclarionType() + { + string declarationTitle = ""; + DeclarationType declarationType = DeclarationType.Table; + string tokenText = ""; + Assert.Throws(() => new SqlCompletionItem(declarationTitle, declarationType, tokenText)); + } + + [Fact] + public void ConstructorShouldThrowExceptionGivenNullDeclarion() + { + string tokenText = ""; + Assert.Throws(() => new SqlCompletionItem(null, tokenText)); + } + [Fact] public void InsertTextShouldIncludeBracketGivenNameWithSpecialCharacter() { @@ -431,23 +447,83 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer } [Fact] - public void LabelShouldIncBracketGivenReservedName() + public void LabelShouldIncludeQuotedIdentifiersGivenTokenWithQuotedIdentifier() + { + string declarationTitle = "name"; + string expected = "\"" + declarationTitle + "\""; + DeclarationType declarationType = DeclarationType.Table; + string tokenText = "\""; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + Assert.Equal(completionItem.Label, expected); + Assert.Equal(completionItem.InsertText, expected); + Assert.Equal(completionItem.Detail, expected); + } + + [Fact] + public void LabelShouldIncludeQuotedIdentifiersGivenTokenWithQuotedIdentifiers() + { + string declarationTitle = "name"; + string expected = "\"" + declarationTitle + "\""; + DeclarationType declarationType = DeclarationType.Table; + string tokenText = "\"\""; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + Assert.Equal(completionItem.Label, expected); + Assert.Equal(completionItem.InsertText, expected); + Assert.Equal(completionItem.Detail, expected); + } + + [Fact] + public void InsertTextShouldIncludeBracketGivenReservedName() { foreach (string word in ReservedWords) { string declarationTitle = word; string expected = "[" + declarationTitle + "]"; DeclarationType declarationType = DeclarationType.Table; - string tokenText = "[]"; + string tokenText = ""; SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); - Assert.Equal(completionItem.Label, expected); + Assert.Equal(completionItem.Label, word); Assert.Equal(completionItem.InsertText, expected); - Assert.Equal(completionItem.Detail, expected); + Assert.Equal(completionItem.Detail, word); } } + [Fact] + public void LabelShouldNotIncludeBracketIfTokenIncludesQuotedIdentifiersGivenReservedName() + { + string declarationTitle = "User"; + string expected = "\"" + declarationTitle + "\""; + DeclarationType declarationType = DeclarationType.Table; + string tokenText = "\""; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + Assert.Equal(completionItem.Label, expected); + Assert.Equal(completionItem.InsertText, expected); + Assert.Equal(completionItem.Detail, expected); + } + + [Fact] + public void LabelShouldNotIncludeDoubleBracketIfTokenIncludesBracketsGivenReservedName() + { + string declarationTitle = "User"; + string expected = "[" + declarationTitle + "]"; + DeclarationType declarationType = DeclarationType.Table; + string tokenText = "["; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + Assert.Equal(completionItem.Label, expected); + Assert.Equal(completionItem.InsertText, expected); + Assert.Equal(completionItem.Detail, expected); + } + [Fact] public void KindShouldBeModuleGivenSchemaDeclarationType() { diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/ServiceHost/ServiceHostTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/ServiceHost/ServiceHostTests.cs new file mode 100644 index 00000000..c4a2b22d --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/ServiceHost/ServiceHostTests.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.ServiceHost +{ + public class ServiceHostTests + { + [Fact] + public async void InitializeResultShouldIncludeTheCharactersThatWouldTriggerTheCompletion() + { + Hosting.ServiceHost host = Hosting.ServiceHost.Instance; + var requesContext = new Mock>(); + requesContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + await host.HandleInitializeRequest(new InitializeRequest(), requesContext.Object); + requesContext.Verify(x => x.SendResult(It.Is( + i => i.Capabilities.CompletionProvider.TriggerCharacters.All(t => Hosting.ServiceHost.CompletionTriggerCharacters.Contains(t))))); + } + } +}