// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion { /// /// 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}$"); private static DelimitedIdentifier BracketedIdentifiers = new DelimitedIdentifier { Start = "[", End = "]" }; private static DelimitedIdentifier FunctionPostfix = new DelimitedIdentifier { Start = "", End = "()" }; private static DelimitedIdentifier[] DelimitedIdentifiers = new DelimitedIdentifier[] { BracketedIdentifiers, new DelimitedIdentifier { Start = "\"", End = "\"" } }; /// /// 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 = DeclarationTitle; Label = DeclarationTitle; DelimitedIdentifier delimitedIdentifier = GetDelimitedIdentifier(TokenText); // If we're not already going to quote this then handle special cases for various // DeclarationTypes if (delimitedIdentifier == null && !string.IsNullOrEmpty(DeclarationTitle)) { switch (this.DeclarationType) { case DeclarationType.Server: case DeclarationType.Database: case DeclarationType.Table: case DeclarationType.Column: case DeclarationType.View: case DeclarationType.Schema: // Only quote if we need to - i.e. if this isn't a valid name (has characters that need escaping such as [) // or if it's a reserved word if (!ValidSqlNameRegex.IsMatch(DeclarationTitle) || AutoCompleteHelper.IsReservedWord(InsertText)) { InsertText = WithDelimitedIdentifier(BracketedIdentifiers, DeclarationTitle); } break; case DeclarationType.BuiltInFunction: case DeclarationType.ScalarValuedFunction: case DeclarationType.TableValuedFunction: // Add ()'s for all functions except global variable system functions (which all start with @@) if (!DeclarationTitle.StartsWith("@@")) { InsertText = WithDelimitedIdentifier(FunctionPostfix, DeclarationTitle); } break; } } // If the user typed a token that starts with a delimiter then always quote both // the display label and text to be inserted if (delimitedIdentifier != null) { Label = WithDelimitedIdentifier(delimitedIdentifier, Label); InsertText = WithDelimitedIdentifier(delimitedIdentifier, 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 bool HasDelimitedIdentifier(DelimitedIdentifier delimiteIidentifier, string text) { 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 delimitedIdentifier, string text) { if (!HasDelimitedIdentifier(delimitedIdentifier, text)) { return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", delimitedIdentifier.Start, text, delimitedIdentifier.End); } else { return text; } } } internal class DelimitedIdentifier { public string Start { get; set; } public string End { get; set; } } }