// 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; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.QuickInfo { // Reproduces logic in IProjectionBufferFactoryServiceExtensions (editor layer) // Used for tests currenty, but probably needed for other non-vs-editor API consumers. internal static class IndentationHelper { /// /// Recomputes span segments so that all text lines appear to have the same reduction in indentation. /// This operation is typically used to align text for display when the initial span does not include all of the first line's identation. /// This operation will potentially split spans that cover multiple lines into separate spans. /// /// /// The initial set of spans to align. /// The number of spaces to /// public static ImmutableArray GetSpansWithAlignedIndentation( SourceText text, ImmutableArray classifiedSpans, int tabSize) { if (!classifiedSpans.IsDefault && classifiedSpans.Length > 0) { // We need to figure out the shortest indentation level of the exposed lines. We'll // then remove that indentation from all lines. var indentationColumn = DetermineIndentationColumn(text, classifiedSpans, tabSize); string spanClassificationType = null; var adjustedClassifiedSpans = new List(); for (var i = 0; i < classifiedSpans.Length; i++) { var classifiedSpan = classifiedSpans[i]; spanClassificationType = classifiedSpan.ClassificationType; var span = classifiedSpan.TextSpan; var startLineNumber = text.Lines.GetLineFromPosition(span.Start).LineNumber; var endLineNumber = text.Lines.GetLineFromPosition(span.End).LineNumber; for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { var line = text.Lines[lineNumber]; var lineOffsetOfColumn = line.GetLineOffsetFromColumn(indentationColumn, tabSize); var deletion = TextSpan.FromBounds(line.Start, line.Start + lineOffsetOfColumn); if (deletion.Start > span.Start) { var spanBeforeDeletion = TextSpan.FromBounds(span.Start, Math.Min(span.End, deletion.Start)); if (spanBeforeDeletion.Length > 0) { adjustedClassifiedSpans.Add(new ClassifiedSpan(spanClassificationType, spanBeforeDeletion)); } } if (deletion.End > span.Start) { span = TextSpan.FromBounds(Math.Min(deletion.End, span.End), span.End); } } if (span.Length > 0) { adjustedClassifiedSpans.Add(new ClassifiedSpan(spanClassificationType, span)); } } return adjustedClassifiedSpans.ToImmutableArray(); } else { return ImmutableArray.Empty; } } private static int DetermineIndentationColumn( SourceText text, ImmutableArray spans, int tabSize) { int? indentationColumn = null; foreach (var span in spans) { var startLineNumber = text.Lines.GetLineFromPosition(span.TextSpan.Start).LineNumber; var endLineNumber = text.Lines.GetLineFromPosition(span.TextSpan.End).LineNumber; // If the span starts after the first non-whitespace of the first line, we'll // exclude that line to avoid throwing off the calculation. Otherwise, the // incorrect indentation will be returned for lambda cases like so: // // void M() // { // Func f = () => // { // return 1; // }; // } // // Without throwing out the first line in the example above, the indentation column // used will be 4, rather than 8. var startLineFirstNonWhitespace = text.Lines[startLineNumber].GetFirstNonWhitespacePosition(); if (startLineFirstNonWhitespace.HasValue && startLineFirstNonWhitespace.Value < span.TextSpan.Start) { startLineNumber++; } for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { var line = text.Lines[lineNumber]; if (line.IsEmptyOrWhitespace()) { continue; } indentationColumn = indentationColumn.HasValue ? Math.Min(indentationColumn.Value, line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(tabSize)) : line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(tabSize); } } return indentationColumn ?? 0; } } }