Autocomplete bug fixes (#43)

Fix-up the autocomplete support to better handle binding timeouts.
Also provide a default keyword suggestion list.
This commit is contained in:
Karl Burtram
2016-09-13 15:22:57 -07:00
committed by GitHub
parent 222f9364d8
commit 1671f762bf
13 changed files with 1177 additions and 539 deletions

View File

@@ -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
{

View File

@@ -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
{
/// <summary>
/// Main class for Language Service functionality including anything that reqires knowledge of
/// the language to perfom, such as definitions, intellisense, etc.
/// </summary>
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
}
}
}
};
}
/// <summary>
/// Converts a list of Declaration objects to CompletionItem objects
/// since VS Code expects CompletionItems but SQL Parser works with Declarations
/// </summary>
/// <param name="suggestions"></param>
/// <param name="cursorRow"></param>
/// <param name="cursorColumn"></param>
/// <returns></returns>
internal static CompletionItem[] ConvertDeclarationsToCompletionItems(
IEnumerable<Declaration> suggestions,
int row,
int startColumn,
int endColumn)
{
List<CompletionItem> completions = new List<CompletionItem>();
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();
}
}
}

View File

@@ -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
{
/// <summary>
/// Main class for Autocomplete functionality
/// </summary>
public class AutoCompleteService
{
#region Singleton Instance Implementation
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<AutoCompleteService> instance
= new Lazy<AutoCompleteService>(() => new AutoCompleteService());
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static AutoCompleteService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Default, parameterless constructor.
/// Internal constructor for use in test cases only
/// </summary>
internal AutoCompleteService()
{
}
#endregion
private ConnectionService connectionService = null;
/// <summary>
/// Internal for testing purposes only
/// </summary>
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);
}
/// <summary>
/// Auto-complete completion provider request callback
/// </summary>
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> requestContext)
{
// get the current list of completion items and return to client
var scriptFile = WorkspaceService<SqlToolsSettings>.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);
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// </summary>
/// <param name="info"></param>
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<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
LanguageService.Instance.ParseAndBind(scriptFile, info);
}
}
});
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sql"></param>
/// <param name="startRow"></param>
/// <param name="startColumn"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Determines whether a reparse and bind is required to provide autocomplete
/// </summary>
/// <param name="info"></param>
/// <returns>TEMP: Currently hard-coded to false for perf</returns>
private bool RequiresReparse(ScriptParseInfo info)
{
return false;
}
/// <summary>
/// Converts a list of Declaration objects to CompletionItem objects
/// since VS Code expects CompletionItems but SQL Parser works with Declarations
/// </summary>
/// <param name="suggestions"></param>
/// <param name="cursorRow"></param>
/// <param name="cursorColumn"></param>
/// <returns></returns>
private CompletionItem[] ConvertDeclarationsToCompletionItems(
IEnumerable<Declaration> suggestions,
int row,
int startColumn,
int endColumn)
{
List<CompletionItem> completions = new List<CompletionItem>();
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();
}
/// <summary>
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
/// </summary>
/// <param name="textDocumentPosition"></param>
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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Main class for Language Service functionality including anything that reqires knowledge of
/// the language to perfom, such as definitions, intellisense, etc.
/// </summary>
public static class DiagnosticsHelper
{
/// <summary>
/// Send the diagnostic results back to the host application
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="semanticMarkers"></param>
/// <param name="eventContext"></param>
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()
});
}
/// <summary>
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
/// </summary>
/// <param name="scriptFileMarker"></param>
/// <returns></returns>
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
}
}
};
}
/// <summary>
/// Map ScriptFileMarker severity to Diagnostic severity
/// </summary>
/// <param name="markerLevel"></param>
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;
}
}
}
}

View File

