Commits

shalabh  committed 593ca1e

initial import

  • Participants

Comments (0)

Files changed (3)

+textredux

File python/post_init.lua

+keys.python['c.'] = {jump_to_node, {'class%s', 'def%s'}}

File quicknav.lua

+--[[-
+Quicknav is module for the Textadept editor.
+
+It allows you to quickly find all lines in the current buffer matching
+a search query and allows you jump to one of the matched lines.
+
+A new 'explore buffer' is created allowing you to enter your query and the displayed matches
+are updated as you type. Moving the cursor to one of the matched lines and pressing Enter
+takes you to the same line in the original buffer.
+
+Regex, case-sensnsitive and word-boundary searches are specified by using special
+prefix operators in your search query. E.g. in the query '/a.b' the leading '/' specifies
+a regex search and 'a.b' is the regex matched.
+
+The prefix operators are:
+
+'/' (slash): Regular expression match (uses scintilla regular expression syntax)
+
+'.' (dot): Word-boundary start match
+
+'!' (exclamation mark): Case sensitive match
+
+']' (closing square bracket):  Fold-level (described below)
+
+';' (semi-colon): Function and class definitions (described below)
+
+These operators can be combined. E.g. '!/A.b' matches the case-sensitive regex 'A.b'.
+
+Quick Outline
+-------------
+
+The fold-level operator ']' and function-and-class-definition operator ';' display the
+matching results as soon as the operator is typed. They are useful for seeing a high level
+outline of the source.
+
+- *Fold Level*: The ']' operator is a repeatable operator that shows lines that are fold points.
+  A single ']' shows the top level fold points. Each repeated ']' operator shows the next level
+  of fold points.
+
+- *Function and Class definitions*: The operator ';' shows lines
+  containing the literals 'class', 'def' or 'function' only.
+  This is useful for seeing list function and class definitions in Python and Lua files
+  (or other languages where these keywords have special meaning)
+
+How to Use
+----------
+
+Requires the [Textredux](http://rgieseke.github.io/textredux/) module.
+
+Invoke the `quicknav.explore_search()` function from the buffer you want to search. To install
+a shortcut use this code:
+
+    _M.quicknav = require 'quicknav'
+
+    keys['c/'] = quicknav.explore_search
+
+Then open any buffer and type `Ctrl+/` to see the explore-search buffer. Then, start typing your
+query.
+
+]]
+
+_M.textredux = require 'textredux'
+
+local M = {}
+
+local flag_prefix = {
+  -- query is a regular expression. e.g. '/a+b', '^\w+' etc.
+  ['/'] = 'regex',
+  -- query must match start of word. e.g. '.get', '.text' etc.
+  ['.'] = 'token',
+  -- query must match case
+  ['!'] = 'strictcase',
+   -- query must words prefixed with 'class ', 'def ' or 'function ' (auto-preview)
+   -- (this is only useful for languages with these keywords (python, lua)
+  [';'] = 'defn'
+}
+
+-- the following special characters can be specified multiple times in the prefix.
+local multi_prefix = {
+  -- match fold levels lower than N - where N is number of times ']' is repeated (auto-preview)
+  [']'] = 'fold_level',
+}
+
+local function save_buffer_margins(buffer, prop_map)
+  prop_map['margin_width'] = {}
+  prop_map['margin_type'] = {}
+  for i=0,4 do
+    prop_map['margin_width'][i] = buffer.margin_width_n[i]
+    prop_map['margin_type'][i] = buffer.margin_type_n[i]
+  end
+end
+
+local function restore_buffer_margins(buffer, prop_map)
+  for i=0,4 do
+    buffer.margin_width_n[i] = prop_map['margin_width'][i]
+    buffer.margin_type_n[i] = prop_map['margin_type'][i]
+  end
+end
+
+local function set_buffer_properties(buffer)
+  -- setup margins for explore buffer
+  -- right aligned text margin is used to display line numbers
+  buffer.margin_type_n[0] = buffer.SC_MARGIN_RTEXT
+  lno_width =  buffer:text_width(0, 'H') * (('' .. buffer.data['source_properties'].line_count):len())
+end
+
+local function get_fold_level(buffer, lno)
+  local fold_level = buffer.fold_level[lno]
+  -- fold_level is an int combined with a bit mask
+
+  local level = bit32.band(fold_level - buffer.SC_FOLDLEVELBASE, buffer.SC_FOLDLEVELNUMBERMASK)
+  local header = bit32.band(fold_level, buffer.SC_FOLDLEVELHEADERFLAG) ==  buffer.SC_FOLDLEVELHEADERFLAG
+  return header, level
+end
+
+local function parse_query(query)
+  local special_tokens = ''
+  local opts = {}
+
+  while query:len() > 0 do
+    local first_char = query:sub(1,1)
+    if flag_prefix[first_char] and not opts[flag_prefix[first_char]] then
+      opts[flag_prefix[first_char]] = true
+      special_tokens = special_tokens..query:sub(1, 1)
+      query = query:sub(2,-1)
+    elseif multi_prefix[first_char] then
+      local prefix_name = multi_prefix[first_char]
+      if not opts[prefix_name] then opts[prefix_name] = 0
+      else opts[prefix_name] = opts[prefix_name] + 1
+      special_tokens = special_tokens..query:sub(1, 1)
+      query = query:sub(2, -1)
+      end
+    else
+      break
+    end
+  end
+  return special_tokens, opts, query
+end
+
+local function display_query(buffer, special_tokens, query)
+  buffer:goto_line(0)
+  buffer:del_line_right()
+  buffer:add_text(special_tokens)
+  buffer:add_text(query)
+  -- next two lines workaround strange bug in interaction of add_text() with hide_lines.
+  -- if you hide_lines(1,n) where line#1 happens to be a fold and then you insert text at line 0,
+  -- lines x+1 thru x+f (end of fold) get un-hidden.
+  buffer:add_text('\n')
+  buffer:delete_back()
+  buffer.margin_text[0] = '>>'
+end
+
+local function clear_search_results(buffer)
+  -- show all lines
+  buffer:show_lines(0, buffer.line_count - 1)
+  -- clear currently highlighted matches
+  buffer:indicator_clear_range(0, buffer.text_length)
+end
+
+local function show_predicated_lines(buffer)
+  for lno = buffer.data['offset_lines'], buffer.line_count do
+    if buffer.data['line_predicate'] then
+      if buffer.data['line_predicate'](lno) then
+        buffer:show_lines(lno, lno)
+      else
+        buffer:hide_lines(lno, lno)
+      end
+    end
+  end
+end
+
+local function show_lines_matching_query(buffer, search_flags, hidden_prefix, query)
+  -- a hidden_prefix is used as prefix of query for matching, but is not highlighted for matches
+  if hidden_prefix then
+    query = hidden_prefix .. query
+  end
+
+  -- setup highlighter for matches
+  buffer.indicator_current = buffer.data['indic_highlight']
+
+  -- position anchor for search
+  buffer:goto_line(buffer.data['offset_lines']-1)  -- goto_line index starts at 0
+  buffer:line_end()
+  buffer:search_anchor()
+
+  -- begin searching
+  local first = -1
+  local last = -1
+  local matches = {}
+
+  local msg = 'Matches: '
+  while true do
+    -- find next match
+    local first = buffer:search_next(search_flags, query)
+    if first == -1 then break end  -- match not found
+    last = buffer.selection_end  -- end position of matched text
+
+    -- find line number for match
+    local curline = buffer:line_from_position(first)
+
+    local pred = buffer.data['line_predicate']
+
+    if not pred or pred(curline) then
+      -- show matched line and highlight match
+      buffer:show_lines(curline, curline)
+      buffer:indicator_fill_range(first + hidden_prefix:len(),
+        last-first - hidden_prefix:len())
+
+      table.insert(matches, {first=first, last=last})
+
+      -- match max limit check
+      if #matches >= 500 then
+        msg = 'Reduced matches: '
+        break
+      end
+    end
+
+    -- update anchor for next match
+    buffer.current_pos = buffer.selection_end
+    buffer:search_anchor()
+  end
+  ui.statusbar_text = msg .. #matches
+
+  table.sort(matches, function(a,b) return a.first < b.first end)
+
+  return matches
+end
+
+local function hide_result_lines(buffer)
+  -- hide all lines (except query lines)
+  buffer:hide_lines(buffer.data['offset_lines'], buffer.line_count - 1)
+end
+
+local function apply_query(buffer, full_query, focus_first_result)
+  local special_tokens, opts, query = parse_query(full_query)
+  clear_search_results(buffer)
+  display_query(buffer, special_tokens, query)
+
+  -- set-up fold level predicate if needed
+  if opts.fold_level ~= nil then
+    buffer.data['line_predicate'] = function(lno)
+      local header, level = get_fold_level(buffer, lno)
+      max_level = buffer.data['level_map'][opts.fold_level]
+      if not max_level then return true end
+      if header and level <= max_level then return true end
+    end
+  else
+    buffer.data['line_predicate'] = nil
+  end
+
+  if query:len() < 1 and not opts.defn and opts.fold_level == nil then
+    -- nothing to search, and no auto-preview flag given
+    return
+  end
+
+  -- build search flags
+  local search_flags = 0
+  if opts.regex then search_flags = bit32.bor(search_flags, buffer.SCFIND_REGEXP) end
+  if opts.strictcase then search_flags = bit32.bor(search_flags, buffer.SCFIND_MATCHCASE) end
+  if opts.token or opts.defn then search_flags = bit32.bor(search_flags, buffer.SCFIND_WORDSTART) end
+
+  -- clear results
+  hide_result_lines(buffer)
+
+
+  if buffer.data['line_predicate'] ~= nil and query:len() < 1 then
+    -- only a predicate but no query
+    show_predicated_lines(buffer)
+    return
+  end
+
+  -- show matches
+  local first_match = 0
+  if opts.defn then
+    -- special handling for defn flag using hidden_prefixes
+    local matches = {}
+    for _, hidden_prefix in pairs{'function ', 'class ', 'def '} do
+      matches = show_lines_matching_query(buffer, search_flags, hidden_prefix, query)
+      if #matches > 0 and (first_match == 0 or matches[1].last < first_match) then first_match = matches[1].last end
+    end
+  else
+    local matches = show_lines_matching_query(buffer, search_flags, '', query)
+    if #matches > 0 then first_match = matches[1].last end
+  end
+
+  if focus_first_result then
+    -- position cursor on first match or query line (if no match)
+    buffer:goto_line(0)
+    if first_match > 0 then
+      buffer:goto_pos(first_match)
+    else
+      buffer:line_end()
+    end
+  end
+end
+
+local function update_explore_buffer(buffer)
+  apply_query(buffer, buffer.data['query'], buffer.data['focus_first_result'])
+end
+
+
+function M.explore_search()
+  local source_buffer = _G.buffer
+  local source_properties = {
+    text = _G.buffer:get_text(),
+    lexer = _G.buffer:get_lexer(),
+    filename = _G.buffer.filename,
+    line_count = _G.buffer.line_count
+  }
+
+  -- ensure file backed buffer (this also prevents recursive invocation of explore_buffer)
+  if not source_properties.filename then return end
+
+  -- we have to save and restore current buffer margins because the explore buffer changes them.
+  -- this is weird because I expected margins to be buffer local.
+  save_buffer_margins(_G.buffer, source_properties)
+
+  -- the explore buffer accepts user input and previews search results
+  local explore_buffer = _M.textredux.core.buffer.new('Find: '..source_properties.filename)
+  explore_buffer:show()
+
+  -- setup indicator to hightlight matches
+  local indic_highlight = _SCINTILLA.next_indic_number()
+  explore_buffer.indic_style[indic_highlight] = buffer.INDIC_ROUNDBOX
+  explore_buffer.indic_fore[indic_highlight] = '0x00FFFF'  -- TODO: should be automatic based on theme
+
+  -- setup buffer data
+  explore_buffer.data['query'] = ''  -- the current query entered by user
+  explore_buffer.data['source_properties'] = source_properties
+  explore_buffer.data['offset_lines'] = 2  -- number of topmost lines used for displaying query
+  explore_buffer.data['search_opts'] = {} -- currently specified search options
+  explore_buffer.data['line_predicate'] = nil  -- currently specified addtional filter for source lines
+  explore_buffer.data['indic_highlight'] = indic_highlight
+
+  -- refresh is only called once because update() is used for subsequent redraws
+  function explore_buffer.on_refresh(buffer)
+    set_buffer_properties(buffer, source_buffer)
+
+    -- the top 'offset' lines are used to display current query
+    local offset = buffer.data['offset_lines']
+    for i=0, offset-1 do
+      buffer:add_text('\n')
+      buffer.margin_text[i] = ''
+    end
+    buffer.margin_text[0] = '>>' -- query line
+
+    -- copy the entire source text into the explore buffer with the appropriate lexer
+    buffer:add_text(source_properties.text)
+    buffer:set_lexer(source_properties.lexer)
+    buffer:colourise(0, -1)  -- forces entire text to be parsed
+
+    -- fold level numbers occasionally have gaps, level_map maps a sequential level number
+    -- to actual level number
+    local all_levels = {}
+    for lno=1, buffer.line_count do
+      local header, level = get_fold_level(buffer, lno)
+      if header then all_levels[level] = true end
+    end
+    local level_map = {}
+    for level, _ in pairs(all_levels) do
+      table.insert(level_map, level)
+    end
+    table.sort(level_map)
+    explore_buffer.data['level_map'] = level_map
+
+
+
+    -- display line numbers (these have to be off by offset because of the query lines at top)
+    for lno=offset,buffer.line_count do
+      buffer.margin_text[lno]=lno-offset+1
+    end
+
+    -- show cursor at top for query input
+    buffer:goto_line(0)
+  end
+
+
+  function explore_buffer.on_keypress(buffer, key, code, shift, ctrl, alt, meta)
+    local query = explore_buffer.data['query']
+    local pos = buffer.current_pos
+
+    -- pos_in_query is the implied cursor position within query.
+    -- if cursor is actually within query then it is same as cursor postion.
+    -- if cursor is somewhere else then pos_in_query is the last position in
+    -- query (i.e. any input will append to query)
+
+    local pos_in_query = query:len()
+    if pos < pos_in_query then
+      pos_in_query = pos
+    end
+
+    if key == '\b' then
+      -- if query selected, delete selection
+      local sel_start, sel_end = buffer.selection_start, buffer.selection_end
+      if sel_end > sel_start and sel_end == pos_in_query then
+        query = query:sub(1, sel_start)..query:sub(sel_end)
+        pos_in_query = pos_in_query - (sel_end - sel_start)
+      else
+        -- delete character just before pos_in_query
+        if query == '' or pos_in_query == 0 then return end
+        query = query:sub(1, pos_in_query-1)..query:sub(pos_in_query+1)
+        pos_in_query = pos_in_query - 1
+      end
+
+    elseif key == 'esc' then
+      -- if cursor is not within query, move cursor to query, else exit buffer.
+      if pos ~= pos_in_query then
+        buffer.data['focus_first_result'] = false
+        explore_buffer:goto_pos(pos_in_query)
+        return true
+      else
+        local prop_map = buffer.data['source_properties']
+        buffer:close()
+        restore_buffer_margins(_G.buffer, prop_map)
+        return true
+      end
+
+    elseif key == '\n' then
+      -- selected current match (close buffer and position cursor on same line in source)
+      local prop_map = buffer.data['source_properties']
+      local pos = buffer.current_pos - buffer.line_end_position[buffer.data['offset_lines']-1]
+      buffer:close()
+      restore_buffer_margins(_G.buffer, prop_map)
+      if pos > 0 then
+        _G.buffer.goto_pos(pos - 1)
+      end
+      return true
+
+    elseif key and not (ctrl or alt or meta) and key:len() == 1 then
+      -- update query (insert typed key at pos_in_query)
+      query = query:sub(1, pos_in_query) .. key .. query:sub(pos_in_query+1)
+      pos_in_query = pos_in_query + 1
+    else
+      return
+    end
+
+    -- save updated query
+    explore_buffer.data['query'] = query
+
+    -- the guideline for positioning cursor is as follows:
+    --   focus first match, unless it hampers user trying to edit query.
+    -- the logic for positioning cursor is as follows:
+    --  if the last typed character was at the end of query, focus the first match (this
+    --  allows user to continue typing at end of query). else, position cursor within query
+    --  (this allows user to edit within the query)
+
+    local focus_first_result = pos_in_query == query:len()
+    explore_buffer.data['focus_first_result'] = focus_first_result
+
+    explore_buffer:update(update_explore_buffer)
+    if not focus_first_result then
+      explore_buffer:goto_pos(pos_in_query)
+    end
+    return true
+  end
+
+  explore_buffer:show()
+end
+
+return M