AbstractAddParameterCodeFixProvider.cs 23.9 KB
Newer Older
C
CyrusNajmabadi 已提交
1 2 3 4
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
C
CyrusNajmabadi 已提交
5
using System.Collections.Immutable;
C
CyrusNajmabadi 已提交
6
using System.Linq;
7
using System.Threading;
C
CyrusNajmabadi 已提交
8
using System.Threading.Tasks;
9
using Microsoft.CodeAnalysis.CodeActions;
C
CyrusNajmabadi 已提交
10
using Microsoft.CodeAnalysis.CodeFixes;
11 12
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
13
using Microsoft.CodeAnalysis.Formatting;
14
using Microsoft.CodeAnalysis.LanguageServices;
T
Tomas Matousek 已提交
15
using Microsoft.CodeAnalysis.PooledObjects;
16 17 18
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
C
CyrusNajmabadi 已提交
19 20 21

namespace Microsoft.CodeAnalysis.AddParameter
{
22 23 24 25 26 27 28 29 30 31 32 33
    internal abstract class AbstractAddParameterCodeFixProvider<
        TArgumentSyntax,
        TAttributeArgumentSyntax,
        TArgumentListSyntax,
        TAttributeArgumentListSyntax,
        TInvocationExpressionSyntax,
        TObjectCreationExpressionSyntax> : CodeFixProvider
        where TArgumentSyntax : SyntaxNode
        where TArgumentListSyntax : SyntaxNode
        where TAttributeArgumentListSyntax : SyntaxNode
        where TInvocationExpressionSyntax : SyntaxNode
        where TObjectCreationExpressionSyntax : SyntaxNode
C
CyrusNajmabadi 已提交
34
    {
C
CyrusNajmabadi 已提交
35 36
        protected abstract ImmutableArray<string> TooManyArgumentsDiagnosticIds { get; }

37 38 39
        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var cancellationToken = context.CancellationToken;
C
CyrusNajmabadi 已提交
40
            var diagnostic = context.Diagnostics.First();
41 42 43 44

            var document = context.Document;
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

C
CyrusNajmabadi 已提交
45
            var initialNode = root.FindNode(diagnostic.Location.SourceSpan);
46 47

            for (var node = initialNode; node != null; node = node.Parent)
48 49 50
            {
                if (node is TObjectCreationExpressionSyntax objectCreation)
                {
C
CyrusNajmabadi 已提交
51
                    var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic);
52
                    await HandleObjectCreationExpressionAsync(context, objectCreation, argumentOpt).ConfigureAwait(false);
53 54 55 56
                    return;
                }
                else if (node is TInvocationExpressionSyntax invocationExpression)
                {
C
CyrusNajmabadi 已提交
57
                    var argumentOpt = TryGetRelevantArgument(initialNode, node, diagnostic);
58
                    await HandleInvocationExpressionAsync(context, invocationExpression, argumentOpt).ConfigureAwait(false);
59 60 61 62 63
                    return;
                }
            }
        }

C
CyrusNajmabadi 已提交
64 65
        private TArgumentSyntax TryGetRelevantArgument(
            SyntaxNode initialNode, SyntaxNode node, Diagnostic diagnostic)
66
        {
C
CyrusNajmabadi 已提交
67 68 69 70 71
            if (this.TooManyArgumentsDiagnosticIds.Contains(diagnostic.Id))
            {
                return null;
            }

72
            return initialNode.GetAncestorsOrThis<TArgumentSyntax>()
73
                              .LastOrDefault(a => a.AncestorsAndSelf().Contains(node));
74 75 76 77
        }

        private Task HandleInvocationExpressionAsync(
            CodeFixContext context, TInvocationExpressionSyntax invocationExpression, TArgumentSyntax argumentOpt)
78
        {
C
CyrusNajmabadi 已提交
79
            // Currently we only support this for 'new obj' calls.
80 81 82 83 84
            return SpecializedTasks.EmptyTask;
        }

        private async Task HandleObjectCreationExpressionAsync(
            CodeFixContext context,
85 86
            TObjectCreationExpressionSyntax objectCreation,
            TArgumentSyntax argumentOpt)
