понедельник, 23 марта 2015 г.

Простой Syntax Highlighter на базе Roslyn

Roslyn – очень классная штука, с помощью которой можно не только анализаторы делать, но и много других интересных штук.

Вот, например, сколько потребуется усилий, чтобы сделать раскрашиватель синтаксиса, который будет печатать C#-файл в консоль и подсвечивать ключевые слова, строковые литералы и т.п вещи?

Вот как это можно сделать.

Подсветка синтаксиса состоит из нескольких этапов. Вначале нужно прочитать C#-файл и получить синтаксическое дерево. Потом нужно проанализировать дерево и получить «классификацию» символов – являются ли элементы дерева ключевыми словами, идентификаторами, строковыми литералами и т.п. Потом нужно пробежаться по исходному коду и получить цвет символа, в зависимости от классификации, и установить нужный цвет консоли.

1. Получение документа и синтаксического дерева

Существует несколько способов получить синтаксическое дерево из строки. Самый простой способ, вызвать метод CSharpSyntaxTree.ParseText. В этот раз этот подход не сработает, поскольку помимо синтаксического дерева классификатору (классу Classifier) нужно получить еще и Workspace и Document. Поэтому используем другой способ:

private static Document CreateDocumentFrom(string sourceCode)
{
   
var workspace = new AdhocWorkspace
();
   
Solution solution = workspace.
CurrentSolution;
   
Project project = solution.AddProject("SyntaxHighlighter", "SyntaxHighlighter",
LanguageNames.
CSharp);
   
Document document = project.AddDocument("source.cs"
, sourceCode);
   
return document;
}
2. Классифицировать дерево с помощью класса Classifier

Метод Classifier.GetClassifiedSpansAsync возвращает список ClassifiedSpan – список простых объектов значений (value objects) с парой свойств: TextSpan и ClassificationType, которые определяют диапазон в исходном коде и тип символа, такие как StringLiteral, Identifier etc.

public static async Task PrintSourceAsync(string sourceCode)
{
   
// Получаем документ по исходному коду
    Document document =
CreateDocumentFrom(sourceCode);
   
var syntaxTreeRoot = await document.
GetSyntaxRootAsync();

   
// Getting classification for the document
    IEnumerable<ClassifiedSpan> classification =
        await Classifier.GetClassifiedSpansAsync(document, syntaxTreeRoot.
FullSpan);

   
// Получаем мапу между началом нового символа и цветом
    IDictionary<int, ConsoleColor> positionColorMap =
        classification.ToDictionary(
c
=> c.TextSpan.Start, c => GetColorFor(c.ClassificationType));
3. Вывести на консоль содержимое файла

Вывод на консоль достаточно прост. Перебираем все элементы, и если текущий символ относится к новому символу, то изменяем цвет консоли:

    Console.BackgroundColor = ConsoleColor.Black;

   
// Iterating over each character in source file and printing it
    for (int charPosition = 0; charPosition < sourceCode.Length; charPosition++
)
    {
       
// Проверяем, нужно ли изменять цвет консоли
        ConsoleColor
newColor;
       
if (positionColorMap.TryGetValue(charPosition, out
newColor))
        {
           
Console.ForegroundColor =
newColor;
        }

       
Console.
Write(sourceCode[charPosition]);
    }

   
Console.
ResetColor();
}
// Конец метода PrintSourceAsync

Нужно еще метод, который будет получать цвет в зависимости от классификации символа и вывести текст на консоль. Для получения цвета подойдет простой switch:

private static ConsoleColor GetColorFor(string classificatioName)
{
   
switch
(classificatioName)
    {
       
case ClassificationTypeNames.
InterfaceName:
       
case ClassificationTypeNames.
EnumName:
       
case ClassificationTypeNames.
Keyword:
           
return ConsoleColor.
DarkCyan;

       
case ClassificationTypeNames.
ClassName:
       
case ClassificationTypeNames.
StructName:
           
return ConsoleColor.
DarkYellow;

       
case ClassificationTypeNames.
Identifier:
           
return ConsoleColor.
DarkGray;

       
case ClassificationTypeNames.
Comment:
           
return ConsoleColor.
DarkGreen;

       
case ClassificationTypeNames.
StringLiteral:
       
case ClassificationTypeNames.
VerbatimStringLiteral:
           
return ConsoleColor.
DarkRed;

       
case ClassificationTypeNames.
Punctuation:
           
return ConsoleColor.
Gray;

       
case ClassificationTypeNames.
WhiteSpace:
           
return ConsoleColor.
Black;

       
case ClassificationTypeNames.
NumericLiteral:
           
return ConsoleColor.
DarkYellow;

       
case ClassificationTypeNames.
PreprocessorKeyword:
           
return ConsoleColor.
DarkMagenta;
       
case ClassificationTypeNames.
PreprocessorText:
           
return ConsoleColor.
DarkGreen;

       
default
:
           
return ConsoleColor.Gray;
    }

}

Все, метод PrintSourceAsync, и теперь его можно использовать для вывода C# кода на консоль с расцветкой синтаксиса. Вот пример запуска:

clip_image002

Подсветка синтаксиса далека от идеала, да и класс Classifier дает далеко не все типы узлов. Но расширить решение не так и сложно. В любом случае, впечатляет, что в 100 строках кода умещается очень даже толковый syntax highlighter.

З.Ы. Все исходники на гитхабе в репозитории PlayingWithRoslyn.

12 комментариев:

  1. Круто :) А не подскажешь случайно подноценный скрипт-едит-контрол на рослине, чтобы была подсветка синтаксиса, автокомплит и поддержка ссылок на сборки (references)? А то много где пишут что с помощью Roslyn такое должно быть достаточно легко реализуемо, но что-то из готового находятся одни поделки... Т.е. ищется что-то типа AvalonEdit (http://avalonedit.net/) из SharpDevelop, но на базе рослина. Не встречалось?

    ОтветитьУдалить
    Ответы
    1. Поскольку сейчас в репе розлина стало больше всего, может быть там что-то такое есть. Навскидку - не подскажу:((

      Удалить
    2. Рекомендую посмотреть в сторону FastColoredTextBox (
      http://www.codeproject.com/Articles/161871/Fast-Colored-TextBox-for-syntax-highlighting). Очень, очень мощный компонент. На гитхабе вместе с исходниками есть и демка, поражающая разнообразием возможностей.

      ПС. Пардон за некропостинг )

      Удалить
    3. Правда он не на рослине.

      Удалить
  2. Привет, небольшой оффтоп: а что вы используете для подсветки синтаксиса сниппетов кода в блоге (кажется, уже упоминали где-то, но не получается найти)?

    ОтветитьУдалить
    Ответы
    1. Я использую Windows Live Writer (который, кстати, недавно заоупенсорсили) и прото копирую из студии (но для этого в студии должен быть установел Productivity Power Tools и включен Copy AsHTML компонент).

      Удалить
  3. Класс, стал постоянным читателем вашего блога. Также купил вашу книгу про патерны проектирования. Вобщем хочу сказать вам спасбо Сергей.

    ОтветитьУдалить
  4. Класс, стал постоянным читателем вашего блога. Также купил вашу книгу про патерны проектирования. Вобщем хочу сказать вам спасбо Сергей.

    ОтветитьУдалить
    Ответы
    1. Арсений, я рад новым читателям! Спасибо за отзывы!

      Удалить