CSharpStructureHelpers.cs 13.8 KB
Newer Older
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;
5
using System.Collections.Immutable;
6 7 8 9
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
10
using Microsoft.CodeAnalysis.Structure;
11 12 13
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

14
namespace Microsoft.CodeAnalysis.CSharp.Structure
15
{
C
CyrusNajmabadi 已提交
16
    internal static class CSharpStructureHelpers
17 18
    {
        public const string Ellipsis = "...";
19
        public const string MultiLineCommentSuffix = "*/";
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
        public const int MaxXmlDocCommentBannerLength = 120;

        private static int GetCollapsibleStart(SyntaxToken firstToken)
        {
            // If the *next* token has any leading comments, we use the end of the last one.
            // If not, we check *this* token to see if it has any trailing comments and use the last one;
            // otherwise, we use the end of this token.

            var start = firstToken.Span.End;

            var nextToken = firstToken.GetNextToken();
            if (nextToken.Kind() != SyntaxKind.None && nextToken.HasLeadingTrivia)
            {
                var lastLeadingCommentTrivia = nextToken.LeadingTrivia.GetLastComment();
                if (lastLeadingCommentTrivia != null)
                {
                    start = lastLeadingCommentTrivia.Value.Span.End;
                }
            }

            if (firstToken.HasTrailingTrivia)
            {
42 43
                var lastTrailingCommentOrWhitespaceTrivia = firstToken.TrailingTrivia.GetLastCommentOrWhitespace();
                if (lastTrailingCommentOrWhitespaceTrivia != null)
44
                {
45
                    start = lastTrailingCommentOrWhitespaceTrivia.Value.Span.End;
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
                }
            }

            return start;
        }

        private static int GetCollapsibleEnd(SyntaxToken lastToken)
        {
            // If the token has any trailing comments, we use the end of the token;
            // otherwise, we skip to the start of the first new line trivia.

            var end = lastToken.Span.End;

            if (lastToken.HasTrailingTrivia &&
                !lastToken.TrailingTrivia.Any(SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia))
            {
                var firstNewLineTrivia = lastToken.TrailingTrivia.GetFirstNewLine();
                if (firstNewLineTrivia != null)
                {
                    end = firstNewLineTrivia.Value.SpanStart;
                }
            }

            return end;
        }

        public static SyntaxToken GetLastInlineMethodBlockToken(SyntaxNode node)
        {
            var lastToken = node.GetLastToken(includeZeroWidth: true);
            if (lastToken.Kind() == SyntaxKind.None)
            {
                return default(SyntaxToken);
            }

            // If the next token is a semicolon, and we aren't in the initializer of a for-loop, use that token as the end.

            SyntaxToken nextToken = lastToken.GetNextToken(includeSkipped: true);
            if (nextToken.Kind() != SyntaxKind.None && nextToken.Kind() == SyntaxKind.SemicolonToken)
            {
                var forStatement = nextToken.GetAncestor<ForStatementSyntax>();
                if (forStatement != null && forStatement.FirstSemicolonToken == nextToken)
                {
                    return default(SyntaxToken);
                }

                lastToken = nextToken;
            }

            return lastToken;
        }

        private static string CreateCommentBannerTextWithPrefix(string text, string prefix)
        {
            Contract.ThrowIfNull(text);
            Contract.ThrowIfNull(prefix);

            int prefixLength = prefix.Length;
            return prefix + " " + text.Substring(prefixLength).Trim() + " " + Ellipsis;
        }

        private static string GetCommentBannerText(SyntaxTrivia comment)
        {
            Contract.ThrowIfFalse(comment.IsSingleLineComment() || comment.IsMultiLineComment());

            if (comment.IsSingleLineComment())
            {
                return CreateCommentBannerTextWithPrefix(comment.ToString(), "//");
            }
            else if (comment.IsMultiLineComment())
            {
                int lineBreakStart = comment.ToString().IndexOfAny(new char[] { '\r', '\n' });

                var text = comment.ToString();
                if (lineBreakStart >= 0)
                {
                    text = text.Substring(0, lineBreakStart);
                }
                else
                {
125
                    text = text.EndsWith(MultiLineCommentSuffix) ? text.Substring(0, text.Length - MultiLineCommentSuffix.Length) : text;
126 127 128 129 130 131 132 133 134 135
                }

                return CreateCommentBannerTextWithPrefix(text, "/*");
            }
            else
            {
                return string.Empty;
            }
        }

136 137
        private static BlockSpan CreateCommentBlockSpan(
            SyntaxTrivia startComment, SyntaxTrivia endComment)
138 139 140
        {
            var span = TextSpan.FromBounds(startComment.SpanStart, endComment.Span.End);

141 142 143
            return new BlockSpan(
                isCollapsible: true,
                textSpan: span,
144 145
                hintSpan: span,
                bannerText: GetCommentBannerText(startComment),
146 147
                autoCollapse: true,
                type: BlockTypes.Nonstructural);
148 149 150
        }

        // For testing purposes
151 152
        internal static ImmutableArray<BlockSpan> CreateCommentBlockSpan(
            SyntaxTriviaList triviaList)
153
        {
154
            var result = ImmutableArray.CreateBuilder<BlockSpan>();
155
            CollectCommentBlockSpans(triviaList, result);
156
            return result.ToImmutable();
157 158
        }

159
        public static void CollectCommentBlockSpans(
160
            SyntaxTriviaList triviaList, ImmutableArray<BlockSpan>.Builder spans)
161 162 163 164 165 166 167 168 169 170
        {
            if (triviaList.Count > 0)
            {
                SyntaxTrivia? startComment = null;
                SyntaxTrivia? endComment = null;

                Action completeSingleLineCommentGroup = () =>
                {
                    if (startComment != null)
                    {
171
                        var singleLineCommentGroupRegion = CreateCommentBlockSpan(startComment.Value, endComment.Value);
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
                        spans.Add(singleLineCommentGroupRegion);
                        startComment = null;
                        endComment = null;
                    }
                };

                // Iterate through trivia and collect the following:
                //    1. Groups of contiguous single-line comments that are only separated by whitespace
                //    2. Multi-line comments
                foreach (var trivia in triviaList)
                {
                    if (trivia.IsSingleLineComment())
                    {
                        startComment = startComment ?? trivia;
                        endComment = trivia;
                    }
                    else if (trivia.IsMultiLineComment())
                    {
                        completeSingleLineCommentGroup();

192
                        var multilineCommentRegion = CreateCommentBlockSpan(trivia, trivia);
193 194 195 196 197 198 199 200 201 202 203 204 205 206
                        spans.Add(multilineCommentRegion);
                    }
                    else if (!trivia.MatchesKind(SyntaxKind.WhitespaceTrivia,
                                                 SyntaxKind.EndOfLineTrivia,
                                                 SyntaxKind.EndOfFileToken))
                    {
                        completeSingleLineCommentGroup();
                    }
                }

                completeSingleLineCommentGroup();
            }
        }

207
        public static void CollectCommentBlockSpans(
208
            SyntaxNode node, ImmutableArray<BlockSpan>.Builder spans)
209 210 211
        {
            if (node == null)
            {
212
                throw new ArgumentNullException(nameof(node));
213 214 215 216
            }

            var triviaList = node.GetLeadingTrivia();

217
            CollectCommentBlockSpans(triviaList, spans);
218 219
        }

220 221 222
        private static BlockSpan CreateBlockSpan(
            TextSpan textSpan, string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
223
        {
224 225
            return CreateBlockSpan(
                textSpan, textSpan, bannerText, autoCollapse, type, isCollapsible);
226 227
        }

228 229 230 231
        private static BlockSpan CreateBlockSpan(
            TextSpan textSpan, TextSpan hintSpan, 
            string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
232
        {
233 234 235 236
            return new BlockSpan(
                textSpan: textSpan,
                hintSpan: hintSpan,
                bannerText: bannerText,
237 238 239
                autoCollapse: autoCollapse,
                type: type,
                isCollapsible: isCollapsible);
240 241
        }

242 243 244
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
245
        {
246
            return CreateBlockSpan(
247 248
                node.Span,
                bannerText,
249 250 251
                autoCollapse, 
                type,
                isCollapsible);
252 253
        }

254 255 256 257
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, SyntaxToken syntaxToken, 
            string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
258
        {
259
            return CreateBlockSpan(
260 261 262 263
                node,
                syntaxToken,
                node.GetLastToken(),
                bannerText,
264 265 266
                autoCollapse,
                type,
                isCollapsible);
267 268
        }

269 270 271 272
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, SyntaxToken startToken, 
            int endPos, string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
273 274 275 276 277 278 279 280 281 282 283 284 285 286
        {
            // If the SyntaxToken is actually missing, don't attempt to create an outlining region.
            if (startToken.IsMissing)
            {
                return null;
            }

            // Since we creating a span for everything after syntaxToken to ensure
            // that it collapses properly. However, the hint span begins at the start
            // of the next token so indentation in the tooltip is accurate.

            var span = TextSpan.FromBounds(GetCollapsibleStart(startToken), endPos);
            var hintSpan = TextSpan.FromBounds(node.SpanStart, endPos);

287
            return CreateBlockSpan(
288 289 290
                span,
                hintSpan,
                bannerText,
291 292 293
                autoCollapse,
                type,
                isCollapsible);
294 295
        }

296 297 298 299
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, SyntaxToken startToken, 
            SyntaxToken endToken, string bannerText, bool autoCollapse,
            string type, bool isCollapsible)
300
        {
301
            return CreateBlockSpan(
302 303 304 305
                node,
                startToken,
                GetCollapsibleEnd(endToken),
                bannerText,
306 307 308
                autoCollapse, 
                type,
                isCollapsible);
309 310
        }

311 312
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, bool autoCollapse, string type, bool isCollapsible)
313
        {
314
            return CreateBlockSpan(
315 316
                node,
                bannerText: Ellipsis,
317 318 319
                autoCollapse: autoCollapse,
                type: type,
                isCollapsible: isCollapsible);
320 321 322 323
        }

        // Adds everything after 'syntaxToken' up to and including the end 
        // of node as a region.  The snippet to display is just "..."
324 325 326
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, SyntaxToken syntaxToken, 
            bool autoCollapse, string type, bool isCollapsible)
327
        {
328
            return CreateBlockSpan(
329 330
                node, syntaxToken,
                bannerText: Ellipsis,
331 332 333
                autoCollapse: autoCollapse,
                type: type,
                isCollapsible: isCollapsible);
334 335 336 337
        }

        // Adds everything after 'syntaxToken' up to and including the end 
        // of node as a region.  The snippet to display is just "..."
338 339 340
        public static BlockSpan CreateBlockSpan(
            SyntaxNode node, SyntaxToken startToken, SyntaxToken endToken, 
            bool autoCollapse, string type, bool isCollapsible)
341
        {
342
            return CreateBlockSpan(
343 344
                node, startToken, endToken,
                bannerText: Ellipsis,
345 346 347
                autoCollapse: autoCollapse,
                type: type,
                isCollapsible: isCollapsible);
348 349 350 351 352
        }

        // Adds the span surrounding the syntax list as a region.  The
        // snippet shown is the text from the first line of the first 
        // node in the list.
353 354 355
        public static BlockSpan CreateBlockSpan(
            IEnumerable<SyntaxNode> syntaxList, bool autoCollapse, 
            string type, bool isCollapsible)
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
        {
            if (syntaxList.IsEmpty())
            {
                return null;
            }

            var end = GetCollapsibleEnd(syntaxList.Last().GetLastToken());

            var spanStart = syntaxList.First().GetFirstToken().FullSpan.End;
            var spanEnd = end >= spanStart
                ? end
                : spanStart;

            var hintSpanStart = syntaxList.First().SpanStart;
            var hintSpanEnd = end >= hintSpanStart
                ? end
                : hintSpanStart;

374
            return CreateBlockSpan(
375 376 377
                textSpan: TextSpan.FromBounds(spanStart, spanEnd),
                hintSpan: TextSpan.FromBounds(hintSpanStart, hintSpanEnd),
                bannerText: Ellipsis,
378 379 380
                autoCollapse: autoCollapse,
                type: type,
                isCollapsible: isCollapsible);
381 382
        }
    }
C
CyrusNajmabadi 已提交
383
}