87 88 89 90 91 92
        {
            var document = context.Document;
            var cancellationToken = context.CancellationToken;
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();

C
CyrusNajmabadi 已提交
93
            // Not supported if this is "new { ... }" (as there are no parameters at all.
94 95 96 97 98 99
            var typeNode = syntaxFacts.GetObjectCreationType(objectCreation);
            if (typeNode == null)
            {
                return;
            }

C
CyrusNajmabadi 已提交
100 101
            // If we can't figure out the type being created, or the type isn't in source,
            // then there's nothing we can do.
102
            var type = semanticModel.GetSymbolInfo(typeNode, cancellationToken).GetAnySymbol() as INamedTypeSymbol;
C
CyrusNajmabadi 已提交
103 104 105 106 107 108
            if (type == null)
            {
                return;
            }

            if (!type.IsNonImplicitAndFromSource())
109 110 111 112 113 114
            {
                return;
            }

            var arguments = (SeparatedSyntaxList<TArgumentSyntax>)syntaxFacts.GetArgumentsOfObjectCreationExpression(objectCreation);

115
            var comparer = syntaxFacts.StringComparer;
C
CyrusNajmabadi 已提交
116 117
            var constructorsAndArgumentToAdd = ArrayBuilder<(IMethodSymbol constructor, TArgumentSyntax argument, int index)>.GetInstance();

118 119
            foreach (var constructor in type.InstanceConstructors.OrderBy(m => m.Parameters.Length))
            {
C
CyrusNajmabadi 已提交
120
                if (constructor.IsNonImplicitAndFromSource() &&
121
                    NonParamsParameterCount(constructor) < arguments.Count)
122 123
                {
                    var argumentToAdd = DetermineFirstArgumentToAdd(
H
Heejae Chang 已提交
124
                        semanticModel, syntaxFacts, comparer, constructor,
125
                        arguments, argumentOpt);
126 127 128

                    if (argumentToAdd != null)
                    {
129 130 131 132 133 134 135 136 137
                        if (argumentOpt != null && argumentToAdd != argumentOpt)
                        {
                            // We were trying to fix a specific argument, but the argument we want
                            // to fix is something different.  That means there was an error earlier
                            // than this argument.  Which means we're looking at a non-viable 
                            // constructor.  Skip this one.
                            continue;
                        }

C
CyrusNajmabadi 已提交
138 139
                        constructorsAndArgumentToAdd.Add(
                            (constructor, argumentToAdd, arguments.IndexOf(argumentToAdd)));
140 141 142
                    }
                }
            }
C
CyrusNajmabadi 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

            // Order by the furthest argument index to the nearest argument index.  The ones with
            // larger argument indexes mean that we matched more earlier arguments (and thus are
            // likely to be the correct match).
            foreach (var tuple in constructorsAndArgumentToAdd.OrderByDescending(t => t.index))
            {
                var constructor = tuple.constructor;
                var argumentToAdd = tuple.argument;

                var parameters = constructor.Parameters.Select(p => p.ToDisplayString(SimpleFormat));
                var signature = $"{type.Name}({string.Join(", ", parameters)})";

                var title = string.Format(FeaturesResources.Add_parameter_to_0, signature);

                context.RegisterCodeFix(
C
CyrusNajmabadi 已提交
158 159
                    new MyCodeAction(title, c => FixAsync(document, constructor, argumentToAdd, arguments, c)),
                    context.Diagnostics);
C
CyrusNajmabadi 已提交
160
            }
161 162
        }

163 164 165
        private int NonParamsParameterCount(IMethodSymbol method)
            => method.IsParams() ? method.Parameters.Length - 1 : method.Parameters.Length;

