diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs index 4495be44..36875ab6 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 ad2846fb..c8e59cd2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -4,6 +4,9 @@ // using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using Microsoft.SqlServer.Management.SqlParser.Binder; using Microsoft.SqlServer.Management.SqlParser.Intellisense; @@ -26,6 +29,8 @@ 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 readonly string[] DefaultCompletionText = new string[] { "absolute", @@ -484,14 +489,44 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int startColumn, int endColumn) { - return new CompletionItem() + return CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn); + } + + internal static CompletionItem[] AddTokenToItems(CompletionItem[] currentList, Token token, int row, + int startColumn, + int endColumn) + { + if (currentList != null && + token != null && !string.IsNullOrWhiteSpace(token.Text) && + token.Text.All(ch => char.IsLetter(ch)) && + currentList.All(x => string.Compare(x.Label, token.Text, true) != 0 + )) + { + var list = currentList.ToList(); + list.Insert(0, 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 = CompletionItemKind.Keyword, - Detail = label + " keyword", + Kind = kind, + Detail = detail, + InsertText = insertText, TextEdit = new TextEdit { - NewText = label, + NewText = insertText, Range = new Range { Start = new Position @@ -507,6 +542,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } }; + + return item; } /// @@ -523,38 +560,56 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int startColumn, int endColumn) { + List completions = new List(); + foreach (var autoCompleteItem in suggestions) { - // convert the completion item candidates into CompletionItems - completions.Add(new CompletionItem() + string insertText = GetCompletionItemInsertName(autoCompleteItem); + CompletionItemKind kind = CompletionItemKind.Variable; + switch (autoCompleteItem.Type) { - Label = autoCompleteItem.Title, - Kind = CompletionItemKind.Variable, - Detail = autoCompleteItem.Title, - TextEdit = new TextEdit - { - NewText = autoCompleteItem.Title, - Range = new Range - { - Start = new Position - { - Line = row, - Character = startColumn - }, - End = new Position - { - Line = row, - Character = endColumn - } - } - } - }); + case DeclarationType.Schema: + kind = CompletionItemKind.Module; + break; + case DeclarationType.Column: + kind = CompletionItemKind.Field; + break; + case DeclarationType.Table: + kind = CompletionItemKind.Method; + break; + case DeclarationType.Database: + kind = CompletionItemKind.File; + break; + case DeclarationType.Server: + kind = CompletionItemKind.Value; + break; + default: + kind = CompletionItemKind.Variable; + break; + } + + + + // convert the completion item candidates into CompletionItems + completions.Add(CreateCompletionItem(autoCompleteItem.Title, autoCompleteItem.Title, insertText, kind, 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 diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index cc993e40..13a8c4a4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -264,10 +264,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices var completionItems = Instance.GetCompletionItems( textDocumentPosition, scriptFile, connInfo); - - await requestContext.SendResult(completionItems); + + await requestContext.SendResult(completionItems); + } } - } /// /// Handle the resolve completion request event to provide additional @@ -686,12 +686,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; this.currentCompletionParseInfo = null; + CompletionItem[] defaultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); + CompletionItem[] resultCompletionItems = defaultCompletionItems; + CompletionItem[] emptyCompletionItems = new CompletionItem[0]; + int line = textDocumentPosition.Position.Line + 1; + int column = textDocumentPosition.Position.Character + 1; // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); if (connInfo == null || scriptParseInfo == null) { - return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); + return defaultCompletionItems; } // reparse and bind the SQL statement if needed @@ -702,8 +707,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (scriptParseInfo.ParseResult == null) { - return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); + return defaultCompletionItems; } + Token token = GetToken(scriptParseInfo, line, column); if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock, LanguageService.FindCompletionStartTimeout)) @@ -721,42 +727,76 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // get the completion list from SQL Parser scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( scriptParseInfo.ParseResult, - textDocumentPosition.Position.Line + 1, - textDocumentPosition.Position.Character + 1, + line, + column, bindingContext.MetadataDisplayInfoProvider); // cache the current script parse info object to resolve completions later this.currentCompletionParseInfo = scriptParseInfo; - + // convert the suggestion list to the VS Code format completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems( scriptParseInfo.CurrentSuggestions, startLine, startColumn, - endColumn); + endColumn + ); return completions; }, timeoutOperation: (bindingContext) => { - return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); + return defaultCompletionItems; }); queueItem.ItemProcessed.WaitOne(); - var completionItems = queueItem.GetResultAsT(); + var completionItems = queueItem.GetResultAsT(); if (completionItems != null && completionItems.Length > 0) { - return completionItems; - } + resultCompletionItems = completionItems; + } + else if (!ShouldShowCompletionList(token)) + { + resultCompletionItems = emptyCompletionItems; + } } finally - { + { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); - } + } } - - return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions); + //resultCompletionItems = AutoCompleteHelper.AddTokenToItems(resultCompletionItems, token, startLine, startColumn, endColumn); + return resultCompletionItems; + } + + private static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn) + { + if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) + { + var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); + if (tokenIndex >= 0) + { + return scriptParseInfo.ParseResult.Script.Tokens.ToList()[tokenIndex]; + } + } + return null; + } + + private static bool ShouldShowCompletionList(Token token) + { + bool result = true; + if (token != null) + { + switch (token.Id) + { + case (int)Tokens.LEX_MULTILINE_COMMENT: + case (int)Tokens.LEX_END_OF_LINE_COMMENT: + result = false; + break; + } + } + return result; } #endregion