@@ -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
/// </summary>
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;
/// <summary>
/// Internal for testing purposes only
/// </summary>
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<SqlToolsSettings>.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification);
// Register the file open update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
WorkspaceService<SqlToolsSettings>.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
/// <summary>
/// Parses the SQL text and binds it to the SMO metadata provider if connected
/// Auto-complete completion provider request callback
/// </summary>
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
private static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> 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<SqlToolsSettings>.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<ParseResult> parseResults = new List<ParseResult>();
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;
}
/// <summary>
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
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<ScriptFileMarker> markers = new List<ScriptFileMarker>();
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<Location[]> requestContext)
@@ -261,22 +245,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await Task.FromResult(true);
}
private static async Task HandleDocumentSymbolRequest(
DocumentSymbolParams documentSymbolParams,
RequestContext<SymbolInformation[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
await Task.FromResult(true);
}
private static async Task HandleWorkspaceSymbolRequest(
WorkspaceSymbolParams workspaceSymbolParams,
RequestContext<SymbolInformation[]> 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<SqlToolsSettings>.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"
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Parses the SQL text and binds it to the SMO metadata provider if connected
/// </summary>
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <returns></returns>
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<ParseResult> parseResults = new List<ParseResult>();
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;
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// </summary>
/// <param name="info"></param>
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<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
ParseAndBind(scriptFile, info);
}
}
});
}
/// <summary>
/// Determines whether a reparse and bind is required to provide autocomplete
/// </summary>
/// <param name="info"></param>
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);
}
/// <summary>
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
/// </summary>
/// <param name="textDocumentPosition"></param>
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<CompletionItem[]> 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
/// <summary>
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
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<ScriptFileMarker> markers = new List<ScriptFileMarker>();
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();
}
/// <summary>
/// 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);
}
}
/// <summary>
/// Send the diagnostic results back to the host application
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="semanticMarkers"></param>
/// <param name="eventContext"></param>
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()
});
}
/// <summary>
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
/// </summary>
/// <param name="scriptFileMarker"></param>
/// <returns></returns>
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
}
}
};
}
/// <summary>
/// Map ScriptFileMarker severity to Diagnostic severity
/// </summary>
/// <param name="markerLevel"></param>
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);
}
}

View File

@@ -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
/// </summary>
internal class ScriptParseInfo
{
private ManualResetEvent buildingMetadataEvent = new ManualResetEvent(initialState: true);
private ParseOptions parseOptions = new ParseOptions();
private ServerConnection serverConnection;
/// <summary>
/// Event which tells if MetadataProvider is built fully or not
/// </summary>
public ManualResetEvent BuildingMetadataEvent
{
get { return this.buildingMetadataEvent; }
}
/// <summary>
/// Gets or sets a flag determining is the LanguageService is connected
/// </summary>
public bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the LanguageService SMO ServerConnection
/// </summary>
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;
}
}
/// <summary>
/// Gets the Language Service ServerVersion
/// </summary>
public ServerVersion ServerVersion
{
get
{
return this.ServerConnection != null
? this.ServerConnection.ServerVersion
: null;
}
}
/// <summary>
/// Gets the current DataEngineType
/// </summary>
public DatabaseEngineType DatabaseEngineType
{
get
{
return this.ServerConnection != null
? this.ServerConnection.DatabaseEngineType
: DatabaseEngineType.Standalone;
}
}
/// <summary>
/// Gets the current connections TransactSqlVersion
/// </summary>
public TransactSqlVersion TransactSqlVersion
{
get
{
return this.IsConnected
? GetTransactSqlVersion(this.ServerVersion)
: TransactSqlVersion.Current;
}
}
/// <summary>
/// Gets the current DatabaseCompatibilityLevel
/// </summary>
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
{
get
{
return this.IsConnected
? GetDatabaseCompatibilityLevel(this.ServerVersion)
: DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the current ParseOptions
/// </summary>
public ParseOptions ParseOptions
{
get
{
return this.parseOptions;
}
}
/// <summary>
/// Gets or sets the SMO binder for schema-aware intellisense
/// </summary>
@@ -28,13 +136,63 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary>
/// Gets or set the SMO metadata provider that's bound to the current connection
/// </summary>
/// <returns></returns>
public SmoMetadataProvider MetadataProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata display info provider
/// </summary>
/// <returns></returns>
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets the database compatibility level from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
/// <summary>
/// Gets the transaction sql version from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
}
}

View File

@@ -45,7 +45,6 @@ namespace Microsoft.SqlTools.ServiceLayer
// Initialize the services that will be hosted here
WorkspaceService<SqlToolsSettings>.Instance.InitializeService(serviceHost);
AutoCompleteService.Instance.InitializeService(serviceHost);
LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext);
ConnectionService.Instance.InitializeService(serviceHost);
CredentialService.Instance.InitializeService(serviceHost);

View File

@@ -14,13 +14,10 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
/// <summary>
/// Reader for SSMS formatted file streams
/// Reader for service buffer formatted file streams
/// </summary>
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

View File

@@ -14,13 +14,10 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
/// <summary>
/// Writer for SSMS formatted file streams
/// Writer for service buffer formatted file streams
/// </summary>
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;

View File

@@ -8,12 +8,6 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
/// </summary>
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();

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="sql"></param>
/// <param name="startRow"></param>
/// <param name="startColumn"></param>
/// <returns></returns>
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;
}
}
}