166
        private async Task<Document> FixAsync(
H
Heejae Chang 已提交
167
            Document invocationDocument,
168 169 170 171 172 173 174
            IMethodSymbol method,
            TArgumentSyntax argument,
            SeparatedSyntaxList<TArgumentSyntax> argumentList,
            CancellationToken cancellationToken)
        {
            var methodDeclaration = await method.DeclaringSyntaxReferences[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false);

175 176
            var (parameterSymbol, isNamedArgument) = await CreateParameterSymbolAsync(
                invocationDocument, method, argument, cancellationToken).ConfigureAwait(false);
177 178

            var methodDocument = invocationDocument.Project.Solution.GetDocument(methodDeclaration.SyntaxTree);
179 180 181 182 183 184 185
            var syntaxFacts = methodDocument.GetLanguageService<ISyntaxFactsService>();
            var methodDeclarationRoot = methodDeclaration.SyntaxTree.GetRoot(cancellationToken);
            var editor = new SyntaxEditor(methodDeclarationRoot, methodDocument.Project.Solution.Workspace);

            var parameterDeclaration = editor.Generator.ParameterDeclaration(parameterSymbol)
                                                       .WithAdditionalAnnotations(Formatter.Annotation);

C
CyrusNajmabadi 已提交
186 187 188 189 190
            var existingParameters = editor.Generator.GetParameters(methodDeclaration);
            var insertionIndex = isNamedArgument
                ? existingParameters.Count
                : argumentList.IndexOf(argument);

191
            AddParameter(
C
CyrusNajmabadi 已提交
192 193
                syntaxFacts, editor, methodDeclaration, argument,
                insertionIndex, parameterDeclaration, cancellationToken);
H
Heejae Chang 已提交
194

195
            var newRoot = editor.GetChangedRoot();
196 197 198 199 200
            var newDocument = methodDocument.WithSyntaxRoot(newRoot);

            return newDocument;
        }

201 202
        private async Task<(IParameterSymbol, bool isNamedArgument)> CreateParameterSymbolAsync(
            Document invocationDocument,
C
CyrusNajmabadi 已提交
203 204
            IMethodSymbol method,
            TArgumentSyntax argument,
205
            CancellationToken cancellationToken)
