File: TaskList\AbstractTaskListService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.TaskList;
 
internal abstract class AbstractTaskListService : ITaskListService
{
    protected abstract bool PreprocessorHasComment(SyntaxTrivia trivia);
    protected abstract bool IsSingleLineComment(SyntaxTrivia trivia);
    protected abstract bool IsMultilineComment(SyntaxTrivia trivia);
    protected abstract bool IsIdentifierCharacter(char ch);
 
    protected abstract string GetNormalizedText(string message);
    protected abstract int GetCommentStartingIndex(string message);
    protected abstract void AppendTaskListItems(ImmutableArray<TaskListItemDescriptor> descriptors, SyntacticDocument document, SyntaxTrivia trivia, ArrayBuilder<TaskListItem> items);
 
    public async Task<ImmutableArray<TaskListItem>> GetTaskListItemsAsync(
        Document document,
        ImmutableArray<TaskListItemDescriptor> descriptors,
        CancellationToken cancellationToken)
    {
        var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false);
        if (client != null)
        {
            var result = await client.TryInvokeAsync<IRemoteTaskListService, ImmutableArray<TaskListItem>>(
                document.Project,
                (service, checksum, cancellationToken) => service.GetTaskListItemsAsync(checksum, document.Id, descriptors, cancellationToken),
                cancellationToken).ConfigureAwait(false);
 
            if (!result.HasValue)
                return [];
 
            return result.Value;
        }
 
        return await GetTaskListItemsInProcessAsync(document, descriptors, cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<ImmutableArray<TaskListItem>> GetTaskListItemsInProcessAsync(
        Document document,
        ImmutableArray<TaskListItemDescriptor> descriptors,
        CancellationToken cancellationToken)
    {
        if (descriptors.IsEmpty)
            return [];
 
        cancellationToken.ThrowIfCancellationRequested();
 
        // strongly hold onto text and tree
        var syntaxDoc = await SyntacticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
 
        // reuse list
        using var _ = ArrayBuilder<TaskListItem>.GetInstance(out var items);
 
        foreach (var trivia in syntaxDoc.Root.DescendantTrivia())
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            if (!ContainsComments(trivia))
                continue;
 
            AppendTaskListItems(descriptors, syntaxDoc, trivia, items);
        }
 
        return items.ToImmutableAndClear();
    }
 
    private bool ContainsComments(SyntaxTrivia trivia)
        => PreprocessorHasComment(trivia) || IsSingleLineComment(trivia) || IsMultilineComment(trivia);
 
    protected void AppendTaskListItemsOnSingleLine(
        ImmutableArray<TaskListItemDescriptor> descriptors,
        SyntacticDocument document,
        string message, int start,
        ArrayBuilder<TaskListItem> items)
    {
        var index = GetCommentStartingIndex(message);
        if (index >= message.Length)
            return;
 
        var normalized = GetNormalizedText(message);
        foreach (var commentDescriptor in descriptors)
        {
            var token = commentDescriptor.Text;
            if (string.Compare(
                    normalized, index, token, indexB: 0,
                    length: token.Length, comparisonType: StringComparison.OrdinalIgnoreCase) != 0)
            {
                continue;
            }
 
            if (message.Length > index + token.Length && IsIdentifierCharacter(message[index + token.Length]))
                // they wrote something like:
                // todoboo
                // instead of
                // todo
                continue;
 
            var trimmedMessage = message[index..];
            var position = start + index;
 
            // Go through SyntaxTree so that any `#line` remapping is picked up
            var location = document.SyntaxTree.GetLocation(new TextSpan(position, 0));
 
            items.Add(new TaskListItem(
                commentDescriptor.Priority, trimmedMessage, document.Document.Id, location.GetLineSpan(), location.GetMappedLineSpan()));
        }
    }
 
    protected void ProcessMultilineComment(
        ImmutableArray<TaskListItemDescriptor> commentDescriptors,
        SyntacticDocument document,
        SyntaxTrivia trivia, int postfixLength,
        ArrayBuilder<TaskListItem> items)
    {
        // this is okay since we know it is already alive
        var text = document.Text;
 
        var fullSpan = trivia.FullSpan;
        var fullString = trivia.ToFullString();
 
        var startLine = text.Lines.GetLineFromPosition(fullSpan.Start);
        var endLine = text.Lines.GetLineFromPosition(fullSpan.End);
 
        // single line multiline comments
        if (startLine.LineNumber == endLine.LineNumber)
        {
            var message = postfixLength == 0 ? fullString : fullString[..(fullSpan.Length - postfixLength)];
            AppendTaskListItemsOnSingleLine(commentDescriptors, document, message, fullSpan.Start, items);
            return;
        }
 
        // multiline 
        var startMessage = text.ToString(TextSpan.FromBounds(fullSpan.Start, startLine.End));
        AppendTaskListItemsOnSingleLine(commentDescriptors, document, startMessage, fullSpan.Start, items);
 
        for (var lineNumber = startLine.LineNumber + 1; lineNumber < endLine.LineNumber; lineNumber++)
        {
            var line = text.Lines[lineNumber];
            var message = line.ToString();
 
            AppendTaskListItemsOnSingleLine(commentDescriptors, document, message, line.Start, items);
        }
 
        var length = fullSpan.End - endLine.Start;
        if (length >= postfixLength)
            length -= postfixLength;
 
        var endMessage = text.ToString(new TextSpan(endLine.Start, length));
        AppendTaskListItemsOnSingleLine(commentDescriptors, document, endMessage, endLine.Start, items);
    }
}