From 1671f762bfb50e353f4e39cc753cb437e35a6bf5 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 13 Sep 2016 15:22:57 -0700 Subject: [PATCH] Autocomplete bug fixes (#43) Fix-up the autocomplete support to better handle binding timeouts. Also provide a default keyword suggestion list. --- .../Hosting/ServiceHost.cs | 6 +- .../LanguageServices/AutoCompleteHelper.cs | 514 +++++++++++++++++ .../LanguageServices/AutoCompleteService.cs | 323 ----------- .../LanguageServices/DiagnosticsHelper.cs | 98 ++++ .../LanguageServices/LanguageService.cs | 522 +++++++++++------- .../LanguageServices/ScriptParseInfo.cs | 164 +++++- .../Program.cs | 1 - .../ServiceBufferFileStreamReader.cs | 5 +- .../ServiceBufferFileStreamWriter.cs | 5 +- .../SqlContext/SqlToolsSettings.cs | 6 - .../Utility/TextUtilities.cs | 62 +++ .../LanguageServer/LanguageServiceTests.cs | 2 +- .../Utility/TestObjects.cs | 8 - 13 files changed, 1177 insertions(+), 539 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs delete mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs index 5f5ef1df..f252d3c6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs @@ -136,13 +136,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting TextDocumentSync = TextDocumentSyncKind.Incremental, DefinitionProvider = true, ReferencesProvider = true, - DocumentHighlightProvider = true, - DocumentSymbolProvider = true, - WorkspaceSymbolProvider = true, + DocumentHighlightProvider = true, 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 new file mode 100644 index 00000000..1069e18d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -0,0 +1,514 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Main class for Language Service functionality including anything that reqires knowledge of + /// the language to perfom, such as definitions, intellisense, etc. + /// + public static class AutoCompleteHelper + { + private static readonly string[] DefaultCompletionText = new string[] + { + "absolute", + "accent_sensitivity", + "action", + "activation", + "add", + "address", + "admin", + "after", + "aggregate", + "algorithm", + "allow_page_locks", + "allow_row_locks", + "allow_snapshot_isolation", + "alter", + "always", + "ansi_null_default", + "ansi_nulls", + "ansi_padding", + "ansi_warnings", + "application", + "arithabort", + "as", + "asc", + "assembly", + "asymmetric", + "at", + "atomic", + "audit", + "authentication", + "authorization", + "auto", + "auto_close", + "auto_shrink", + "auto_update_statistics", + "auto_update_statistics_async", + "availability", + "backup", + "before", + "begin", + "binary", + "bit", + "block", + "break", + "browse", + "bucket_count", + "bulk", + "by", + "call", + "caller", + "card", + "cascade", + "case", + "catalog", + "catch", + "change_tracking", + "changes", + "char", + "character", + "check", + "checkpoint", + "close", + "clustered", + "collection", + "column", + "column_encryption_key", + "columnstore", + "commit", + "compatibility_level", + "compress_all_row_groups", + "compression", + "compression_delay", + "compute", + "concat_null_yields_null", + "configuration", + "connect", + "constraint", + "containstable", + "continue", + "create", + "cube", + "current", + "current_date", + "cursor", + "cursor_close_on_commit", + "cursor_default", + "data", + "data_compression", + "database", + "date", + "date_correlation_optimization", + "datefirst", + "datetime", + "datetime2", + "days", + "db_chaining", + "dbcc", + "deallocate", + "dec", + "decimal", + "declare", + "default", + "delayed_durability", + "delete", + "deny", + "desc", + "description", + "disable_broker", + "disabled", + "disk", + "distinct", + "distributed", + "double", + "drop", + "drop_existing", + "dump", + "durability", + "dynamic", + "else", + "enable", + "encrypted", + "encryption_type", + "end", + "end-exec", + "entry", + "errlvl", + "escape", + "event", + "except", + "exec", + "execute", + "exit", + "external", + "fast_forward", + "fetch", + "file", + "filegroup", + "filename", + "filestream", + "fillfactor", + "filter", + "first", + "float", + "for", + "foreign", + "freetext", + "freetexttable", + "from", + "full", + "fullscan", + "fulltext", + "function", + "generated", + "geography", + "get", + "global", + "go", + "goto", + "grant", + "group", + "hash", + "hashed", + "having", + "hidden", + "hierarchyid", + "holdlock", + "hours", + "identity", + "identity_insert", + "identitycol", + "if", + "ignore_dup_key", + "image", + "immediate", + "include", + "index", + "inflectional", + "insensitive", + "insert", + "instead", + "int", + "integer", + "integrated", + "intersect", + "into", + "isolation", + "json", + "key", + "kill", + "language", + "last", + "legacy_cardinality_estimation", + "level", + "lineno", + "load", + "local", + "locate", + "location", + "login", + "masked", + "master", + "maxdop", + "memory_optimized", + "merge", + "message", + "modify", + "move", + "multi_user", + "namespace", + "national", + "native_compilation", + "nchar", + "next", + "no", + "nocheck", + "nocount", + "nonclustered", + "none", + "norecompute", + "now", + "numeric", + "numeric_roundabort", + "object", + "of", + "off", + "offsets", + "on", + "online", + "open", + "opendatasource", + "openquery", + "openrowset", + "openxml", + "option", + "order", + "out", + "output", + "over", + "owner", + "pad_index", + "page", + "page_verify", + "parameter_sniffing", + "parameterization", + "partial", + "partition", + "password", + "path", + "percent", + "percentage", + "period", + "persisted", + "plan", + "policy", + "population", + "precision", + "predicate", + "primary", + "print", + "prior", + "proc", + "procedure", + "public", + "query_optimizer_hotfixes", + "query_store", + "quoted_identifier", + "raiserror", + "range", + "raw", + "read", + "read_committed_snapshot", + "read_only", + "read_write", + "readonly", + "readtext", + "real", + "rebuild", + "receive", + "reconfigure", + "recovery", + "recursive", + "recursive_triggers", + "references", + "relative", + "remove", + "reorganize", + "replication", + "required", + "restart", + "restore", + "restrict", + "resume", + "return", + "returns", + "revert", + "revoke", + "role", + "rollback", + "rollup", + "row", + "rowcount", + "rowguidcol", + "rows", + "rule", + "sample", + "save", + "schema", + "schemabinding", + "scoped", + "scroll", + "secondary", + "security", + "securityaudit", + "select", + "semantickeyphrasetable", + "semanticsimilaritydetailstable", + "semanticsimilaritytable", + "send", + "sent", + "sequence", + "server", + "session", + "set", + "sets", + "setuser", + "shutdown", + "simple", + "smallint", + "smallmoney", + "snapshot", + "sort_in_tempdb", + "sql", + "standard", + "start", + "started", + "state", + "statement", + "static", + "statistics", + "statistics_norecompute", + "status", + "stopped", + "supported", + "symmetric", + "sysname", + "system", + "system_time", + "system_versioning", + "table", + "tablesample", + "take", + "target", + "textimage_on", + "textsize", + "then", + "thesaurus", + "throw", + "time", + "timestamp", + "tinyint", + "to", + "top", + "tran", + "transaction", + "trigger", + "truncate", + "trustworthy", + "try", + "tsql", + "type", + "union", + "unique", + "uniqueidentifier", + "unlimited", + "updatetext", + "use", + "user", + "using", + "value", + "values", + "varchar", + "varying", + "version", + "view", + "waitfor", + "weight", + "when", + "where", + "while", + "with", + "within", + "within group", + "without", + "writetext", + "xact_abort", + "xml", + "zone" + }; + + internal static CompletionItem[] GetDefaultCompletionItems( + int row, + int startColumn, + int endColumn) + { + var completionItems = new CompletionItem[DefaultCompletionText.Length]; + for (int i = 0; i < DefaultCompletionText.Length; ++i) + { + completionItems[i] = CreateDefaultCompletionItem( + DefaultCompletionText[i].ToUpper(), + row, + startColumn, + endColumn); + } + return completionItems; + } + + private static CompletionItem CreateDefaultCompletionItem( + string label, + int row, + int startColumn, + int endColumn) + { + return new CompletionItem() + { + Label = label, + Kind = CompletionItemKind.Keyword, + Detail = label + " keyword", + TextEdit = new TextEdit + { + NewText = label, + Range = new Range + { + Start = new Position + { + Line = row, + Character = startColumn + }, + End = new Position + { + Line = row, + Character = endColumn + } + } + } + }; + } + + /// + /// Converts a list of Declaration objects to CompletionItem objects + /// since VS Code expects CompletionItems but SQL Parser works with Declarations + /// + /// + /// + /// + /// + internal static CompletionItem[] ConvertDeclarationsToCompletionItems( + IEnumerable suggestions, + int row, + int startColumn, + int endColumn) + { + List completions = new List(); + foreach (var autoCompleteItem in suggestions) + { + // convert the completion item candidates into CompletionItems + completions.Add(new CompletionItem() + { + Label = autoCompleteItem.Title, + Kind = CompletionItemKind.Variable, + Detail = autoCompleteItem.Title, + Documentation = autoCompleteItem.Description, + TextEdit = new TextEdit + { + NewText = autoCompleteItem.Title, + Range = new Range + { + Start = new Position + { + Line = row, + Character = startColumn + }, + End = new Position + { + Line = row, + Character = endColumn + } + } + } + }); + } + + return completions.ToArray(); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs deleted file mode 100644 index 5abe27f7..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs +++ /dev/null @@ -1,323 +0,0 @@ -// -// 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.Collections.Generic; -using System.Data.SqlClient; -using System.Threading.Tasks; -using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.SmoMetadataProvider; -using Microsoft.SqlServer.Management.SqlParser.Binder; -using Microsoft.SqlServer.Management.SqlParser.Intellisense; -using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; -using Microsoft.SqlTools.ServiceLayer.Hosting; -using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; -using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; -using Microsoft.SqlTools.ServiceLayer.SqlContext; -using Microsoft.SqlTools.ServiceLayer.Workspace; -using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; - -namespace Microsoft.SqlTools.ServiceLayer.LanguageServices -{ - /// - /// Main class for Autocomplete functionality - /// - public class AutoCompleteService - { - #region Singleton Instance Implementation - - /// - /// Singleton service instance - /// - private static Lazy instance - = new Lazy(() => new AutoCompleteService()); - - /// - /// Gets the singleton service instance - /// - public static AutoCompleteService Instance - { - get - { - return instance.Value; - } - } - - /// - /// Default, parameterless constructor. - /// Internal constructor for use in test cases only - /// - internal AutoCompleteService() - { - } - - #endregion - - private ConnectionService connectionService = null; - - /// - /// Internal for testing purposes only - /// - internal ConnectionService ConnectionServiceInstance - { - get - { - if(connectionService == null) - { - connectionService = ConnectionService.Instance; - } - return connectionService; - } - - set - { - connectionService = value; - } - } - - public void InitializeService(ServiceHost serviceHost) - { - // Register auto-complete request handler - serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); - - // Register a callback for when a connection is created - ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache); - - // Register a callback for when a connection is closed - ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); - } - - /// - /// Auto-complete completion provider request callback - /// - /// - /// - /// - private static async Task HandleCompletionRequest( - TextDocumentPosition textDocumentPosition, - RequestContext requestContext) - { - // get the current list of completion items and return to client - var scriptFile = WorkspaceService.Instance.Workspace.GetFile( - textDocumentPosition.TextDocument.Uri); - - ConnectionInfo connInfo; - ConnectionService.Instance.TryFindConnection( - scriptFile.ClientFilePath, - out connInfo); - - var completionItems = Instance.GetCompletionItems( - textDocumentPosition, scriptFile, connInfo); - - await requestContext.SendResult(completionItems); - } - - /// - /// Remove a reference to an autocomplete cache from a URI. If - /// it is the last URI connected to a particular connection, - /// then remove the cache. - /// - public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) - { - // currently this method is disabled, but we need to reimplement now that the - // implementation of the 'cache' has changed. - await Task.FromResult(0); - } - - /// - /// Update the cached autocomplete candidate list when the user connects to a database - /// - /// - public async Task UpdateAutoCompleteCache(ConnectionInfo info) - { - await Task.Run( () => - { - if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(info.OwnerUri)) - { - var sqlConn = info.SqlConnection as SqlConnection; - if (sqlConn != null) - { - var srvConn = new ServerConnection(sqlConn); - var displayInfoProvider = new MetadataDisplayInfoProvider(); - var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn); - var binder = BinderProvider.CreateBinder(metadataProvider); - - LanguageService.Instance.ScriptParseInfoMap.Add(info.OwnerUri, - new ScriptParseInfo() - { - Binder = binder, - MetadataProvider = metadataProvider, - MetadataDisplayInfoProvider = displayInfoProvider - }); - - var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); - - LanguageService.Instance.ParseAndBind(scriptFile, info); - } - } - }); - } - - /// - /// Find the position of the previous delimeter for autocomplete token replacement. - /// SQL Parser may have similar functionality in which case we'll delete this method. - /// - /// - /// - /// - /// - private int PositionOfPrevDelimeter(string sql, int startRow, int startColumn) - { - if (string.IsNullOrWhiteSpace(sql)) - { - return 1; - } - - int prevLineColumns = 0; - for (int i = 0; i < startRow; ++i) - { - while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length) - { - ++prevLineColumns; - } - ++prevLineColumns; - } - - startColumn += prevLineColumns; - - if (startColumn - 1 < sql.Length) - { - while (--startColumn >= prevLineColumns) - { - if (sql[startColumn] == ' ' - || sql[startColumn] == '\t' - || sql[startColumn] == '\n' - || sql[startColumn] == '.' - || sql[startColumn] == '+' - || sql[startColumn] == '-' - || sql[startColumn] == '*' - || sql[startColumn] == '>' - || sql[startColumn] == '<' - || sql[startColumn] == '=' - || sql[startColumn] == '/' - || sql[startColumn] == '%') - { - break; - } - } - } - - return startColumn + 1 - prevLineColumns; - } - - /// - /// Determines whether a reparse and bind is required to provide autocomplete - /// - /// - /// TEMP: Currently hard-coded to false for perf - private bool RequiresReparse(ScriptParseInfo info) - { - return false; - } - - /// - /// Converts a list of Declaration objects to CompletionItem objects - /// since VS Code expects CompletionItems but SQL Parser works with Declarations - /// - /// - /// - /// - /// - private CompletionItem[] ConvertDeclarationsToCompletionItems( - IEnumerable suggestions, - int row, - int startColumn, - int endColumn) - { - List completions = new List(); - foreach (var autoCompleteItem in suggestions) - { - // convert the completion item candidates into CompletionItems - completions.Add(new CompletionItem() - { - Label = autoCompleteItem.Title, - Kind = CompletionItemKind.Keyword, - Detail = autoCompleteItem.Title, - Documentation = autoCompleteItem.Description, - TextEdit = new TextEdit - { - NewText = autoCompleteItem.Title, - Range = new Range - { - Start = new Position - { - Line = row, - Character = startColumn - }, - End = new Position - { - Line = row, - Character = endColumn - } - } - } - }); - } - - return completions.ToArray(); - } - - /// - /// Return the completion item list for the current text position. - /// This method does not await cache builds since it expects to return quickly - /// - /// - public CompletionItem[] GetCompletionItems( - TextDocumentPosition textDocumentPosition, - ScriptFile scriptFile, - ConnectionInfo connInfo) - { - string filePath = textDocumentPosition.TextDocument.Uri; - - // Take a reference to the list at a point in time in case we update and replace the list - if (connInfo == null - || !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri)) - { - return new CompletionItem[0]; - } - - // reparse and bind the SQL statement if needed - var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri]; - if (RequiresReparse(scriptParseInfo)) - { - LanguageService.Instance.ParseAndBind(scriptFile, connInfo); - } - - if (scriptParseInfo.ParseResult == null) - { - return new CompletionItem[0]; - } - - // get the completion list from SQL Parser - var suggestions = Resolver.FindCompletions( - scriptParseInfo.ParseResult, - textDocumentPosition.Position.Line + 1, - textDocumentPosition.Position.Character + 1, - scriptParseInfo.MetadataDisplayInfoProvider); - - // convert the suggestion list to the VS Code format - return ConvertDeclarationsToCompletionItems( - suggestions, - textDocumentPosition.Position.Line, - PositionOfPrevDelimeter( - scriptFile.Contents, - textDocumentPosition.Position.Line, - textDocumentPosition.Position.Character), - textDocumentPosition.Position.Character); - } - } -} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs new file mode 100644 index 00000000..0c493b0f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs @@ -0,0 +1,98 @@ +// +// 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.Protocol; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Main class for Language Service functionality including anything that reqires knowledge of + /// the language to perfom, such as definitions, intellisense, etc. + /// + public static class DiagnosticsHelper + { + /// + /// Send the diagnostic results back to the host application + /// + /// + /// + /// + internal static async Task PublishScriptDiagnostics( + ScriptFile scriptFile, + ScriptFileMarker[] semanticMarkers, + EventContext eventContext) + { + var allMarkers = scriptFile.SyntaxMarkers != null + ? scriptFile.SyntaxMarkers.Concat(semanticMarkers) + : semanticMarkers; + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + await eventContext.SendEvent( + PublishDiagnosticsNotification.Type, + new PublishDiagnosticsNotification + { + Uri = scriptFile.ClientFilePath, + Diagnostics = + allMarkers + .Select(GetDiagnosticFromMarker) + .ToArray() + }); + } + + /// + /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible + /// + /// + /// + internal static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) + { + return new Diagnostic + { + Severity = MapDiagnosticSeverity(scriptFileMarker.Level), + Message = scriptFileMarker.Message, + Range = new Range + { + Start = new Position + { + Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 + } + } + }; + } + + /// + /// Map ScriptFileMarker severity to Diagnostic severity + /// + /// + internal static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) + { + switch (markerLevel) + { + case ScriptFileMarkerLevel.Error: + return DiagnosticSeverity.Error; + + case ScriptFileMarkerLevel.Warning: + return DiagnosticSeverity.Warning; + + case ScriptFileMarkerLevel.Information: + return DiagnosticSeverity.Information; + + default: + return DiagnosticSeverity.Error; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index d414b41e..b4da32aa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -5,22 +5,28 @@ using System; using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; -using System.Linq; -using Microsoft.SqlServer.Management.SqlParser.Parser; -using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlServer.Management.SqlParser.Binder; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.SqlParser; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlServer.Management.SmoMetadataProvider; + +using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -30,6 +36,42 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public sealed class LanguageService { + public const string DefaultBatchSeperator = "GO"; + + private const int DiagnosticParseDelay = 750; + + private const int FindCompletionsTimeout = 3000; + + private const int FindCompletionStartTimeout = 50; + + private const int OnConnectionWaitTimeout = 30000; + + private bool ShouldEnableAutocomplete() + { + return true; + } + + private ConnectionService connectionService = null; + + /// + /// Internal for testing purposes only + /// + internal ConnectionService ConnectionServiceInstance + { + get + { + if(connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } #region Singleton Instance Implementation @@ -98,8 +140,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest); serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest); serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest); - serviceHost.SetRequestHandler(DocumentSymbolRequest.Type, HandleDocumentSymbolRequest); - serviceHost.SetRequestHandler(WorkspaceSymbolRequest.Type, HandleWorkspaceSymbolRequest); + serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); // Register a no-op shutdown task for validation of the shutdown logic serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) => @@ -115,104 +156,47 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); // Register the file open update handler - WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); + WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); + + // Register a callback for when a connection is created + ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection); + + // Register a callback for when a connection is closed + ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); // Store the SqlToolsContext for future use Context = context; } + #endregion + + #region Request Handlers + /// - /// Parses the SQL text and binds it to the SMO metadata provider if connected + /// Auto-complete completion provider request callback /// - /// - /// + /// + /// /// - public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) + private static async Task HandleCompletionRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) { - ScriptParseInfo parseInfo = null; - if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath)) - { - parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath]; - } + // get the current list of completion items and return to client + var scriptFile = WorkspaceService.Instance.Workspace.GetFile( + textDocumentPosition.TextDocument.Uri); - // parse current SQL file contents to retrieve a list of errors - ParseOptions parseOptions = new ParseOptions(); - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo != null ? parseInfo.ParseResult : null, - parseOptions); - - // save previous result for next incremental parse - if (parseInfo != null) - { - parseInfo.ParseResult = parseResult; - } - - if (connInfo != null) - { - try - { - List parseResults = new List(); - parseResults.Add(parseResult); - parseInfo.Binder.Bind( - parseResults, - connInfo.ConnectionDetails.DatabaseName, - BindMode.Batch); - } - catch (ConnectionException) - { - Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); - } - catch (SqlParserInternalBinderError) - { - Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); - } - } - - return parseResult; - } - - /// - /// Gets a list of semantic diagnostic marks for the provided script file - /// - /// - public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) - { ConnectionInfo connInfo; ConnectionService.Instance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); - - var parseResult = ParseAndBind(scriptFile, connInfo); - // build a list of SQL script file markers from the errors - List markers = new List(); - foreach (var error in parseResult.Errors) - { - markers.Add(new ScriptFileMarker() - { - Message = error.Message, - Level = ScriptFileMarkerLevel.Error, - ScriptRegion = new ScriptRegion() - { - File = scriptFile.FilePath, - StartLineNumber = error.Start.LineNumber, - StartColumnNumber = error.Start.ColumnNumber, - StartOffset = 0, - EndLineNumber = error.End.LineNumber, - EndColumnNumber = error.End.ColumnNumber, - EndOffset = 0 - } - }); - } + var completionItems = Instance.GetCompletionItems( + textDocumentPosition, scriptFile, connInfo); - return markers.ToArray(); + await requestContext.SendResult(completionItems); } - #endregion - - #region Request Handlers - private static async Task HandleDefinitionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) @@ -261,22 +245,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices await Task.FromResult(true); } - private static async Task HandleDocumentSymbolRequest( - DocumentSymbolParams documentSymbolParams, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest"); - await Task.FromResult(true); - } - - private static async Task HandleWorkspaceSymbolRequest( - WorkspaceSymbolParams workspaceSymbolParams, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest"); - await Task.FromResult(true); - } - #endregion #region Handlers for Events from Other Services @@ -336,7 +304,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices foreach (var scriptFile in WorkspaceService.Instance.Workspace.GetOpenedFiles()) { - await PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext); + await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext); } } else @@ -352,7 +320,269 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices #endregion - #region Private Helpers + + #region "AutoComplete Provider methods" + + /// + /// Remove a reference to an autocomplete cache from a URI. If + /// it is the last URI connected to a particular connection, + /// then remove the cache. + /// + public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) + { + // currently this method is disabled, but we need to reimplement now that the + // implementation of the 'cache' has changed. + await Task.FromResult(0); + } + + /// + /// Parses the SQL text and binds it to the SMO metadata provider if connected + /// + /// + /// + /// + public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) + { + ScriptParseInfo parseInfo = null; + if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath)) + { + parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath]; + } + else + { + parseInfo = new ScriptParseInfo(); + this.ScriptParseInfoMap.Add(scriptFile.ClientFilePath, parseInfo); + } + + if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout)) + { + try + { + parseInfo.BuildingMetadataEvent.Reset(); + + // parse current SQL file contents to retrieve a list of errors + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + parseInfo.ParseOptions); + + parseInfo.ParseResult = parseResult; + + if (connInfo != null && parseInfo.IsConnected) + { + try + { + List parseResults = new List(); + parseResults.Add(parseResult); + parseInfo.Binder.Bind( + parseResults, + connInfo.ConnectionDetails.DatabaseName, + BindMode.Batch); + } + catch (ConnectionException) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + catch (SqlParserInternalBinderError) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + } + } + finally + { + parseInfo.BuildingMetadataEvent.Set(); + } + } + + return parseInfo.ParseResult; + } + + /// + /// Update the cached autocomplete candidate list when the user connects to a database + /// + /// + public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info) + { + await Task.Run( () => + { + if (ShouldEnableAutocomplete()) + { + ScriptParseInfo scriptInfo = + this.ScriptParseInfoMap.ContainsKey(info.OwnerUri) + ? this.ScriptParseInfoMap[info.OwnerUri] + : new ScriptParseInfo(); + + try + { + scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout); + scriptInfo.BuildingMetadataEvent.Reset(); + + var sqlConn = info.SqlConnection as SqlConnection; + if (sqlConn != null) + { + ServerConnection serverConn = new ServerConnection(sqlConn); + scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); + scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn); + scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); + scriptInfo.ServerConnection = new ServerConnection(sqlConn); + this.ScriptParseInfoMap[info.OwnerUri] = scriptInfo; + } + } + catch (Exception) + { + scriptInfo.IsConnected = false; + } + finally + { + // Set Metadata Build event to Signal state. + // (Tell Language Service that I am ready with Metadata Provider Object) + scriptInfo.BuildingMetadataEvent.Set(); + } + + if (scriptInfo.IsConnected) + { + var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); + ParseAndBind(scriptFile, info); + } + } + }); + } + + /// + /// Determines whether a reparse and bind is required to provide autocomplete + /// + /// + private bool RequiresReparse(ScriptParseInfo info, ScriptFile scriptFile) + { + if (info.ParseResult == null) + { + return true; + } + + string prevSqlText = info.ParseResult.Script.Sql; + string currentSqlText = scriptFile.Contents; + + return prevSqlText.Length != currentSqlText.Length + || !string.Equals(prevSqlText, currentSqlText); + } + + /// + /// Return the completion item list for the current text position. + /// This method does not await cache builds since it expects to return quickly + /// + /// + public CompletionItem[] GetCompletionItems( + TextDocumentPosition textDocumentPosition, + ScriptFile scriptFile, + ConnectionInfo connInfo) + { + string filePath = textDocumentPosition.TextDocument.Uri; + int startLine = textDocumentPosition.Position.Line; + int startColumn = TextUtilities.PositionOfPrevDelimeter( + scriptFile.Contents, + textDocumentPosition.Position.Line, + textDocumentPosition.Position.Character); + int endColumn = textDocumentPosition.Position.Character; + + // Take a reference to the list at a point in time in case we update and replace the list + if (connInfo == null + || !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri)) + { + return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn); + } + + // reparse and bind the SQL statement if needed + var scriptParseInfo = ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri]; + if (RequiresReparse(scriptParseInfo, scriptFile)) + { + ParseAndBind(scriptFile, connInfo); + } + + if (scriptParseInfo.ParseResult == null) + { + return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn); + } + + if (scriptParseInfo.IsConnected + && scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout)) + { + scriptParseInfo.BuildingMetadataEvent.Reset(); + Task findCompletionsTask = Task.Run(() => { + try + { + // get the completion list from SQL Parser + var suggestions = Resolver.FindCompletions( + scriptParseInfo.ParseResult, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.Position.Character + 1, + scriptParseInfo.MetadataDisplayInfoProvider); + + // convert the suggestion list to the VS Code format + return AutoCompleteHelper.ConvertDeclarationsToCompletionItems( + suggestions, + startLine, + startColumn, + endColumn); + } + finally + { + scriptParseInfo.BuildingMetadataEvent.Set(); + } + }); + + findCompletionsTask.Wait(LanguageService.FindCompletionsTimeout); + if (findCompletionsTask.IsCompleted + && findCompletionsTask.Result != null + && findCompletionsTask.Result.Length > 0) + { + return findCompletionsTask.Result; + } + } + + return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn); + } + + #endregion + + #region Diagnostic Provider methods + + /// + /// Gets a list of semantic diagnostic marks for the provided script file + /// + /// + internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) + { + ConnectionInfo connInfo; + ConnectionService.Instance.TryFindConnection( + scriptFile.ClientFilePath, + out connInfo); + + var parseResult = ParseAndBind(scriptFile, connInfo); + + // build a list of SQL script file markers from the errors + List markers = new List(); + foreach (var error in parseResult.Errors) + { + markers.Add(new ScriptFileMarker() + { + Message = error.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = new ScriptRegion() + { + File = scriptFile.FilePath, + StartLineNumber = error.Start.LineNumber, + StartColumnNumber = error.Start.ColumnNumber, + StartOffset = 0, + EndLineNumber = error.End.LineNumber, + EndColumnNumber = error.End.ColumnNumber, + EndOffset = 0 + } + }); + } + + return markers.ToArray(); + } /// /// Runs script diagnostics on changed files @@ -401,7 +631,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Task.Factory.StartNew( () => DelayThenInvokeDiagnostics( - 750, + LanguageService.DiagnosticParseDelay, filesToAnalyze, eventContext, ExistingRequestCancellation.Token), @@ -451,85 +681,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); Logger.Write(LogLevel.Verbose, "Analysis complete."); - await PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); - } - } - - /// - /// Send the diagnostic results back to the host application - /// - /// - /// - /// - private static async Task PublishScriptDiagnostics( - ScriptFile scriptFile, - ScriptFileMarker[] semanticMarkers, - EventContext eventContext) - { - var allMarkers = scriptFile.SyntaxMarkers != null - ? scriptFile.SyntaxMarkers.Concat(semanticMarkers) - : semanticMarkers; - - // Always send syntax and semantic errors. We want to - // make sure no out-of-date markers are being displayed. - await eventContext.SendEvent( - PublishDiagnosticsNotification.Type, - new PublishDiagnosticsNotification - { - Uri = scriptFile.ClientFilePath, - Diagnostics = - allMarkers - .Select(GetDiagnosticFromMarker) - .ToArray() - }); - } - - /// - /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible - /// - /// - /// - private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) - { - return new Diagnostic - { - Severity = MapDiagnosticSeverity(scriptFileMarker.Level), - Message = scriptFileMarker.Message, - Range = new Range - { - Start = new Position - { - Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 - } - } - }; - } - - /// - /// Map ScriptFileMarker severity to Diagnostic severity - /// - /// - private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) - { - switch (markerLevel) - { - case ScriptFileMarkerLevel.Error: - return DiagnosticSeverity.Error; - - case ScriptFileMarkerLevel.Warning: - return DiagnosticSeverity.Warning; - - case ScriptFileMarkerLevel.Information: - return DiagnosticSeverity.Information; - - default: - return DiagnosticSeverity.Error; + await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs index 4da2c57e..48fb2cce 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -3,10 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.SmoMetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Common; using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Parser; -using Microsoft.SqlServer.Management.SmoMetadataProvider; +using System; +using System.Threading; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -15,6 +19,110 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// internal class ScriptParseInfo { + private ManualResetEvent buildingMetadataEvent = new ManualResetEvent(initialState: true); + + private ParseOptions parseOptions = new ParseOptions(); + + private ServerConnection serverConnection; + + /// + /// Event which tells if MetadataProvider is built fully or not + /// + public ManualResetEvent BuildingMetadataEvent + { + get { return this.buildingMetadataEvent; } + } + + + /// + /// Gets or sets a flag determining is the LanguageService is connected + /// + public bool IsConnected { get; set; } + + /// + /// Gets or sets the LanguageService SMO ServerConnection + /// + public ServerConnection ServerConnection + { + get + { + return this.serverConnection; + } + set + { + this.serverConnection = value; + this.parseOptions = new ParseOptions( + batchSeparator: LanguageService.DefaultBatchSeperator, + isQuotedIdentifierSet: true, + compatibilityLevel: DatabaseCompatibilityLevel, + transactSqlVersion: TransactSqlVersion); + this.IsConnected = true; + } + } + + /// + /// Gets the Language Service ServerVersion + /// + public ServerVersion ServerVersion + { + get + { + return this.ServerConnection != null + ? this.ServerConnection.ServerVersion + : null; + } + } + + /// + /// Gets the current DataEngineType + /// + public DatabaseEngineType DatabaseEngineType + { + get + { + return this.ServerConnection != null + ? this.ServerConnection.DatabaseEngineType + : DatabaseEngineType.Standalone; + } + } + + /// + /// Gets the current connections TransactSqlVersion + /// + public TransactSqlVersion TransactSqlVersion + { + get + { + return this.IsConnected + ? GetTransactSqlVersion(this.ServerVersion) + : TransactSqlVersion.Current; + } + } + + /// + /// Gets the current DatabaseCompatibilityLevel + /// + public DatabaseCompatibilityLevel DatabaseCompatibilityLevel + { + get + { + return this.IsConnected + ? GetDatabaseCompatibilityLevel(this.ServerVersion) + : DatabaseCompatibilityLevel.Current; + } + } + + /// + /// Gets the current ParseOptions + /// + public ParseOptions ParseOptions + { + get + { + return this.parseOptions; + } + } + /// /// Gets or sets the SMO binder for schema-aware intellisense /// @@ -28,13 +136,63 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Gets or set the SMO metadata provider that's bound to the current connection /// - /// public SmoMetadataProvider MetadataProvider { get; set; } /// /// Gets or sets the SMO metadata display info provider /// - /// public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } + + /// + /// Gets the database compatibility level from a server version + /// + /// + private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion) + { + int versionMajor = Math.Max(serverVersion.Major, 8); + + switch (versionMajor) + { + case 8: + return DatabaseCompatibilityLevel.Version80; + case 9: + return DatabaseCompatibilityLevel.Version90; + case 10: + return DatabaseCompatibilityLevel.Version100; + case 11: + return DatabaseCompatibilityLevel.Version110; + case 12: + return DatabaseCompatibilityLevel.Version120; + case 13: + return DatabaseCompatibilityLevel.Version130; + default: + return DatabaseCompatibilityLevel.Current; + } + } + + /// + /// Gets the transaction sql version from a server version + /// + /// + private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion) + { + int versionMajor = Math.Max(serverVersion.Major, 9); + + switch (versionMajor) + { + case 9: + case 10: + // In case of 10.0 we still use Version 10.5 as it is the closest available. + return TransactSqlVersion.Version105; + case 11: + return TransactSqlVersion.Version110; + case 12: + return TransactSqlVersion.Version120; + case 13: + return TransactSqlVersion.Version130; + default: + return TransactSqlVersion.Current; + } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Program.cs b/src/Microsoft.SqlTools.ServiceLayer/Program.cs index f0d2d6e8..377afe9d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Program.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Program.cs @@ -45,7 +45,6 @@ namespace Microsoft.SqlTools.ServiceLayer // Initialize the services that will be hosted here WorkspaceService.Instance.InitializeService(serviceHost); - AutoCompleteService.Instance.InitializeService(serviceHost); LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); ConnectionService.Instance.InitializeService(serviceHost); CredentialService.Instance.InitializeService(serviceHost); diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs index 0cfc2466..b6c23349 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs @@ -14,13 +14,10 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage { /// - /// Reader for SSMS formatted file streams + /// Reader for service buffer formatted file streams /// public class ServiceBufferFileStreamReader : IFileStreamReader { - // Most of this code is based on code from the Microsoft.SqlServer.Management.UI.Grid, SSMS DataStorage - // $\Data Tools\SSMS_XPlat\sql\ssms\core\DataStorage\src\FileStreamReader.cs - private const int DefaultBufferSize = 8192; #region Member Variables diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs index d0a1c2a9..c978bade 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs @@ -14,13 +14,10 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage { /// - /// Writer for SSMS formatted file streams + /// Writer for service buffer formatted file streams /// public class ServiceBufferFileStreamWriter : IFileStreamWriter { - // Most of this code is based on code from the Microsoft.SqlServer.Management.UI.Grid, SSMS DataStorage - // $\Data Tools\SSMS_XPlat\sql\ssms\core\DataStorage\src\FileStreamWriter.cs - #region Properties public const int DefaultBufferLength = 8192; diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs index 198884f2..e21dcb47 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs @@ -8,12 +8,6 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext /// public class SqlToolsSettings { - // TODO: Is this needed? I can't make sense of this comment. - // NOTE: This property is capitalized as 'SqlTools' because the - // mode name sent from the client is written as 'SqlTools' and - // JSON.net is using camelCasing. - //public ServiceHostSettings SqlTools { get; set; } - public SqlToolsSettings() { this.ScriptAnalysis = new ScriptAnalysisSettings(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs new file mode 100644 index 00000000..0da32040 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/TextUtilities.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.EditorServices.Utility +{ + public static class TextUtilities + { + /// + /// Find the position of the previous delimeter for autocomplete token replacement. + /// SQL Parser may have similar functionality in which case we'll delete this method. + /// + /// + /// + /// + /// + public static int PositionOfPrevDelimeter(string sql, int startRow, int startColumn) + { + if (string.IsNullOrWhiteSpace(sql)) + { + return 1; + } + + int prevLineColumns = 0; + for (int i = 0; i < startRow; ++i) + { + while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length) + { + ++prevLineColumns; + } + ++prevLineColumns; + } + + startColumn += prevLineColumns; + + if (startColumn - 1 < sql.Length) + { + while (--startColumn >= prevLineColumns) + { + if (sql[startColumn] == ' ' + || sql[startColumn] == '\t' + || sql[startColumn] == '\n' + || sql[startColumn] == '.' + || sql[startColumn] == '+' + || sql[startColumn] == '-' + || sql[startColumn] == '*' + || sql[startColumn] == '>' + || sql[startColumn] == '<' + || sql[startColumn] == '=' + || sql[startColumn] == '/' + || sql[startColumn] == '%') + { + break; + } + } + } + + return startColumn + 1 - prevLineColumns; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index 0725c209..bb07c56a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -164,7 +164,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices textDocument.Position.Character = 7; scriptFile.Contents = "select "; - var autoCompleteService = AutoCompleteService.Instance; + var autoCompleteService = LanguageService.Instance; var completions = autoCompleteService.GetCompletionItems( textDocument, scriptFile, diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs index 23ff7260..82ffffd0 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs @@ -67,14 +67,6 @@ namespace Microsoft.SqlTools.Test.Utility return new LanguageService(); } - /// - /// Creates a test autocomplete service instance - /// - public static AutoCompleteService GetAutoCompleteService() - { - return AutoCompleteService.Instance; - } - /// /// Creates a test sql connection factory instance ///