206
        {
207 208 209 210 211 212 213 214
            var syntaxFacts = invocationDocument.GetLanguageService<ISyntaxFactsService>();
            var semanticFacts = invocationDocument.GetLanguageService<ISemanticFactsService>();
            var argumentName = syntaxFacts.GetNameForArgument(argument);
            var expression = syntaxFacts.GetExpressionOfArgument(argument);

            var semanticModel = await invocationDocument.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var parameterType = semanticModel.GetTypeInfo(expression).Type ?? semanticModel.Compilation.ObjectType;

215 216 217
            if (!string.IsNullOrWhiteSpace(argumentName))
            {
                var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol(
218
                    attributes: default, refKind: RefKind.None, isParams: false, type: parameterType, name: argumentName);
219

220
                return (newParameterSymbol, isNamedArgument: true);
221 222 223
            }
            else
            {
224 225
                var name = semanticFacts.GenerateNameForExpression(
                    semanticModel, expression, capitalize: false, cancellationToken: cancellationToken);
226 227 228
                var uniqueName = NameGenerator.EnsureUniqueness(name, method.Parameters.Select(p => p.Name));

                var newParameterSymbol = CodeGenerationSymbolFactory.CreateParameterSymbol(
229 230 231 232 233 234 235 236 237 238 239
                    attributes: default, refKind: RefKind.None, isParams: false, type: parameterType, name: uniqueName);

                return (newParameterSymbol, isNamedArgument: false);
            }
        }

        private static void AddParameter(
            ISyntaxFactsService syntaxFacts,
            SyntaxEditor editor,
            SyntaxNode declaration,
            TArgumentSyntax argument,
C
CyrusNajmabadi 已提交
240
            int insertionIndex,
241 242 243
            SyntaxNode parameterDeclaration,
            CancellationToken cancellationToken)
        {
C
CyrusNajmabadi 已提交
244
            var sourceText = declaration.SyntaxTree.GetText(cancellationToken);
245 246 247 248 249
            var generator = editor.Generator;

            var existingParameters = generator.GetParameters(declaration);
            var placeOnNewLine = ShouldPlaceParametersOnNewLine(existingParameters, cancellationToken);

250 251 252 253 254 255 256
            if (!placeOnNewLine)
            {
                // Trivial case.  Just let the stock editor impl handle this for us.
                editor.InsertParameter(declaration, insertionIndex, parameterDeclaration);
                return;
            }

C
CyrusNajmabadi 已提交
257
            if (insertionIndex == existingParameters.Count)
258
            {
259 260 261
                // Placing the last parameter on its own line.  Get the indentation of the 
                // curent last parameter and give the new last parameter the same indentation.
                var leadingIndentation = GetDesiredLeadingIndentation(
H
Heejae Chang 已提交
262
                    generator, syntaxFacts, existingParameters[existingParameters.Count - 1], includeLeadingNewLine: true);
263 264 265
                parameterDeclaration = parameterDeclaration.WithPrependedLeadingTrivia(leadingIndentation)
                                                            .WithAdditionalAnnotations(Formatter.Annotation);

266 267
                editor.AddParameter(declaration, parameterDeclaration);
            }
268
            else if (insertionIndex == 0)
269
            {
270 271 272 273 274 275
                // Inserting into the start of the list.  The existing first parameter might
                // be on the same line as the parameter list, or it might be on the next line.
                var firstParameter = existingParameters[0];
                var previousToken = firstParameter.GetFirstToken().GetPreviousToken();

                if (sourceText.AreOnSameLine(previousToken, firstParameter.GetFirstToken()))
276
                {
277
                    // First parameter is on hte same line as the method.  
278

279 280 281 282 283 284 285 286 287 288 289 290 291
                    // We want to insert the parameter at the front of the exsiting parameter
                    // list.  That means we need to move the current first parameter to a new
                    // line.  Give the current first parameter the indentation of the second
                    // parameter in the list.
                    editor.InsertParameter(declaration, insertionIndex, parameterDeclaration);
                    var nextParameter = existingParameters[insertionIndex];

                    var nextLeadingIndentation = GetDesiredLeadingIndentation(
                        generator, syntaxFacts, existingParameters[insertionIndex + 1], includeLeadingNewLine: true);
                    editor.ReplaceNode(
                        nextParameter,
                        nextParameter.WithPrependedLeadingTrivia(nextLeadingIndentation)
                                     .WithAdditionalAnnotations(Formatter.Annotation));
292 293 294
                }
                else
                {
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
                    // First parameter is on its own line.  No need to adjust its indentation.
                    // Just copy its indentation over to the parameter we're inserting, and
                    // make sure the current first parameter gets a newline so it stays on 
                    // its own line.

                    // We want to insert the parameter at the front of the exsiting parameter
                    // list.  That means we need to move the current first parameter to a new
                    // line.  Give the current first parameter the indentation of the second
                    // parameter in the list.
                    var firstLeadingIndentation = GetDesiredLeadingIndentation(
                        generator, syntaxFacts, existingParameters[0], includeLeadingNewLine: false);

                    editor.InsertParameter(declaration, insertionIndex,
                        parameterDeclaration.WithLeadingTrivia(firstLeadingIndentation));
                    var nextParameter = existingParameters[insertionIndex];

                    editor.ReplaceNode(
                        nextParameter,
                        nextParameter.WithPrependedLeadingTrivia(generator.ElasticCarriageReturnLineFeed)
                                     .WithAdditionalAnnotations(Formatter.Annotation));
315 316
                }
            }
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
            else
            {
                // We're inserting somewhere after the start (but not at the end). Because 
                // we've set placeOnNewLine, we know that the current comma we'll be placed
                // after already have a newline following it.  So all we need for this new 
                // parameter is to get the indentation of the following parameter.
                // Because we're going to 'steal' the existing comma from that parameter,
                // ensure that the next parameter has a new-line added to it so that it will
                // still stay on a new line.
                var nextParameter = existingParameters[insertionIndex];
                var leadingIndentation = GetDesiredLeadingIndentation(
                    generator, syntaxFacts, existingParameters[insertionIndex], includeLeadingNewLine: false);
                parameterDeclaration = parameterDeclaration.WithPrependedLeadingTrivia(leadingIndentation);

                editor.InsertParameter(declaration, insertionIndex, parameterDeclaration);
                editor.ReplaceNode(
                    nextParameter,
                    nextParameter.WithPrependedLeadingTrivia(generator.ElasticCarriageReturnLineFeed)
                                 .WithAdditionalAnnotations(Formatter.Annotation));
            }
337 338 339
        }

        private static List<SyntaxTrivia> GetDesiredLeadingIndentation(
H
Heejae Chang 已提交
340
            SyntaxGenerator generator, ISyntaxFactsService syntaxFacts,
341 342 343 344 345 346 347 348
            SyntaxNode node, bool includeLeadingNewLine)
        {
            var triviaList = new List<SyntaxTrivia>();
            if (includeLeadingNewLine)
            {
                triviaList.Add(generator.ElasticCarriageReturnLineFeed);
            }

H
Heejae Chang 已提交
349 350
            var lastWhitespace = default(SyntaxTrivia);
            foreach (var trivia in node.GetLeadingTrivia().Reverse())
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
            {
                if (syntaxFacts.IsWhitespaceTrivia(trivia))
                {
                    lastWhitespace = trivia;
                }
                else if (syntaxFacts.IsEndOfLineTrivia(trivia))
                {
                    break;
                }
            }

            if (lastWhitespace.RawKind != 0)
            {
                triviaList.Add(lastWhitespace);
            }

            return triviaList;
        }
