SynEdit Highlighter/ru

From Lazarus wiki
Jump to navigationJump to search

Template:MenuTranslate

Для получения дополнительной информации о SynEdit перейдите к: SynEdit


Прим.перев.: дабы далее не загромождать текст, под термином Highlighter будет подразумеваться механизм(маркер) подсветки синтаксиса.



Понимание SynEdit Highlighter

Взаимоотношения SynEdit - Highlighter

SynEdit - Highlighter имеют взаимоотношение N к 1.

  • Один экземпляр Highlighter может обслуживать N (много) экземпляров SynEdits
  • Каждый SynEdit имеет только один Highlighter
  • Но: один текст (текстовый буфер) может иметь много маркеров подсветки синтаксиса, если он используется несколькими SynEdit (каждый SynEdit будет иметь один HL, но все HL будут работать с одним и тем же документом)

В результате:

  • Экземпляр Highlighter не имеет (фиксированной) ссылки на SynEdit.
(Однако Highlighter'ы хранят список SynEditTextBuffers, к которому они прикреплены)
  • Все данные для Highlighter (должны быть) сохранены в SynEdit (фактически в TextBuffer SynEdit (называемом «Линии»).

Однако перед каждым вызовом Highlighter SynEdit гарантирует, что для Highlighter.CurrentLines будут установлены текущие строки SynEdits. Таким образом, маркер может получить доступ к данным в любое время. Формат хранения данных определяется маркером (TSynCustomHighlighter.AttachToLines).

Сканирование и возврат атрибутов подсветки

Ожидается, что Highlighter будет работать на основе каждой строки.

Если какой-либо текст был изменен, SynEdit будет вызывать (TSynCustomHighlighter.ScanFrom / в настоящее время вызывается из TSynEdit.ScanFrom) с диапазоном строк. Highlighter должен знать состояние предыдущей строки.

Если требуются атрибуты подсветки, SynEdit будет запрашивать их также для каждой строки. SynEdit будет проходить через отдельные токены на линии. В настоящее время это происходит из вложенных процедур PaintLines в SynEdit.PaintTextLines. Он вызывает TSynCustomHighlighter.StartAtLineIndex, за которым следует HL.GetTokenEx/HL.GetTokenAttribute до тех пор, пока HL.GetEol имеет значение false.

Кроме того, базовый класс для данных Highlighter'а (см. AttachToLines) основан на хранении данных на каждой строке, а TextBuffer (строки) SynEdit выполняет обслуживание этих данных для их синхронизации. То есть: всякий раз, когда строки текста вставляются или удаляются, записи также вставляются или удаляются из данных highlighter'ов (следовательно, в каждой строке должна быть одна запись).

Обычно Highlighter'ы сохраняют статус конца строки в этом поле. Поэтому, если Highlighter будет работать со строчкой, он продолжит ввод состояния из предыдущей строки.

Folding (сворачивание текста)

Событие фолдинга(схлопывания/сворачивание текста) SynEdit обрабатывается модулями SynEditFoldedView и SynGutterCodeFolding. Highlighter'ы, которые реализуют сворачивание, должны основываться на TSynCustomFoldHighlighter.

Базовая информация для связи между SynEditFoldedView и HL требует 2 значения, сохраненных для каждой строчки (конечно, сам highlighter может хранить больше информации):

  • FoldLevel в конце строки
  • Минимальный FoldLevel, встречающийся где-либо в строке

Foldlevel указывает, сколько (вложенных) уровней схлопывания/сворачивания текста существует. Он повышается всякий раз, когда уровень сворачивания начинается, и понижается, когда уровень сворачивания заканчивается:

                            EndLvl   MinLvl
  Procedure a;               1 -      0
  Begin                      2 --     1 -
    b:= 1;                   2 --     2 --
    if c > b then begin      3 ---    2 --
      c:=b;                  3 ---    3 ---
    end else begin           3 ---    2 --
      b:=c;                  3 ---    3 ---
    end;                     2 --     2 --
  end;                       0        0  // Оператор end закрывает оба уровня сворачивания текста: операторов                                                       
                                         //begin и procedure

На строке

Procedure a;               1 -      0

MinLvl равен 0, потому что строка началась с уровня 0 (и она никогда не спускалась / не закрывалась). Аналогично во всех строках, где есть только ключевое слово, открывающее уровень сворачивания текста ("begin").


А строка

    end else begin           3 ---    2 --

начинается с уровня 3, а также заканчивается им (один закрытый, один открытый). Но так как она спускается первой, минимальный уровень, встречающийся где-либо в строке, равен 2.


Без MinLvl было бы невозможно сказать, что уровень сворачивания текста заканчивается на этой строке.

MaxLvl не существует, потому что уровни сворачивания текста, начинающиеся и заканчивающиеся на одной и той же линии, не могут быть свернуты в любом случае. Не нужно их обнаруживать.

  if a then begin b:=1; c:=2; end; // нет уровня сворачивания текста на этой строке

Создание SynEdit Highlighter

Начиная с версии 0.9.31 rev.35115, сворачивание текста в highlighter'е изменилось. Реализовать базовое свертывание текста теперь проще.

Все исходники можно найти в каталоге установки Lazarus в: examples\SynEdit\NewHighlighterTutorial\

Проект HighlighterTutorial содержит 3 различных примера highlighter'ов:

  • SimpleHl: используется в шаге 1 ниже
  • ContextHl: используется в шаге 2 ниже
  • FoldHl: используется в шаге 3 ниже

SimpleHl и ContextHl тоже будут работать с v.0.9.30 Лазаруса

Сворачивание текста в highlighter'е в действии

SynEditFoldingHighlighterDemo.png

Основы: возврат токенов и атрибутов

Как указано, модуль SimpleHl демонстрирует этот процесс.

Что он делает

  • Он разбивает каждую строку на слова и пробелы (или табуляции)
    • Пробелы являются частью текста и также должны быть подсвечены
  • Этот пример позволяет указать разные цвета для
- текста (по умолчанию не подсвечен)
- пробелов (по умолчанию в серебристой рамке)
- слов, разделенных пробелами, начинающихся с a,e,i,o,u (по умолчанию выделены жирным шрифтом)
- "не" слов (по умолчанию на красном фоне)

Как это работает

  • Creation

Highlighter создает Атрибуты, которые могут вернуть Words(слова) и Spaces(пробелы).

  • SetLine

Событие вызывается SynEdit до того, как будет прорисована строка (или до того, как потребуется информация о выделении)

  • GetTokenEx, GetTokenAttribute, Next, GetEol

События используются SynEdit для перебора строки. Обратите внимание, что первый токен (слово или пробел) должен быть готов после SetLine, без вызова Next.

Light bulb  Примечание: токены, возвращаемые для каждой строки, должны представлять исходный текст строки и возвращаться в правильном порядке.
  • GetToken, GetTokenPos, GetTokenKind

SynEdit использует их, например, для поиска подходящих скобок. Если tokenKind возвращает разные значения для каждого атрибута, то скобки совпадают только в том случае, если они имеют одинаковый вид (например, если был строковый атрибут, скобки вне строки не будут совпадать с скобками внутри строки).

Другие заметки

Для удобства чтения highlighter не имеет оптимизации, поэтому он может быть очень медленным для больших текстов. Многие из поставляемых highlighter'ов используют хеш-функции, чтобы найти слово (или любую группу символов).

Шаг 2: Использование диапазонов

Как указано, модуль ContextHl демонстрирует этот процесс.

Следующий пример допускает влияние содержимого строки на последующие строки. Например: "(*" в Паскале делает все последующие строки комментарием, пока не будет найден "*)".

Этот пример расширяет показанный выше SimpleHl: Токены -- и ++ (должны быть обрамлены пробелом или началом/концом строки, чтобы стать их собственным токеном) будут переключать слова, начинающиеся с a,e,i,o,u

Несколько ++ и -- могут быть вложенными. Тогда для каждого слова должны быть заданы -- и ++, прежде чем оно снова будет подсвечено.

Then we extend the scanner. The pre-scan to store the information calls the same functions as the highlighter. It is automatically called, if anything changes. (It is called for all lines below the changed line, until a line returns the same Range-value as it already had) Затем мы расширяем сканер. Предварительное сканирование для хранения информации вызывает те же функции, что и highlighter. Он автоматически вызывается, если что-то меняется (он вызывается для всех строк ниже измененной строки, пока строка не вернет то же значение Range, что и раньше).

Текущее количество "--" рассчитывается в

  FCurRange: Integer;

Количество уменьшается посредством "++"


Для хранения информации мы используем:

GetRange
Вызывается после полного сканирования строки, чтобы получить значение в конце строки. Значение будет сохранено.
SetRange
Вызывается до сканирования линии. Устанавливает значение, хранящееся в конце предыдущей строки.
ResetRange
Вызывается до сканирования 1-й строки (так как предыдущей строки нет).
Light bulb  Примечание: Сканирование запускается при *каждом* изменении строки (при каждом нажатии клавиши). Он сканирует текущую строку и все строки ниже, пока строка не вернет тот же диапазон, который у нее уже был. См.: http://forum.lazarus.freepascal.org/index.php/topic,21727.msg139420.html#msg139420

Важное замечание о диапазонах

Назначение диапазона - позволить Highlighter начать сканирование в любой строке. Highlighter никогда не понадобится смотреть на предыдущую строку. Любая информация, необходимая для сканирования текущей строки, может быть получена из значения диапазонов.

Пример:

  writeln; (*
  readln;
  *)

при сканировании "readln" Highlighter знает из диапазона, который находится в комментарии, ему не нужно осматривать предыдущие строки.

Поэтому сканирование может начинаться с любой строки.

Это также объясняет примечание в предыдущей главе: "пока строка не вернет тот же диапазон, который у нее уже был". Ибо, даже если текст строки не был изменен, если значение диапазона в начале строк изменилось, то результат сканирования также изменится.

Шаг 3: Добавление маркеров сворачивания текста

Как указано выше, модуль FoldHl демонстрирует этот процесс.

Например, highlighter должен сворачивать весь текст между отдельно стоящими "-(-", "-)-".

Изменим наследование:

  uses SynEditHighlighterFoldBase;
  ...
  TSynDemoHl = class(TSynCustomFoldHighlighter)

Изменим способ хранения информации о диапазоне, поскольку базовый класс использует ее для информации о схлопывании текста:

procedure TSynDemoHl.SetRange(Value: Pointer);
begin
  inherited;
  FCurRange := PtrInt(CodeFoldRange.RangeType);
end;

procedure TSynDemoHl.ResetRange;
begin
  inherited;
  FCurRange := 0;
end;

function TSynDemoHl.GetRange: Pointer;
begin
  CodeFoldRange.RangeType := Pointer(PtrInt(FCurRange));
  inherited;
end;

Теперь добавим код в сканер, который сообщает highlighter'у об открытии и закрытии маркеров схлопывания текста:

procedure TSynDemoHl.FindTokenEnd;
begin
   ...

  if (FTokenEnd = FTokenPos+1) and (FLineText[FTokenPos] = '[') then
    StartCodeFoldBlock(nil);
  if (FTokenEnd = FTokenPos+1) and (FLineText[FTokenPos] = ']') then
    EndCodeFoldBlock();
end;
  • Для 0.9.30

Пожалуйста, смотрите историю этой страницы, если вы используете версию 0.9.30 [[1]]

More info on StartCodeFoldBlock / EndCodeFoldBlock

  function StartCodeFoldBlock(ABlockType: Pointer; IncreaseLevel: Boolean = true): TSynCustomCodeFoldBlock; virtual;
ABlockType
Can be used to specify an ID for the block.

The field is not normally used as a pointer (though this is permitted). Normally IDs are a numeric enumeration.
If you have different types of block (e.g. in Pascal: begin/end; repeat/until, ...), and you do not want a block being closed by the wrong keyword ("end" should not close "repeat"), then you can give them IDs:
StartCodeFoldBlock(PtrUInt(1)) // or other numbers

IncreaseLevel
If set to False, then a block will be inserted that can not be folded.
Blocks like that can be used for internal tracking.

NOTE: All folds must be nested, they can not overlap. That is, the last opened fold must be closed first.
This refers to the "EndLvl" as shown in "Folding"(1.3) above
Overlaps (like IFDEF and begin in the IDE) can not be done this way.

  procedure EndCodeFoldBlock(DecreaseLevel: Boolean = True); virtual;
DecreaseLevel
This *must* match IncreaseLevel, as it was given on the StartCodeFoldBlock

True means a fold ends; False means an internal block is ended. If mismatched then folds will either continue, or end early.
TopCodeFoldBlockType can be used to indicate the ID of the innermost open block. One can use different IDs for internal blocks, and use this to set the value.

  function TopCodeFoldBlockType(DownIndex: Integer = 0): Pointer;

Returns the ID of the innermost block.

DownIndex
can be used to get ID for the other block.

DownIndex=1 means the block that is surrounding the innermost.

Configurable Highlighters (incl. 3rd party)

SynAnySyn

A real simple Highlighter. More a reference implementation, than a real life useable Highlighter.

SynFacilSyn

Flexible fully configurable Highlighter. https://github.com/t-edson/SynFacilSyn

References

Threads on the forum:

  • Folding
    • "SynEdit - improved highlighters to handle Code Folding?" [topic,7879]
    • "SynEdit - Add support code folding for Java" [topic,7338]
    • "CodeFolding" Config [topic,11064]
    • Fold blocks "end" keyword (end on the line before the next keyword) [topic,23411.msg139621]
    • Folding selected text from code (user/application code): [24473.msg147312]
    • Obtaining state of folding (save fold state with session) [topic=26748]