<commit_before>﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers
{
    [ExportCompletionProvider(nameof(AttributeNamedParameterCompletionProvider), LanguageNames.CSharp)]
    [ExtensionOrder(After = nameof(FirstBuiltInCompletionProvider))]
    [Shared]
    internal class AttributeNamedParameterCompletionProvider : LSPCompletionProvider
    {
        private const string EqualsString = "=";
        private const string SpaceEqualsString = " =";
        private const string ColonString = ":";

        private static readonly CompletionItemRules _spaceItemFilterRule = CompletionItemRules.Default.WithFilterCharacterRule(
            CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' '));

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public AttributeNamedParameterCompletionProvider()
        {
        }

        internal override string Language => LanguageNames.CSharp;

        public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
            => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options);

        public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters;

        public override async Task ProvideCompletionsAsync(CompletionContext context)
        {
            try
            {
                var document = context.Document;
                var position = context.Position;
                var cancellationToken = context.CancellationToken;

                var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                if (syntaxTree.IsInNonUserCode(position, cancellationToken))
                {
                    return;
                }

                var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
                token = token.GetPreviousTokenIfTouchingWord(position);

                if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.CommaToken))
                {
                    return;
                }

                if (token.Parent!.Parent is not AttributeSyntax attributeSyntax || token.Parent is not AttributeArgumentListSyntax attributeArgumentList)
                {
                    return;
                }

                if (IsAfterNameColonArgument(token) || IsAfterNameEqualsArgument(token))
                {
                    context.IsExclusive = true;
                }

                // We actually want to collect two sets of named parameters to present the user.  The
                // normal named parameters that come from the attribute constructors.  These will be
                // presented like "goo:".  And also the named parameters that come from the writable
                // fields/properties in the attribute.  These will be presented like "bar =".  

                var existingNamedParameters = GetExistingNamedParameters(attributeArgumentList, position);

                var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeSyntax, cancellationToken).ConfigureAwait(false);
                var nameColonItems = GetNameColonItems(context, semanticModel, token, attributeSyntax, existingNamedParameters);
                var nameEqualsItems = GetNameEqualsItems(context, semanticModel, token, attributeSyntax, existingNamedParameters);

                context.AddItems(nameEqualsItems);

                // If we're after a name= parameter, then we only want to show name= parameters.
                // Otherwise, show name: parameters too.
                if (!IsAfterNameEqualsArgument(token))
                {
                    context.AddItems(nameColonItems);
                }
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e))
            {
                // nop
            }
        }

        private static bool IsAfterNameColonArgument(SyntaxToken token)
        {
            if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList)
            {
                foreach (var item in argumentList.Arguments.GetWithSeparators())
                {
                    if (item.IsToken && item.AsToken() == token)
                    {
                        return false;
                    }

                    var node = (AttributeArgumentSyntax?)item.AsNode();
                    if (node?.NameColon != null)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static bool IsAfterNameEqualsArgument(SyntaxToken token)
        {
            if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList)
            {
                foreach (var item in argumentList.Arguments.GetWithSeparators())
                {
                    if (item.IsToken && item.AsToken() == token)
                    {
                        return false;
                    }

                    var node = (AttributeArgumentSyntax?)item.AsNode();
                    if (node?.NameEquals != null)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static ImmutableArray<CompletionItem> GetNameEqualsItems(
            CompletionContext context, SemanticModel semanticModel,
            SyntaxToken token, AttributeSyntax attributeSyntax, ISet<string> existingNamedParameters)
        {
            var attributeNamedParameters = GetAttributeNamedParameters(semanticModel, context.Position, attributeSyntax, context.CancellationToken);
            var unspecifiedNamedParameters = attributeNamedParameters.Where(p => !existingNamedParameters.Contains(p.Name));

            var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken);
            var displayTextSuffix = !rightToken.IsKind(SyntaxKind.EqualsToken) ? SpaceEqualsString : null;

            var q = from p in attributeNamedParameters
                    where !existingNamedParameters.Contains(p.Name)
                    select SymbolCompletionItem.CreateWithSymbolId(
                       displayText: p.Name.ToIdentifierToken().ToString(),
                       displayTextSuffix: displayTextSuffix,
                       insertionText: null,
                       symbols: ImmutableArray.Create(p),
                       contextPosition: token.SpanStart,<commit_msg>Invert if 1<commit_after>﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers
{
    [ExportCompletionProvider(nameof(AttributeNamedParameterCompletionProvider), LanguageNames.CSharp)]
    [ExtensionOrder(After = nameof(FirstBuiltInCompletionProvider))]
    [Shared]
    internal class AttributeNamedParameterCompletionProvider : LSPCompletionProvider
    {
        private const string EqualsString = "=";
        private const string SpaceEqualsString = " =";
        private const string ColonString = ":";

        private static readonly CompletionItemRules _spaceItemFilterRule = CompletionItemRules.Default.WithFilterCharacterRule(
            CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ' '));

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public AttributeNamedParameterCompletionProvider()
        {
        }

        internal override string Language => LanguageNames.CSharp;

        public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
            => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options);

        public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters;

        public override async Task ProvideCompletionsAsync(CompletionContext context)
        {
            try
            {
                var document = context.Document;
                var position = context.Position;
                var cancellationToken = context.CancellationToken;

                var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                if (syntaxTree.IsInNonUserCode(position, cancellationToken))
                {
                    return;
                }

                var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
                token = token.GetPreviousTokenIfTouchingWord(position);

                if (token.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.CommaToken))
                {
                    return;
                }

                if (token.Parent!.Parent is not AttributeSyntax attributeSyntax || token.Parent is not AttributeArgumentListSyntax attributeArgumentList)
                {
                    return;
                }

                if (IsAfterNameColonArgument(token) || IsAfterNameEqualsArgument(token))
                {
                    context.IsExclusive = true;
                }

                // We actually want to collect two sets of named parameters to present the user.  The
                // normal named parameters that come from the attribute constructors.  These will be
                // presented like "goo:".  And also the named parameters that come from the writable
                // fields/properties in the attribute.  These will be presented like "bar =".  

                var existingNamedParameters = GetExistingNamedParameters(attributeArgumentList, position);

                var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeSyntax, cancellationToken).ConfigureAwait(false);
                var nameColonItems = GetNameColonItems(context, semanticModel, token, attributeSyntax, existingNamedParameters);
                var nameEqualsItems = GetNameEqualsItems(context, semanticModel, token, attributeSyntax, existingNamedParameters);

                context.AddItems(nameEqualsItems);

                // If we're after a name= parameter, then we only want to show name= parameters.
                // Otherwise, show name: parameters too.
                if (!IsAfterNameEqualsArgument(token))
                {
                    context.AddItems(nameColonItems);
                }
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e))
            {
                // nop
            }
        }

        private static bool IsAfterNameColonArgument(SyntaxToken token)
        {
            if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList)
            {
                foreach (var item in argumentList.Arguments.GetWithSeparators())
                {
                    if (item.IsToken && item.AsToken() == token)
                    {
                        return false;
                    }

                    var node = (AttributeArgumentSyntax?)item.AsNode();
                    if (node?.NameColon != null)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static bool IsAfterNameEqualsArgument(SyntaxToken token)
        {
            if (token.Kind() == SyntaxKind.CommaToken && token.Parent is AttributeArgumentListSyntax argumentList)
            {
                foreach (var item in argumentList.Arguments.GetWithSeparators())
                {
                    if (item.IsToken && item.AsToken() == token)
                    {
                        return false;
                    }

                    var node = (AttributeArgumentSyntax?)item.AsNode();
                    if (node?.NameEquals != null)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static ImmutableArray<CompletionItem> GetNameEqualsItems(
            CompletionContext context, SemanticModel semanticModel,
            SyntaxToken token, AttributeSyntax attributeSyntax, ISet<string> existingNamedParameters)
        {
            var attributeNamedParameters = GetAttributeNamedParameters(semanticModel, context.Position, attributeSyntax, context.CancellationToken);
            var unspecifiedNamedParameters = attributeNamedParameters.Where(p => !existingNamedParameters.Contains(p.Name));

            var rightToken = semanticModel.SyntaxTree.FindTokenOnRightOfPosition(context.Position, context.CancellationToken);
            var displayTextSuffix = rightToken.IsKind(SyntaxKind.EqualsToken) ? null : SpaceEqualsString;

            var q = from p in attributeNamedParameters
                    where !existingNamedParameters.Contains(p.Name)
                    select SymbolCompletionItem.CreateWithSymbolId(
                       displayText: p.Name.ToIdentifierToken().ToString(),
                       displayTextSuffix: displayTextSuffix,
                       insertionText: null,
                       symbols: ImmutableArray.Create(p),
                       contextPosition: token.SpanStart,
{'fraction_non_alphanumeric': 0.06784699742353174, 'fraction_numerical': 6.606328863050802e-05, 'mean_word_length': 2.138710346257516, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '382398', 'n_tokens_mistral': 3452, 'n_tokens_neox': 3252, 'n_words': 811}