mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-23 17:24:12 -05:00
- TSqlFormatterService with support for formatting document and text range inside document - Settings support for all formatting options. - Extensibility support so that the service can be initialized using MEF extensibility, and can find all necessary TSqlFormatters using the same process Fix Initialize request error on startup - Messages were being read from the input channel before all request handlers were registered - In particular, the Initialize request which is key for any server to talk to the client was getting lost because the message reader thread begins consuming, and we take an extra few hundred milliseconds due to MEF startup before we register the handler - The solution is to initialize the message handler so request handlers can register, but not actually start processing incoming messages until all handers are ready. This is a safer way to go and should improve reliability overall Improvements from internal prototype: - Normalizing baselines to handle the line ending differences on Mac & Linux vs. Windows - Significantly shortened most lines by implementing base class methods to wrap common objects from Visitor.Context and removing unnecessary "this." syntax - Refactored the SqlCommonTableExpressionFormatter and related classes to reduce code count significantly. This provides a pattern to follow when refactoring other classes for similar clarity. It's likely a lot of common logic could be found and reused across these. - Reduced overall code size by adding utility methods
169 lines
7.2 KiB
C#
169 lines
7.2 KiB
C#
//
|
|
// 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.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Microsoft.SqlServer.Management.SqlParser.Parser;
|
|
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
|
|
using Microsoft.SqlTools.ServiceLayer.Extensibility;
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.Formatter
|
|
{
|
|
/// <summary>
|
|
/// The main entry point for our formatter implementation, via the <see cref="Format(string, FormatOptions, bool, Replacement.OnReplace)"/> method.
|
|
/// This converts a text string into a parsed AST using the Intellisense parser.
|
|
/// It then uses the Visitor pattern to find each element in the tree and determine if any edits are needed based on
|
|
/// All edits are applied after the entire AST has been visited using an algorithm that keeps track of index changes caused by previous updates. This allows
|
|
/// us to apply multiple edits to a text string in one sweep.
|
|
///
|
|
/// A note on the <see cref="SqlCodeObjectVisitor"/> implementation: All of the override nodes in the Intellisense AST are defined here, and routed to the Format method which looks up a matching
|
|
/// formatter to handle them. Any entry not explicitly formatted will use the no-op formatter which passes through the text unchanged.
|
|
/// </summary>
|
|
internal partial class FormatterVisitor : SqlCodeObjectVisitor
|
|
{
|
|
private readonly IMultiServiceProvider serviceProvider;
|
|
|
|
public FormatterVisitor(FormatContext context, IMultiServiceProvider serviceProvider)
|
|
: base()
|
|
{
|
|
Context = context;
|
|
this.serviceProvider = serviceProvider;
|
|
}
|
|
|
|
private void Format<T>(T codeObject) where T : SqlCodeObject
|
|
{
|
|
ASTNodeFormatter f = GetFormatter(codeObject);
|
|
f.Format();
|
|
}
|
|
|
|
private ASTNodeFormatter GetFormatter<T>(T codeObject) where T:SqlCodeObject
|
|
{
|
|
Type astType = typeof(T);
|
|
ASTNodeFormatter formatter;
|
|
|
|
var formatterFactory = serviceProvider.GetServices<ASTNodeFormatterFactory>().FirstOrDefault(f => astType.Equals(f.SupportedNodeType));
|
|
if (formatterFactory != null)
|
|
{
|
|
formatter = formatterFactory.Create(this, codeObject);
|
|
}
|
|
else
|
|
{
|
|
formatter = new NoOpFormatter(this, codeObject);
|
|
}
|
|
|
|
return formatter;
|
|
}
|
|
|
|
public FormatContext Context { get; private set; }
|
|
|
|
public void VerifyFormat()
|
|
{
|
|
ParseResult result = Parser.Parse(Context.FormattedSql);
|
|
SqlScript newScript = result.Script;
|
|
VerifyTokenStreamsOnlyDifferByWhitespace(Context.Script, newScript);
|
|
}
|
|
|
|
internal static bool IsTokenWhitespaceOrComma(SqlScript script, int tokenIndex)
|
|
{
|
|
int tokenId = script.TokenManager.TokenList[tokenIndex].TokenId;
|
|
return script.TokenManager.IsTokenWhitespace(tokenId) || (tokenId == 44);
|
|
}
|
|
|
|
internal static bool IsTokenWhitespaceOrComment(SqlScript script, int tokenIndex)
|
|
{
|
|
int tokenId = script.TokenManager.TokenList[tokenIndex].TokenId;
|
|
|
|
return script.TokenManager.IsTokenWhitespace(tokenId) || script.TokenManager.IsTokenComment(tokenId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks that the token streams of two SqlScript objects differ only by whitespace tokens or
|
|
/// by the relative positioning of commas and comments. The important rule enforced is that there are
|
|
/// no changes in relative positioning which involve tokens other than commas, comments or whitespaces.
|
|
/// </summary>
|
|
/// <param name="script1">SQL script containing the first token stream.</param>
|
|
/// <param name="script2">SQL script containing the second token stream.</param>
|
|
public static void VerifyTokenStreamsOnlyDifferByWhitespace(SqlScript script1, SqlScript script2)
|
|
{
|
|
// We break down the relative positioning problem into assuring that the token streams have identical ids
|
|
// both when we ignore whitespaces and commas as well as when we ignore whitespaces and comments
|
|
VerifyTokenStreamsOnlyDifferBy(script1, script2, IsTokenWhitespaceOrComma);
|
|
VerifyTokenStreamsOnlyDifferBy(script1, script2, IsTokenWhitespaceOrComment);
|
|
}
|
|
|
|
internal delegate bool IgnoreToken(SqlScript script, int tokenIndex);
|
|
|
|
public static void VerifyTokenStreamsOnlyDifferBy(SqlScript script1, SqlScript script2, IgnoreToken ignoreToken )
|
|
{
|
|
int t1 = 0;
|
|
int t2 = 0;
|
|
|
|
while (t1 < script1.TokenManager.Count && t2 < script2.TokenManager.Count)
|
|
{
|
|
// advance t1 until it is pointing at a non-whitespace token
|
|
while (t1 < script1.TokenManager.Count && ignoreToken(script1, t1))
|
|
{
|
|
++t1;
|
|
}
|
|
|
|
// advance t2 until it is pointing at a non-whitespace token
|
|
while (t2 < script2.TokenManager.Count && ignoreToken(script2, t2))
|
|
{
|
|
++t2;
|
|
}
|
|
|
|
if (t1 >= script1.TokenManager.Count || t2 >= script2.TokenManager.Count)
|
|
{
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// TODO: need special logic here to deal with the placement of commas
|
|
//
|
|
|
|
// verify the tokens are equal
|
|
if (script1.TokenManager.TokenList[t1].TokenId != script2.TokenManager.TokenList[t2].TokenId)
|
|
{
|
|
string msg = "The comparison failed between tokens at {0} & {1}. The token IDs were {2} and {3} respectively. Script1 = {4}. Script2 = {5}";
|
|
msg = string.Format(CultureInfo.CurrentCulture, msg, t1, t2, script1.TokenManager.TokenList[t1].TokenId, script2.TokenManager.TokenList[t2].TokenId, script1.Sql, script2.Sql);
|
|
throw new FormatFailedException(msg);
|
|
}
|
|
|
|
++t1;
|
|
++t2;
|
|
}
|
|
|
|
// one of the streams is exhausted, verify that the only tokens left in the other stream are whitespace tokens
|
|
Debug.Assert(t1 >= script1.TokenManager.Count || t2 >= script2.TokenManager.Count, "expected to be at the end of one of the token's streams");
|
|
int t = t1;
|
|
SqlScript s = script1;
|
|
if (t2 < script2.TokenManager.Count)
|
|
{
|
|
Debug.Assert(t1 >= script1.TokenManager.Count, "expected to be at end of script1's token stream");
|
|
t = t2;
|
|
s = script2;
|
|
}
|
|
|
|
while (t < s.TokenManager.Count)
|
|
{
|
|
if (!ignoreToken(s, t))
|
|
{
|
|
string msg = "Unexpected non-whitespace token at index {0}, token ID {1}";
|
|
msg = string.Format(CultureInfo.CurrentCulture, msg, t, s.TokenManager.TokenList[t].TokenId);
|
|
throw new FormatFailedException(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|