// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php
using System;
using System.Linq;
using Nova.Parsing;
using Nova.Rendering;
using Nova.Utilities;
namespace Nova.CodeDOM
{
/// <summary>
/// Represents user comments, and may either be independent or associated with another <see cref="CodeObject"/>.
/// </summary>
/// <remarks>
/// A comment can be flagged as EOL, Block, TODO, HACK, NOTE, or combinations of these.
/// By default, comments use the '//' style, unless flagged as Block, in which case they
/// use the '/*...*/' style.
/// Regular comments appear before the object they belong to, and are usually on separate
/// lines, although they can sometimes be on the same line, in which case they are forcibly
/// rendered in the Block style whether flagged as such or not.
/// Comments flagged as EOL appear after the object on the same line, and are usually the
/// last thing on the line, although they can sometimes be followed by other objects, in
/// which case they are forcibly rendered in the Block style whether flagged as such or not.
/// Comments can also have a "Post" format, which means they appear after the object. EOL
/// comments are usually Post, but can be non-post in some cases, such as when they appear
/// after the opening symbol ('{') of a Block.
/// A code object can have a regular comment, an EOL comment, or both. Technically,
/// multiple comments of each type are supported, but having more than one of the same type,
/// or a regular comment in addition to a documentation comment, is very rare and not
/// recommended - it gets messy in the UI, and helper properties are provided for each
/// comment type which only return the first comment of that type found.
/// A regular comment can have multiple lines of text whether Block type or not, but EOL
/// comments shouldn't - newlines will be converted to ';' on display if present.
/// </remarks>
public class Comment : CommentBase
{
#region /* STATIC FIELDS */
/// <summary>
/// Determines if special comments are listed.
/// </summary>
public static bool ListSpecialComments = true;
#endregion
#region /* FIELDS */
protected string _text;
protected CommentFlags _commentFlags;
#endregion
#region /* CONSTRUCTORS */
/// <summary>
/// Create a <see cref="Comment"/> with the specified text content.
/// </summary>
public Comment(string text, CommentFlags flags)
{
Text = text;
_commentFlags = flags;
// Default to same line for EOL comments, and separate line for regular comments
SetNewLines(flags.HasFlag(CommentFlags.EOL) ? 0 : 1);
}
/// <summary>
/// Create a <see cref="Comment"/> with the specified text content.
/// </summary>
public Comment(string text)
: this(text, CommentFlags.None)
{ }
#endregion
#region /* STATIC CONSTRUCTOR */
static Comment()
{
// Force a reference to CodeObject to trigger the loading of any config file if it hasn't been done yet
ForceReference();
}
#endregion
#region /* PROPERTIES */
/// <summary>
/// The text content of the comment.
/// </summary>
public override string Text
{
get { return _text; }
set
{
_text = value.Replace("\r\n", "\n"); // Normalize newlines
SetSpecialFlags();
}
}
/// <summary>
/// True if the comment appears at the end-of-line (EOL).
/// </summary>
public override bool IsEOL
{
get { return _commentFlags.HasFlag(CommentFlags.EOL); }
set { SetCommentFlag(CommentFlags.EOL, value); }
}
/// <summary>
/// Determines if the comment has a block style.
/// </summary>
public bool IsBlock
{
get { return _commentFlags.HasFlag(CommentFlags.Block); }
set { SetCommentFlag(CommentFlags.Block, value); }
}
/// <summary>
/// Determines if the comment has a raw format.
/// </summary>
public bool IsRawFormat
{
get { return _commentFlags.HasFlag(CommentFlags.Raw); }
set { SetCommentFlag(CommentFlags.Raw, value); }
}
/// <summary>
/// Determines if the comment is a 'TODO' comment.
/// </summary>
public bool IsTODO
{
get { return _commentFlags.HasFlag(CommentFlags.TODO); }
set { SetCommentFlag(CommentFlags.TODO, value); }
}
/// <summary>
/// Determines if the comment is a 'HACK' comment.
/// </summary>
public bool IsHack
{
get { return _commentFlags.HasFlag(CommentFlags.HACK); }
set { SetCommentFlag(CommentFlags.HACK, value); }
}
/// <summary>
/// Determines if the comment is a 'NOTE' comment.
/// </summary>
public bool IsNote
{
get { return _commentFlags.HasFlag(CommentFlags.NOTE); }
set { SetCommentFlag(CommentFlags.NOTE, value); }
}
/// <summary>
/// True if the annotation should be listed at the <see cref="CodeUnit"/> and <see cref="Solution"/> levels (for display in an output window).
/// </summary>
public override bool IsListed
{
get { return (ListSpecialComments && (_commentFlags.HasFlag(CommentFlags.TODO) || _commentFlags.HasFlag(CommentFlags.HACK))); }
}
/// <summary>
/// The comment flags.
/// </summary>
public CommentFlags CommentFlags
{
get { return _commentFlags; }
}
#endregion
#region /* METHODS */
protected internal void SetSpecialFlags()
{
_commentFlags |= CheckForSpecialComment(_text);
}
/// <summary>
/// Check if text represents a "special" comment.
/// </summary>
/// <returns>CommentFlag for special comment type, or None.</returns>
protected internal CommentFlags CheckForSpecialComment(string comment)
{
if (ContainsSpecial(comment, "TODO"))
return CommentFlags.TODO;
if (ContainsSpecial(comment, "HACK"))
return CommentFlags.HACK;
if (ContainsSpecial(comment, "NOTE"))
return CommentFlags.NOTE;
return CommentFlags.None;
}
protected bool ContainsSpecial(string text, string special)
{
// Match special comment indicators by finding a case-insensitive match
int index = text.IndexOf(special, StringComparison.CurrentCultureIgnoreCase);
if (index >= 0)
{
if (index >= 1)
{
// Ignore if there is an immediately preceeding alpha char, or a '/' (nested in another comment) or '"' ignoring spaces
char preceeding = text[index - 1];
if (char.IsLetter(preceeding))
return false;
int preIndex = index - 1;
while (preceeding == ' ' && preIndex > 0)
preceeding = text[--preIndex];
if (preceeding == '/' || preceeding == '"')
return false;
}
// Ignore if there is a trailing alpha char
int end = index + special.Length;
if (end < text.Length && char.IsLetter(text, end))
return false;
return true;
}
return false;
}
protected internal void SetCommentFlag(CommentFlags flag, bool value)
{
if (value)
_commentFlags |= flag;
else
_commentFlags &= ~flag;
}
#endregion
#region /* PARSING */
/// <summary>
/// The token used to parse the code object.
/// </summary>
public const string ParseToken = "//";
/// <summary>
/// The block-comment start token.
/// </summary>
public const string ParseTokenBlockStart = "/*";
/// <summary>
/// The block-comment end token.
/// </summary>
public const string ParseTokenBlockEnd = "*/";
// NOTE: No parse-points are installed for comments - instead, the parser calls the
// parsing constructor directly based upon the token type. This is because we
// want to treat entire comments as individual tokens to preserve whitespace.
/// <summary>
/// Parse a <see cref="Comment"/>.
/// </summary>
public Comment(Parser parser, CodeObject parent)
: base(parser, parent)
{
Token token = parser.Token;
_prefixSpaceCount = (token.LeadingWhitespace.Length < byte.MaxValue ? (byte)token.LeadingWhitespace.Length : byte.MaxValue);
string text = token.Text;
int start;
bool noSpaceAfterDelimiter;
// Parse a standard '//' comment
if (text.StartsWith(ParseToken))
{
// Keep track of whether or not all '//' are followed by a space
start = ParseToken.Length;
noSpaceAfterDelimiter = (start < text.Length && text[start] != ' ');
_text = text.Substring(start).TrimEnd();
if (!IsFirstOnLine)
{
// The comment occurred after another token on the same line - it's an EOL comment
_commentFlags |= CommentFlags.EOL;
parser.NextToken(true);
}
else
{
// The comment was on a line by itself - combine with following lines if they're also comments,
// and they're compatible - not Doc or block comments, same # of prefix spaces, no special prefix.
while (true)
{
int nextLine = token.LineNumber + 1;
token = parser.NextToken(true);
if ((token != null) && token.IsComment && token.Text.StartsWith(ParseToken)
&& (token.LineNumber == nextLine) && (token.LeadingWhitespace.Length == _prefixSpaceCount)
&& (string.IsNullOrEmpty(_text) || CheckForSpecialComment(token.Text.Substring(ParseToken.Length)) == CommentFlags.None))
{
text = parser.TokenText;
start = ParseToken.Length;
if (start < text.Length && text[start] != ' ')
noSpaceAfterDelimiter = true;
_text += "\n" + text.Substring(start).TrimEnd();
}
else
break;
}
// If the comment was left-justified, set the format flag as such so that it will be displayed
// at the left margin regardless of the current level of code indentation.
if (_prefixSpaceCount == 0)
_formatFlags |= FormatFlags.NoIndentation;
}
}
else
{
// Parse a '/* ... */' block comment
IsBlock = true;
noSpaceAfterDelimiter = false;
bool formatted = true;
// The parser will pass through everything between the delimiters, so we have to deal with
// that here by breaking it up into lines, stripping leading spaces and asterisks, and then
// putting them back together.
string[] lines = text.Split('\n');
for (int i = 0; i < lines.Length; ++i)
{
string line = lines[i];
bool hasBlockEnd = (i == lines.Length - 1 && line.EndsWith(ParseTokenBlockEnd));
if (i == 0)
start = ParseTokenBlockStart.Length;
else
{
// Skip leading spaces on subsequent lines up to the indent of the first line
start = Math.Min(_prefixSpaceCount, StringUtil.CharCount(line, ' ', 0));
// Special format handling: Skip a leading "*"
if (start < line.Length && line[start] == '*' && !(hasBlockEnd && start == line.Length - 2))
++start;
else if (start < line.Length - 1)
{
// Special format handling: Skip " *" or "\" if followed by '*' and on the last line (if not part of the ending "*/"),
// or "//" (and NOT "///"), or " " (if followed by a non-space).
if ((line[start] == ' ' && line[start + 1] == '*') || (line[start] == '\\' && line[start + 1] == '*' && i == lines.Length - 1))
{
if (!(hasBlockEnd && start == line.Length - 3))
start += 2;
}
else if ((line[start] == '/' && line[start + 1] == '/' && (start == line.Length - 2 || line[start + 2] != '/'))
|| (line[start] == ' ' && line[start + 1] == ' ' && (start == line.Length - 2 || line[start + 2] != ' ')))
{
start += 2;
formatted = false;
}
else
formatted = false;
}
}
if (start < line.Length && line[start] != ' ')
noSpaceAfterDelimiter = true;
int length = line.Length - start;
if (hasBlockEnd)
length -= ParseTokenBlockEnd.Length;
_text += (i == 0 ? "" : "\n") + line.Substring(start, length).TrimEnd();
}
if (!formatted)
_commentFlags |= CommentFlags.Raw;
Token nextToken = parser.NextToken(true);
// If the comment occurred after another token on the same line AND it's a single line block
// comment AND it's the last token on the line - it's an inline EOL comment.
if (!IsFirstOnLine && lines.Length == 1 && nextToken.IsFirstOnLine)
_commentFlags |= CommentFlags.EOL;
}
// If any line is missing a space after the delimiter, then set the special flag indicating that no
// spaces should be displayed. Otherwise, remove one column of spaces and add it back when displayed.
if (noSpaceAfterDelimiter)
NoSpaceAfterDelimiter = true;
else
RemoveSpaces(1);
SetSpecialFlags();
}
/// <summary>
/// Remove the specified number of prefixed spaces from each line of the comment.
/// Fails if any non-blank lines don't start with at least the specified number of spaces.
/// </summary>
public bool RemoveSpaces(int spaces)
{
string[] lines = _text.Split('\n');
if (Enumerable.Any(lines, delegate(string line) { return StringUtil.CharCount(line, ' ', 0) < spaces && line.Length > 0; }))
return false;
_text = null;
foreach (string line in lines)
_text += (_text == null ? "" : "\n") + (string.IsNullOrEmpty(line) ? "" : line.Remove(0, spaces));
return true;
}
#endregion
#region /* FORMATTING */
/// <summary>
/// True if there is no space between the comment delimiter and the comment text.
/// </summary>
public bool NoSpaceAfterDelimiter
{
get { return _annotationFlags.HasFlag(AnnotationFlags.NoSpace); }
set { SetAnnotationFlag(AnnotationFlags.NoSpace, value); }
}
/// <summary>
/// Determines if the code object only requires a single line for display.
/// </summary>
public override bool IsSingleLine
{
get { return (base.IsSingleLine && (IsEOL || (_text == null || _text.IndexOf('\n') < 0))); }
set
{
base.IsSingleLine = value;
if (value && _text != null)
_text = _text.Trim().Replace("\n", "; ");
}
}
#endregion
#region /* RENDERING */
public override void AsText(CodeWriter writer, RenderFlags flags)
{
bool isEOL = IsEOL;
bool isInline = flags.HasFlag(RenderFlags.CommentsInline);
bool isBlock = (IsBlock || isInline);
bool isRawFormat = IsRawFormat;
int newLines = NewLines;
bool isPrefix = flags.HasFlag(RenderFlags.IsPrefix);
if (!isPrefix)
{
if (newLines > 0)
{
if (!flags.HasFlag(RenderFlags.SuppressNewLine))
writer.WriteLines(newLines);
}
else if (!IsInfix || isEOL)
{
// Handle a special case of an EOL comment on an 'else' that is actually commented-out
// code for an 'if' (common practice to document an implied 'if' condition).
bool elseIf = (isEOL && Parent is Else && _text.StartsWith("if ("));
writer.Write(isBlock || elseIf ? " " : " ");
}
}
if (_text == null)
UpdateLineCol(writer, flags);
else
{
string space = (NoSpaceAfterDelimiter ? "" : " ");
writer.EscapeUnicode = false;
if (isEOL || isInline)
{
// Render a single-line EOL or in-line block comment.
// There shouldn't be any newlines in the comment, but handle them just in case.
UpdateLineCol(writer, flags);
string comment = (isBlock ? ParseTokenBlockStart : ParseToken) + space
+ _text.Replace("\n", "; ") + (isBlock ? ((_text.EndsWith("*") ? "" : space) + ParseTokenBlockEnd) : "");
writer.Write(comment);
}
else
{
// Render a non-EOL (one or more lines) comment or block comment
if (HasNoIndentation)
writer.BeginOutdentOnNewLine(this, 0);
UpdateLineCol(writer, flags);
string[] lines = _text.Split('\n');
bool lastIsEmpty = true;
for (int i = 0; i < lines.Length; ++i)
{
string line = lines[i];
bool isEmptyLine = string.IsNullOrEmpty(line);
string prefixSpace = (isEmptyLine ? "" : space);
if (i > 0)
writer.WriteLine();
// Write comment prefix as appropriate
if (isBlock)
{
lastIsEmpty = ((i == lines.Length - 1) && isEmptyLine);
writer.Write(i == 0 ? ParseTokenBlockStart + prefixSpace : (isRawFormat ? "" : (lastIsEmpty ? " " : " *" + prefixSpace)));
if (lastIsEmpty)
break;
}
else
writer.Write(ParseToken + prefixSpace);
writer.Write(line);
}
if (isBlock)
{
space = ((isRawFormat || lastIsEmpty || _text.EndsWith("*")) ? "" : space);
writer.Write(space + ParseTokenBlockEnd);
}
if (HasNoIndentation)
writer.EndIndentation(this);
}
writer.EscapeUnicode = true;
}
if (isPrefix)
{
// If this object is rendered as a child prefix object of another, then any whitespace is
// rendered here *after* the object instead of before it.
if (newLines > 0)
writer.WriteLines(newLines);
else
writer.Write(" ");
}
}
/// <summary>
/// Get any <see cref="CommentFlags"/> and <see cref="AnnotationFlags"/> as a comma separated string.
/// </summary>
public string FlagsAsText()
{
string result = "";
if (_commentFlags != CommentFlags.None)
result += _commentFlags;
if (_annotationFlags != AnnotationFlags.None || string.IsNullOrEmpty(result))
result = StringUtil.Append(result, ", ", _annotationFlags.ToString());
return result;
}
#endregion
}
#region /* COMMENT TYPE FLAGS */
/// <summary>
/// Comment type flags.
/// </summary>
[Flags]
public enum CommentFlags
{
/// <summary>No flags.</summary>
None = 0x00,
/// <summary>The comment is an "end-of-line" comment - the last item on the current line.</summary>
EOL = 0x01,
/// <summary>The comment is "block" style.</summary>
Block = 0x02,
/// <summary>The comment isn't formatted - for block style, this means no preceeding asterisks on each line.</summary>
Raw = 0x04,
/// <summary>The comment is a 'to-do' comment.</summary>
TODO = 0x10,
/// <summary>The comment documents a 'hack' in the code.</summary>
HACK = 0x20,
/// <summary>The comment represents a special note.</summary>
NOTE = 0x40
}
#endregion
}