369

370 371 372 373 374 375
        private static bool ShouldPlaceParametersOnNewLine(
            IReadOnlyList<SyntaxNode> parameters, CancellationToken cancellationToken)
        {
            if (parameters.Count <= 1)
            {
                return false;
376
            }
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391

            var text = parameters[0].SyntaxTree.GetText(cancellationToken);
            for (int i = 1, n = parameters.Count; i < n; i++)
            {
                var lastParameter = parameters[i - 1];
                var thisParameter = parameters[i];

                if (text.AreOnSameLine(lastParameter.GetLastToken(), thisParameter.GetFirstToken()))
                {
                    return false;
                }
            }

            // All parameters are on different lines.  Place the new parameter on a new line as well.
            return true;
392 393 394 395 396 397 398 399 400 401
        }

        private static readonly SymbolDisplayFormat SimpleFormat =
                    new SymbolDisplayFormat(
                        typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
                        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
                        parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeType,
                        miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes);

        private TArgumentSyntax DetermineFirstArgumentToAdd(
402 403 404 405
            SemanticModel semanticModel,
            ISyntaxFactsService syntaxFacts,
            StringComparer comparer,
            IMethodSymbol method,
406 407
            SeparatedSyntaxList<TArgumentSyntax> arguments,
            TArgumentSyntax argumentOpt)
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
        {
            var methodParameterNames = new HashSet<string>(comparer);
            methodParameterNames.AddRange(method.Parameters.Select(p => p.Name));

            for (int i = 0, n = arguments.Count; i < n; i++)
            {
                var argument = arguments[i];
                var argumentName = syntaxFacts.GetNameForArgument(argument);

                if (!string.IsNullOrWhiteSpace(argumentName))
                {
                    // If the user provided an argument-name and we don't have any parameters that
                    // match, then this is the argument we want to add a parameter for.
                    if (!methodParameterNames.Contains(argumentName))
                    {
                        return argument;
                    }
                }
                else
                {
                    // Positional argument.  If the position is beyond what the method supports,
                    // then this definitely is an argument we could add.
                    if (i >= method.Parameters.Length)
                    {
432 433 434 435 436 437
                        if (method.Parameters.LastOrDefault()?.IsParams == true)
                        {
                            // Last parameter is a params.  We can't place any parameters past it.
                            return null;
                        }

438 439 440
                        return argument;
                    }

C
CyrusNajmabadi 已提交
441 442
                    // Now check the type of the argument versus the type of the parameter.  If they
                    // don't match, then this is the argument we should make the parameter for.
443 444
                    var expressionOfArgumment = syntaxFacts.GetExpressionOfArgument(argument);
                    var argumentTypeInfo = semanticModel.GetTypeInfo(expressionOfArgumment);
C
CyrusNajmabadi 已提交
445 446 447
                    var isNullLiteral = syntaxFacts.IsNullLiteralExpression(expressionOfArgumment);
                    var isDefaultLiteral = syntaxFacts.IsDefaultLiteralExpression(expressionOfArgumment);

C
CyrusNajmabadi 已提交
448 449 450
                    if (argumentTypeInfo.Type == null && argumentTypeInfo.ConvertedType == null)
                    {
                        // Didn't know the type of the argument.  We shouldn't assume it doesn't
451 452
                        // match a parameter.  However, if the user wrote 'null' and it didn't
                        // match anything, then this is the problem argument.
C
CyrusNajmabadi 已提交
453
                        if (!isNullLiteral && !isDefaultLiteral)
454 455 456
                        {
                            continue;
                        }
C
CyrusNajmabadi 已提交
457 458
                    }

459 460
                    var parameter = method.Parameters[i];

C
CyrusNajmabadi 已提交
461
                    if (!TypeInfoMatchesType(argumentTypeInfo, parameter.Type, isNullLiteral, isDefaultLiteral))
462
                    {
C
CyrusNajmabadi 已提交
463
                        if (TypeInfoMatchesWithParamsExpansion(argumentTypeInfo, parameter, isNullLiteral, isDefaultLiteral))
464
                        {
C
CyrusNajmabadi 已提交
465 466 467 468
                            // The argument matched if we expanded out the params-parameter.
                            // As the params-parameter has to be last, there's nothing else to 
                            // do here.
                            return null;
469 470
                        }

471 472 473 474 475 476 477
                        return argument;
                    }
                }
            }

            return null;
        }
