Синтаксис ANTLR3 Подсветка скрытого канала в ICSharpCode.TextEditor
Я добился некоторого прогресса в разработке нашего небольшого DSL, но столкнулся с проблемой при попытке выделить комментарии в TextEditorControl, который мы используем. Между прочим, элемент управления ICSharpCode великолепен и в сочетании с ANTLR делает его отличной платформой для DSL.
У меня есть рабочий грамматик и лексер, и я написал стратегию выделения в текстовом редакторе, которая также хорошо работает. Единственный элемент DSL, который отказывается правильно окрашивать, - это "Комментарий", который я имею на скрытом канале.
Comment
: '//' ~('\r' | '\n')* {$channel=Hidden;}
| '/*' .* '*/' {$channel=Hidden;}
;
Разочарование в том, что я могу заставить подсветку работать, если я уберу лексруль Comment со скрытого канала... но когда я это сделаю, парсер прекратит синтаксический анализ во время оценки после последнего фрагмента текста, следующего за комментарием.
В качестве примера; это работает, когда комментарии скрыты, но прекращает синтаксический анализ на первом "abc", когда они не
//Comment
abc=[7,8,9];
return abc[2];
Я пытался получить доступ к скрытому каналу отдельно, чтобы я мог объединить списки по умолчанию и скрытые токены в один список, упорядоченный по стартовому индексу, а затем выделить его, но мне не повезло, используя параметр BaseRecognizer.Hidden для CommonTokenStream конструктор.
Моя текущая попытка выделить строку TextEditor выглядит следующим образом
private void MarkUsingParalexTokens(IDocument document, LineSegment line)
{
var text = document.GetText(line).ToLower();
var input = new ANTLRStringStream(text);
_lexer.CharStream = input;
_tokens = new CommonTokenStream(_lexer, BaseRecognizer.Hidden);
//_tokens.TokenSource =_lexer;
var wordStart = 0;
if (_tokens.Count > 1)
{
do
{
_tokens.Consume();
} while (_tokens.LastToken.Type != ParalexLexer.EOF);
var tokenList = _tokens.GetTokens();
var tokenEnum = tokenList.GetEnumerator();
var tokenAvailable = tokenEnum.MoveNext();
if (tokenAvailable)
{
for (var i = 0; i < text.Length; i++)
{
var token = tokenEnum.Current;
if (token != null)
{
var c = text[i];
if (c == ' ' || c == '\t')
{
if (i > wordStart)
AddWord(document, line, wordStart, i);
line.Words.Add(c == ' ' ? TextWord.Space : TextWord.Tab);
wordStart = i + 1;
}
else
{
var atStartOfToken = (i == token.StartIndex);
if (atStartOfToken)
{
if (i > wordStart)
AddWord(document, line, wordStart, i);
var tokenLength = token.StopIndex - token.StartIndex + 1;
AddWord(document, line, i, tokenLength, token);
tokenEnum.MoveNext();
wordStart = i + tokenLength;
i = wordStart - 1;
}
}
}
}
}
}
if (wordStart < line.Length)
AddWord(document, line, wordStart, line.Length);
}
void AddWord(IDocument document, LineSegment line, int startOffset, int length, IToken token = null)
{
if (length==0) return;
var hasSpecialColor = token != null;
var color = hasSpecialColor ? GetColor(token) : _highlightColors["Default"];
line.Words.Add(new TextWord(document, line, startOffset, length, color, !hasSpecialColor));
if (token != null) Debug.WriteLine("From typing: Text {0}, Type {1}, Color {2}", token.Text, token.Type, color);
}
private HighlightColor GetColor(IToken token)
{
var name = token.Type;
var groupName = "Default";
var punctuation = new[]
{6, 7, 9, 14, 15, 16, 17, 18, 22, 28, 33, 34, 47, 48, 49, 50, 51, 52, 55, 56, 57, 58, 60, 62, 65, 71};
var paralexVerbs = new[] { 8, 13, 23, 26, 27, 31, 32, 38, 39, 40, 54, 64, 68, 73, 75, 76 };
var paralexNouns = new[] {11, 12, 42, 43, 59, 66};
var paralexNumbers = new[] { 53, 61, 41 };
var paralexStrings = new[] {70};
if (Array.IndexOf(punctuation, name) >= 0)
{
groupName = "Punctuation";
}
else if (Array.IndexOf(paralexVerbs, name) >= 0)
{
groupName = "ParalexVerbs";
}
else if (Array.IndexOf(paralexNouns, name) >= 0)
{
groupName = "ParalexNouns";
}
else if (Array.IndexOf(paralexNumbers, name) >= 0)
{
groupName = "ParalexNumbers";
}
else if (Array.IndexOf(paralexStrings, name) >= 0)
{
groupName = "ParalexStrings";
}
else if (name == 19)
{
groupName = "ParalexComment";
}
return _highlightColors[groupName];
}
Похоже, что do... while необходимо для добавления токенов в список, иначе GetTokens никогда ничего не доставит. В приведенном выше коде токены не создаются даже при вводе комментариев в мой тестовый стенд.
Если я возьму вызов параметризованного конструктора для CommonTokenStream и пойду с базовым конструктором, я получу хороший поток токенов, которые я могу раскрасить, но все скрытые токены... ну... спрятаны, я думаю.
Мы будем благодарны за ваши коллективные мысли по этой небольшой проблеме, а также за ваши идеи о том, как я могу программно поддерживать списки типов, вместо того, чтобы настраивать их каждый раз, когда я меняю парсер.
Я думал создать независимые каналы для каждого типа, требующего окраски, но на данный момент я только рекурсивно добавляю к своей проблеме!
Заранее спасибо Ян
РЕДАКТИРОВАТЬ:
Спасибо за твой отличный ответ, Сэм, это очень ценится. Это отмечено и забито.
Я перешел к концепции переопределения, поскольку она также решает проблему отслеживания различных типов токенов по имени и, таким образом, упрощает мое обслуживание при добавлении в грамматику.
Я создал лексер выделения синтаксиса и отдельный лексер оценки и использовал независимые каналы, которые я создал в исходной грамматике.
Комментарий теперь выглядит так, хотя я думаю, что alt еще не работает, основной работает хорошо
Comment
: '//' ~('\r' | '\n')*
| '/*' .* '*/'
;
Участники Lexer добавили эти
@lexer::members{
public const int StringChannel = 98;
public const int NumberChannel = 97;
public const int NounChannel = 96;
public const int VerbChannel = 95;
public const int CommentChannel = 94;
}
и лексер выделения использует это переопределение в Emit() ваше выдвинутое переопределение также установлено и работает
public class HighlightLexer : ParalexLexer
{
public override IToken Emit()
{
switch (state.type)
{
case Strng:
state.channel = StringChannel;
break;
case Nmber:
case Null:
case Bool:
case Instrument:
case Price:
case PeriodType:
state.channel = NumberChannel;
break;
case BarPeriod:
case BarValue:
case InstrumentList:
case SMA:
case Identifier:
state.channel = NounChannel;
break;
case Assert:
case Do:
case Else:
case End:
case Fetch:
case For:
case If:
case In:
case Return:
case Size:
case To:
case While:
case T__77:
state.channel = VerbChannel;
break;
case Comment:
state.channel = CommentChannel;
break;
default:
state.channel = DefaultTokenChannel;
break;
}
return base.Emit();
}
}
Одна вещь, которая беспокоила меня, была неспособность аппарата легко получить список токенов. Я не мог заставить CommonTokenStream доставлять свои токены без задержек и отключений. Я решил использовать BufferedTokenStream для "_tokens", так как это больше походило на то, что я преследовал, и эй до... токенов! Я подозреваю ошибку пользователя с моей стороны?
Методы разметки теперь выглядят так
private void MarkUsingParalexTokens(IDocument document, LineSegment line)
{
var text = document.GetText(line).ToLower();
var input = new ANTLRStringStream(text);
_lexer.CharStream = input;
_tokens.TokenSource = _lexer;
var wordStart = 0;
var tokenCounter = 1;
for (var i = 0; i < text.Length; i++)
{
var token = _tokens.LT(tokenCounter);
if (token != null)
{
var c = text[i];
if (c == ' ' || c == '\t')
{
if (i > wordStart)
AddWord(document, line, wordStart, i);
line.Words.Add(c == ' ' ? TextWord.Space : TextWord.Tab);
wordStart = i + 1;
}
else
{
var atStartOfToken = (i == token.StartIndex);
if (atStartOfToken)
{
if (i > wordStart)
AddWord(document, line, wordStart, i);
var tokenLength = token.StopIndex - token.StartIndex + 1;
AddWord(document, line, i, tokenLength, token);
tokenCounter++;
wordStart = i + tokenLength;
i = wordStart - 1;
}
}
}
}
if (wordStart < line.Length)
AddWord(document, line, wordStart, line.Length);
}
void AddWord(IDocument document, LineSegment line, int startOffset, int length, IToken token = null)
{
if (length==0) return;
var hasSpecialColor = token != null;
var color = hasSpecialColor ? GetColor(token) : _highlightColors["Default"];
line.Words.Add(new TextWord(document, line, startOffset, length, color, !hasSpecialColor));
if (token != null) Debug.WriteLine("From typing: Text {0}, Type {1}, Color {2}", token.Text, token.Type, color);
}
private HighlightColor GetColor(IToken token)
{
var name = token.Channel;
var groupName = "Default";
if (name==0)
{
groupName = "Punctuation";
}
else if (name==95)
{
groupName = "ParalexVerbs";
}
else if (name==96)
{
groupName = "ParalexNouns";
}
else if (name==97)
{
groupName = "ParalexNumbers";
}
else if (name==98)
{
groupName = "ParalexStrings";
}
else if (name == 94)
{
groupName = "ParalexComment";
}
return _highlightColors[groupName];
}
В очередной раз благодарим за помощь. Я ухожу, чтобы посмотреть на распознавание ошибок и разметку... С уважением, Ян
2 ответа
Я всегда использую другой лексер для подсветки синтаксиса, чем тот, который используется для других задач анализа. Лексер, используемый для подсветки синтаксиса, всегда соответствует следующему:
Нет токена кроме
NEWLINE
содержит\r
или же\n
персонаж. Вместо этого, несколько режимов лексера используются для таких вещей, как блочные комментарии и любые другие конструкции, которые занимают несколько строк (это относится даже к лексерам ANTLR 3, но без поддержки режимов лексера в самом ANTLR 3 это быстро усложняется).NEWLINE
определяется как следующее:// ANTLR 3-based syntax highlighter: NEWLINE : ('\r' '\n'? | '\n') {skip();}; // ANTLR 4-based syntax highlighter: NEWLINE : ('\r' '\n'? | '\n') -> skip;
Нет токена на скрытом канале.
Если вы не хотите идти по этому пути, вы можете удалить действия {$channel=Hidden;}
от твоего Comment
Правило, и вместо этого получить новый класс от вашего базового лексера. В производном классе переопределить Emit()
, Используйте базовую реализацию для подсветки синтаксиса и производную реализацию для передачи парсеру. В некоторых случаях это проще, но для языков с многострочными строками или комментариями вводится существенное ограничение производительности, которое мы считаем неприемлемым для любого из наших продуктов.
public override IToken Emit()
{
if (state.type == Comment)
state.channel = Hidden;
return base.Emit();
}
Я использую C++(библиотека QT/SCintilla), но в любом случае я бы рекомендовал использовать другой Lexer для подсветки синтаксиса. Подсветка мин лексера отличается от разбора:
нет необходимости в контекстно-зависимом лексинге ( "X" является ключевым словом, если оно есть, только если за ним следует "Y", в противном случае это идентификатор)
лексер выделения никогда не должен потерпеть неудачу
Я хочу, чтобы встроенные функции были выделены (это не нужно для разбора)
Грамматика Gui Lexer содержит дополнительные правила (в конце).
QUOTED_STRING_FRAGMENT
: '"' (~('"') | '\"')+ EOF
;
// Last resort rule matches any character. This lexer should never fail.
TOKEN_FAILURE : . ;
Правило TOKEN_FAILURE будет соответствовать любому "недопустимому" символу из пользовательского ввода и будет отображаться с красным фоном. В противном случае этот символ будет пропущен, а выделение будет смещено.
QUOTED_STRING_FRAGMENT обрабатывает ситуацию, когда пользователь вводит первую кавычку и строка еще не закончена.