-- Copyright --- Модуль с универсальным фолдером -- Благодаря универсальности можно настроить для большинства лексеров для Lexer.Fold -- @author Tymur Gubayev -- @version 0.9 pre-release -- @usage Fold = require'folder'{ -- ['+'] = {'function', 'do', 'then', 'repeat', --[['if',]] '{', '--[['}, -- ['-'] = {'end', 'elseif', 'until', '}', ']]'}, -- ['+-'] = {'else'}, -- types = {'keyword', ['operator']={'{','}'}, ['comment']={'--[[',']]'}}, -- } -- @see MakeFold -- @bug (script) (lexer lua) Может ошибочно поставить точку фолдинга в комментарии -- если там встречается последовательность `--[[` -- @bug (script) (lexer lua) Если в том же блоке комментария будет `]]`, то этот баг -- практически не заметен -- @bug (core) Неправильно обрабатывается точка фолдинга в самой первой строке текста (баг ядра) -- @bug (core) Не обрабатывается последняя строка текста (баг ядра) -- @todo см. в тексте `@todo:` --- Makes iterator from a LPEG-pattern -- (for use in generic `for` loop) -- @param p (pattern) should return index as last value -- and nil if dont matches -- @param str (string) text to apply `p` at -- @usage for idx, word in iterate(search,input) do print(idx, word) end -- @see Fold local function iterate( p, str ) return function (s, idx) local a,b = p:match( s, idx ) return b,a end, str end --- Creates a search pattern from words-array -- @param wordlist (table) -- @param case_insensitive Optional boolean flag indicating whether the word -- match is case-insensitive. -- @usage local search = list2search{ 'foo', 'bar', 'baz' } creates a pattern -- that matches any of the words 'foo', 'bar', or 'baz' case sensitively. local function list2search ( wordlist, case_insensitive ) local patts = {} if not case_insensitive then for i,w in ipairs(wordlist) do patts[i]=lpeg.C(lpeg.P(wordlist[i])) end else -- create case insensitive LPEG-patterns for i,w in ipairs(wordlist) do -- перебираем слова local l, u = w:lower(), w:upper() if l == u then -- ни одной буквы, и замечательно! patts[i]=lpeg.C(lpeg.P(w)) else -- перебираем буквы local c = lpeg.S(l:sub(1,1)..u:sub(1,1)) for j = 2, #w do c = c * lpeg.S(l:sub(j,j)..u:sub(j,j)) end patts[i] = c/string.lower -- Ключевые слова в foldkeywords _обязательно_ lowercase! end end -- for end -- else (монстрячий код) -- собираем конечный паттерн local p = patts[1] for i = 2, #patts do p = p + patts[i] end -- паттерн ловит новую строку и позицию. Кошерно было бы тестить ещё и на \r\n, но не нужно. @see Fold local nl = --[[lpeg.P('\r')^-1*]]lpeg.C(lpeg.P('\n')) * lpeg.Cp() p = nl + p * lpeg.Cp() --- Данная грамматика читается так: -- проверяем `p`. Если не подошло, то сдвигаемся сразу на слово/число (`alnum^1`), -- или хотя-бы на 1 символ (`any`), после чего запускаем цикл сначала (`lpeg.V(1)`). return lpeg.P{ [1] = p + (alnum^1 + any) * lpeg.V(1) } end -- list2search() --- Генерирует таблицы проверки типов -- @param types (table) список типов и/или таблица соответствий тип-ключевые слова -- @usage type_idxs, keyword_types = generatetypes( {'keyword', ['operator']={'{','}'},} ) -- @see Fold local function generatetypes ( types ) local t_idx, t_key = {}, {} for k, v in pairs(types) do if type(k)=='number' then -- default type t_idx[Lexer.Types[v]]=v else -- table type={keyword1, keyword2,} t_idx[Lexer.Types[k]]=k for _,w in ipairs(v) do t_key[w]=k end end end return t_idx, t_key end -- generatetypes() --- Функция создающая функцию-фолдер для использования как Lexer.Fold -- Все настройки передаются в единственном параметре -- @param foldkeywords (table) таблица с описанием фолдера -- Выглядит примерно так: -- local foldkeywords = { -- ['+'] = {'function', 'do', 'then', 'repeat', --[['if',]] '{', '--[['}, -- ['-'] = {'end', 'elseif', 'until', '}', ']]'}, -- ['+-'] = {'else'}, -- types = {'keyword', ['operator']={'{','}'}, ['comment']={'--[[',']]'}}, -- -- case_insensitive = false, -- default -- } -- (works fine with Lua) -- список в `+` - слова, увеличивающие уровень (метка точки фолдинга) -- список в `-` - слова, уменьшающие уровень -- список в `+-` - слова, уменьшающие уровень -- case_insensitive = true выключает чувствительность к регистру -- types определяют допустимые и специальные типы ключевых слов: -- допустимые в array-части, специальные отдельными списками -- вида тип=список_слов. -- @! ключевые слова в `foldkeywords` для case-insensitiv языков _обязательно_ lowercase! -- @usage Fold = MakeFold( foldkeywords ) local function MakeFold( foldkeywords ) local types = foldkeywords.types or {'keyword'} local allwords = {} local keyword2op = {} for _,op in ipairs{'+','-','+-'} do for _,v in ipairs(foldkeywords[op]) do keyword2op[v]=op allwords[#allwords+1]=v end end local search = list2search( allwords, foldkeywords.case_insensitive ) --- Собственно фолдер. -- Монструозьненькая получилась функция -- @see lexer.lua local function Fold(input, start_pos, start_line, start_level) --подготовка... local folds = {} local current_line = start_line local current_level = start_level local offset = start_pos local keyword_idxs, keyword_types = generatetypes( types ) -- @todo: возможно ли перенести в MakeFolder() ? -- главный цикл: перебираем ключевые для фолдинга слова (и переносы строк) for idx, word in iterate(search,input) do if word == '\n' then -- print(current_line+1, current_level%2^10) folds[current_line] = folds[current_line] or { current_level } current_line = current_line + 1 else -- ключевое слово local postype = keyword_idxs[GetStyleAt(idx+offset-1)] -- тип в месте, где найдено слово -- local wordtype = keyword_types[word] -- "специальный" тип у слова (используется только однажды, поэтому закомментен) -- Проверка выглядит так: тип найденного слова должен быть одним из допустимых (keyword_idxs) -- а также либо НЕ являтся специальным (не быть ключом в types) -- либо совпадать со специальным типом найденного слова (postype == keyword_types[word]) if postype and (not types[postype] or postype == keyword_types[word]) then local op = keyword2op[word] if op == '+' then folds[current_line] = { current_level, SC_FOLDLEVELHEADERFLAG } current_level = current_level + 1 elseif op == '-' then current_level = current_level - 1 -- Если это первая операция в первой строке, то if current_line==start_line and not folds[current_line] then current_level = current_level + 1 end folds[current_line] = { current_level } elseif op == '+-' then -- Если это первая операция в первой строке, то if current_line==start_line and not folds[current_line] then current_level = current_level + 1 end current_level = current_level - 1 folds[current_line] = { current_level, SC_FOLDLEVELHEADERFLAG } current_level = current_level + 1 -- сначала отняли, потом прибавили 1. Казалось бы, можно сэкономить пару строк? А вот хрен. См. `if` чуть выше. else print('unknown folder op:',op) end end -- if слово надо обработать end -- else end -- for return folds end -- Fold() return Fold end -- MakeFold() return MakeFold