C
CyrusNajmabadi 已提交
478

C
CyrusNajmabadi 已提交
479 480 481
        private bool TypeInfoMatchesWithParamsExpansion(
            TypeInfo argumentTypeInfo, IParameterSymbol parameter, 
            bool isNullLiteral, bool isDefaultLiteral)
C
CyrusNajmabadi 已提交
482 483 484
        {
            if (parameter.IsParams && parameter.Type is IArrayTypeSymbol arrayType)
            {
C
CyrusNajmabadi 已提交
485
                if (TypeInfoMatchesType(argumentTypeInfo, arrayType.ElementType, isNullLiteral, isDefaultLiteral))
C
CyrusNajmabadi 已提交
486 487 488 489 490 491 492 493
                {
                    return true;
                }
            }

            return false;
        }

C
CyrusNajmabadi 已提交
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        private bool TypeInfoMatchesType(
            TypeInfo argumentTypeInfo, ITypeSymbol type,
            bool isNullLiteral, bool isDefaultLiteral)
        {
            if (type.Equals(argumentTypeInfo.Type) || type.Equals(argumentTypeInfo.ConvertedType))
            {
                return true;
            }

            if (isDefaultLiteral)
            {
                return true;
            }

            if (isNullLiteral)
            {
                return type.IsReferenceType || type.IsNullable();
            }

            return false;
        }
515

516
        private class MyCodeAction : CodeAction.DocumentChangeAction
C
CyrusNajmabadi 已提交
517
        {
518 519 520 521 522 523
            public MyCodeAction(
                string title,
                Func<CancellationToken, Task<Document>> createChangedDocument)
                : base(title, createChangedDocument)
            {
            }
C
CyrusNajmabadi 已提交
524 525
        }
    }
T
Tomas Matousek 已提交
526
}