diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs index cdf39e29..32b8301e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs @@ -157,7 +157,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting CompletionProvider = new CompletionOptions { ResolveProvider = true, - TriggerCharacters = new string[] { ".", "-", ":", "\\" } + TriggerCharacters = new string[] { ".", "-", ":", "\\", "[" } }, SignatureHelpProvider = new SignatureHelpOptions { diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index 9667c0e3..a1568d7a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -31,8 +31,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private static WorkspaceService workspaceServiceInstance; - private static Regex ValidSqlNameRegex = new Regex(@"^[\p{L}_@][\p{L}\p{N}@$#_]{0,127}$"); - private static CompletionItem[] emptyCompletionList = new CompletionItem[0]; private static readonly string[] DefaultCompletionText = new string[] @@ -433,7 +431,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int startColumn, int endColumn) { - return CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn); + return SqlCompletionItem.CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn); } internal static CompletionItem[] AddTokenToItems(CompletionItem[] currentList, Token token, int row, @@ -447,49 +445,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices )) { var list = currentList.ToList(); - list.Insert(0, CreateCompletionItem(token.Text, token.Text, token.Text, CompletionItemKind.Text, row, startColumn, endColumn)); + list.Insert(0, SqlCompletionItem.CreateCompletionItem(token.Text, token.Text, token.Text, CompletionItemKind.Text, row, startColumn, endColumn)); return list.ToArray(); } return currentList; } - private static CompletionItem CreateCompletionItem( - string label, - string detail, - string insertText, - CompletionItemKind kind, - int row, - int startColumn, - int endColumn) - { - CompletionItem item = new CompletionItem() - { - Label = label, - Kind = kind, - Detail = detail, - InsertText = insertText, - TextEdit = new TextEdit - { - NewText = insertText, - Range = new Range - { - Start = new Position - { - Line = row, - Character = startColumn - }, - End = new Position - { - Line = row, - Character = endColumn - } - } - } - }; - - return item; - } - /// /// Converts a list of Declaration objects to CompletionItem objects /// since VS Code expects CompletionItems but SQL Parser works with Declarations @@ -502,56 +463,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices IEnumerable suggestions, int row, int startColumn, - int endColumn) + int endColumn, + string tokenText = null) { List completions = new List(); foreach (var autoCompleteItem in suggestions) { - string insertText = GetCompletionItemInsertName(autoCompleteItem); - CompletionItemKind kind = CompletionItemKind.Variable; - switch (autoCompleteItem.Type) - { - case DeclarationType.Schema: - kind = CompletionItemKind.Module; - break; - case DeclarationType.Column: - kind = CompletionItemKind.Field; - break; - case DeclarationType.Table: - case DeclarationType.View: - kind = CompletionItemKind.File; - break; - case DeclarationType.Database: - kind = CompletionItemKind.Method; - break; - case DeclarationType.ScalarValuedFunction: - case DeclarationType.TableValuedFunction: - case DeclarationType.BuiltInFunction: - kind = CompletionItemKind.Value; - break; - default: - kind = CompletionItemKind.Unit; - break; - } + SqlCompletionItem sqlCompletionItem = new SqlCompletionItem(autoCompleteItem, tokenText); // convert the completion item candidates into CompletionItems - completions.Add(CreateCompletionItem(autoCompleteItem.Title, autoCompleteItem.Title, insertText, kind, row, startColumn, endColumn)); + completions.Add(sqlCompletionItem.CreateCompletionItem(row, startColumn, endColumn)); } return completions.ToArray(); } - private static string GetCompletionItemInsertName(Declaration autoCompleteItem) - { - string insertText = autoCompleteItem.Title; - if (!string.IsNullOrEmpty(autoCompleteItem.Title) && !ValidSqlNameRegex.IsMatch(autoCompleteItem.Title)) - { - insertText = string.Format(CultureInfo.InvariantCulture, "[{0}]", autoCompleteItem.Title); - } - return insertText; - } - /// /// Preinitialize the parser and binder with common metadata. /// This should front load the long binding wait to the time the @@ -567,7 +494,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { if (scriptInfo.IsConnected) { - var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri); + var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri); LanguageService.Instance.ParseAndBind(scriptFile, info); if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 716d7cd8..32d812f6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -977,7 +977,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptParseInfo.CurrentSuggestions, startLine, startColumn, - endColumn); + endColumn, + tokenText); }, timeoutOperation: (bindingContext) => { diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs new file mode 100644 index 00000000..8aade3ec --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs @@ -0,0 +1,207 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Creates a completion item from SQL parser declaration item + /// + public class SqlCompletionItem + { + private static Regex ValidSqlNameRegex = new Regex(@"^[\p{L}_@][\p{L}\p{N}@$#_]{0,127}$"); + + /// + /// Create new instance given the SQL parser declaration + /// + public SqlCompletionItem(Declaration declaration, string tokenText) : + this(declaration == null ? null : declaration.Title, declaration == null ? DeclarationType.Table : declaration.Type, tokenText) + { + } + + /// + /// Creates new instance given declaration title and type + /// + public SqlCompletionItem(string declarationTitle, DeclarationType declarationType, string tokenText) + { + Validate.IsNotNullOrEmptyString("declarationTitle", declarationTitle); + + DeclarationTitle = declarationTitle; + DeclarationType = declarationType; + TokenText = tokenText; + + Init(); + } + + private void Init() + { + InsertText = GetCompletionItemInsertName(); + Label = DeclarationTitle; + if (StartsWithBracket(TokenText)) + { + Label = WithBracket(Label); + InsertText = WithBracket(InsertText); + } + Detail = Label; + Kind = CreateCompletionItemKind(); + } + + private CompletionItemKind CreateCompletionItemKind() + { + CompletionItemKind kind = CompletionItemKind.Variable; + switch (DeclarationType) + { + case DeclarationType.Schema: + kind = CompletionItemKind.Module; + break; + case DeclarationType.Column: + kind = CompletionItemKind.Field; + break; + case DeclarationType.Table: + case DeclarationType.View: + kind = CompletionItemKind.File; + break; + case DeclarationType.Database: + kind = CompletionItemKind.Method; + break; + case DeclarationType.ScalarValuedFunction: + case DeclarationType.TableValuedFunction: + case DeclarationType.BuiltInFunction: + kind = CompletionItemKind.Value; + break; + default: + kind = CompletionItemKind.Unit; + break; + } + + return kind; + } + + /// + /// Declaration Title + /// + public string DeclarationTitle { get; private set; } + + /// + /// Token text from the editor + /// + public string TokenText { get; private set; } + + /// + /// SQL declaration type + /// + public DeclarationType DeclarationType { get; private set; } + + /// + /// Completion item label + /// + public string Label { get; private set; } + + /// + /// Completion item kind + /// + public CompletionItemKind Kind { get; private set; } + + /// + /// Completion insert text + /// + public string InsertText { get; private set; } + + /// + /// Completion item detail + /// + public string Detail { get; private set; } + + /// + /// Creates a completion item given the editor info + /// + public CompletionItem CreateCompletionItem( + int row, + int startColumn, + int endColumn) + { + return CreateCompletionItem(Label, Detail, InsertText, Kind, row, startColumn, endColumn); + } + + /// + /// Creates a completion item + /// + public static CompletionItem CreateCompletionItem( + string label, + string detail, + string insertText, + CompletionItemKind kind, + int row, + int startColumn, + int endColumn) + { + CompletionItem item = new CompletionItem() + { + Label = label, + Kind = kind, + Detail = detail, + InsertText = insertText, + TextEdit = new TextEdit + { + NewText = insertText, + Range = new Range + { + Start = new Position + { + Line = row, + Character = startColumn + }, + End = new Position + { + Line = row, + Character = endColumn + } + } + } + }; + + return item; + } + + private string GetCompletionItemInsertName() + { + string insertText = DeclarationTitle; + if (!string.IsNullOrEmpty(DeclarationTitle) && !ValidSqlNameRegex.IsMatch(DeclarationTitle)) + { + 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); + } + else + { + return text; + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs new file mode 100644 index 00000000..4688b739 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs @@ -0,0 +1,209 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer +{ + public class SqlCompletionItemTests + { + [Fact] + public void InsertTextShouldIncludeBracketGivenNameWithSpace() + { + string declarationTitle = "name with space"; + string expected = "[" + declarationTitle + "]"; + + DeclarationType declarationType = DeclarationType.Table; + string tokenText = ""; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + Assert.True(completionItem.InsertText.StartsWith("[") && completionItem.InsertText.EndsWith("]")); + } + + [Fact] + public void InsertTextShouldIncludeBracketGivenNameWithSpecialCharacter() + { + 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.InsertText, expected); + Assert.Equal(completionItem.Detail, declarationTitle); + Assert.Equal(completionItem.Label, declarationTitle); + } + + [Fact] + public void LabelShouldIncludeBracketGivenTokenWithBracket() + { + 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 LabelShouldIncludeBracketGivenTokenWithBrackets() + { + 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 LabelShouldIncludeBracketGivenSqlObjectNameWithBracket() + { + string declarationTitle = @"Bracket\["; + 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, declarationTitle); + Assert.Equal(completionItem.InsertText, expected); + Assert.Equal(completionItem.Detail, declarationTitle); + } + + [Fact] + public void LabelShouldIncludeBracketGivenSqlObjectNameWithBracketAndTokenWithBracket() + { + string declarationTitle = @"Bracket\["; + 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 LabelShouldNotIncludeBracketGivenNameWithBrackets() + { + 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 LabelShouldIncludeBracketGivenNameWithOneBracket() + { + 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 KindShouldBeModuleGivenSchemaDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Module; + DeclarationType declarationType = DeclarationType.Schema; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeFieldGivenColumnDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Field; + DeclarationType declarationType = DeclarationType.Column; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeFileGivenTableDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.File; + DeclarationType declarationType = DeclarationType.Table; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeFileGivenViewDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.File; + DeclarationType declarationType = DeclarationType.View; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeMethodGivenDatabaseDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Method; + DeclarationType declarationType = DeclarationType.Database; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeValueGivenScalarValuedFunctionDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Value; + DeclarationType declarationType = DeclarationType.ScalarValuedFunction; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeValueGivenTableValuedFunctionDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Value; + DeclarationType declarationType = DeclarationType.TableValuedFunction; + ValidateDeclarationType(declarationType, expectedType); + } + + [Fact] + public void KindShouldBeUnitGivenUnknownDeclarationType() + { + CompletionItemKind expectedType = CompletionItemKind.Unit; + DeclarationType declarationType = DeclarationType.XmlIndex; + ValidateDeclarationType(declarationType, expectedType); + } + + private void ValidateDeclarationType(DeclarationType declarationType, CompletionItemKind expectedType) + { + string declarationTitle = "name"; + string tokenText = ""; + SqlCompletionItem item = new SqlCompletionItem(declarationTitle, declarationType, tokenText); + CompletionItem completionItem = item.CreateCompletionItem(0, 1, 2); + + + Assert.Equal(completionItem.Kind, expectedType); + } + } +}