aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept/find.lua
diff options
context:
space:
mode:
Diffstat (limited to 'modules/textadept/find.lua')
-rw-r--r--modules/textadept/find.lua361
1 files changed, 361 insertions, 0 deletions
diff --git a/modules/textadept/find.lua b/modules/textadept/find.lua
new file mode 100644
index 00000000..eafef5ed
--- /dev/null
+++ b/modules/textadept/find.lua
@@ -0,0 +1,361 @@
+-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE.
+
+local locale = _G.locale
+local events = _G.events
+local find = gui.find
+
+local lfs = require 'lfs'
+
+local MARK_FIND = 0
+local MARK_FIND_COLOR = 0x4D9999
+local previous_view
+
+-- Text escape sequences with their associated characters.
+local escapes = {
+ ['\\a'] = '\a', ['\\b'] = '\b', ['\\f'] = '\f', ['\\n'] = '\n',
+ ['\\r'] = '\r', ['\\t'] = '\t', ['\\v'] = '\v', ['\\\\'] = '\\'
+}
+
+-- Finds and selects text in the current buffer.
+-- @param text The text to find.
+-- @param next Flag indicating whether or not the search direction is forward.
+-- @param flags Search flags. This is a number mask of 4 flags: match case (2),
+-- whole word (4), Lua pattern (8), and in files (16) joined with binary OR.
+-- If nil, this is determined based on the checkboxes in the find box.
+-- @param nowrap Flag indicating whether or not the search won't wrap.
+-- @param wrapped Utility flag indicating whether or not the search has wrapped
+-- for displaying useful statusbar information. This flag is used and set
+-- internally, and should not be set otherwise.
+-- @return position of the found text or -1
+local function find_(text, next, flags, nowrap, wrapped)
+ if #text == 0 then return end
+ local buffer = buffer
+ local first_visible_line = buffer.first_visible_line -- for 'no results found'
+
+ local increment
+ if buffer.current_pos == buffer.anchor then
+ increment = 0
+ elseif not wrapped then
+ increment = next and 1 or -1
+ end
+
+ if not flags then
+ local find, c = find, _SCINTILLA.constants
+ flags = 0
+ if find.match_case then flags = flags + c.SCFIND_MATCHCASE end
+ if find.whole_word then flags = flags + c.SCFIND_WHOLEWORD end
+ if find.lua then flags = flags + 8 end
+ if find.in_files then flags = flags + 16 end
+ end
+
+ local result
+ find.captures = nil
+
+ if flags < 8 then
+ buffer:goto_pos(buffer[next and 'current_pos' or 'anchor'] + increment)
+ buffer:search_anchor()
+ if next then
+ result = buffer:search_next(flags, text)
+ else
+ result = buffer:search_prev(flags, text)
+ end
+ if result ~= -1 then buffer:scroll_caret() end
+
+ elseif flags < 16 then -- lua pattern search (forward search only)
+ text = text:gsub('\\[abfnrtv\\]', escapes)
+ local buffer_text = buffer:get_text(buffer.length)
+ local results = { buffer_text:find(text, buffer.anchor + increment) }
+ if #results > 0 then
+ result = results[1]
+ find.captures = { unpack(results, 3) }
+ buffer:set_sel(results[2], result - 1)
+ else
+ result = -1
+ end
+
+ else -- find in files
+ local utf8_dir =
+ gui.dialog('fileselect',
+ '--title', locale.FIND_IN_FILES_TITLE,
+ '--select-only-directories',
+ '--with-directory',
+ (buffer.filename or ''):match('^.+[/\\]') or '',
+ '--no-newline')
+ if #utf8_dir > 0 then
+ if not find.lua then text = text:gsub('([().*+?^$%%[%]-])', '%%%1') end
+ if not find.match_case then text = text:lower() end
+ if find.whole_word then text = '[^%W_]'..text..'[^%W_]' end
+ local match_case = find.match_case
+ local whole_word = find.whole_word
+ local format = string.format
+ local matches = { 'Find: '..text }
+ function search_file(file)
+ local line_num = 1
+ for line in io.lines(file) do
+ local optimized_line = line
+ if not match_case then optimized_line = line:lower() end
+ if whole_word then optimized_line = ' '..line..' ' end
+ if string.find(optimized_line, text) then
+ file = file:iconv('UTF-8', _CHARSET)
+ matches[#matches + 1] = format('%s:%s:%s', file, line_num, line)
+ end
+ line_num = line_num + 1
+ end
+ end
+ function search_dir(directory)
+ for file in lfs.dir(directory) do
+ if not file:find('^%.%.?$') then -- ignore . and ..
+ local path = directory..'/'..file
+ local type = lfs.attributes(path).mode
+ if type == 'directory' then
+ search_dir(path)
+ elseif type == 'file' then
+ search_file(path)
+ end
+ end
+ end
+ end
+ local dir = utf8_dir:iconv(_CHARSET, 'UTF-8')
+ search_dir(dir)
+ if #matches == 1 then matches[2] = locale.FIND_NO_RESULTS end
+ matches[#matches + 1] = ''
+ if buffer._type ~= locale.FIND_FILES_FOUND_BUFFER then
+ previous_view = view
+ end
+ gui._print(locale.FIND_FILES_FOUND_BUFFER, table.concat(matches, '\n'))
+ end
+ return
+ end
+
+ if result == -1 and not nowrap and not wrapped then -- wrap the search
+ local anchor, pos = buffer.anchor, buffer.current_pos
+ if next or flags >= 8 then
+ buffer:goto_pos(0)
+ else
+ buffer:goto_pos(buffer.length)
+ end
+ gui.statusbar_text = locale.FIND_SEARCH_WRAPPED
+ result = find_(text, next, flags, true, true)
+ if result == -1 then
+ gui.statusbar_text = locale.FIND_NO_RESULTS
+ buffer:line_scroll(0, first_visible_line)
+ buffer:goto_pos(anchor)
+ end
+ return result
+ elseif result ~= -1 and not wrapped then
+ gui.statusbar_text = ''
+ end
+
+ return result
+end
+events.connect('find', find_)
+
+-- Finds and selects text incrementally in the current buffer from a start
+-- point.
+-- Flags other than SCFIND_MATCHCASE are ignored.
+-- @param text The text to find.
+local function find_incremental(text)
+ local c = _SCINTILLA.constants
+ local flags = find.match_case and c.SCFIND_MATCHCASE or 0
+ --if find.lua then flags = flags + 8 end
+ buffer:goto_pos(find.incremental_start or 0)
+ find_(text, true, flags)
+end
+
+-- LuaDoc is in core/.find.lua.
+function find.find_incremental()
+ find.incremental = true
+ find.incremental_start = buffer.current_pos
+ gui.command_entry.entry_text = ''
+ gui.command_entry.focus()
+end
+
+events.connect('command_entry_keypress',
+ function(code)
+ if find.incremental then
+ if code == 0xff1b then -- escape
+ find.incremental = nil
+ elseif code < 256 or code == 0xff08 then -- character or backspace
+ local text = gui.command_entry.entry_text
+ if code == 0xff08 then
+ find_incremental(text:sub(1, -2))
+ else
+ find_incremental(text..string.char(code))
+ end
+ end
+ end
+ end, 1) -- place before command_entry.lua's handler (if necessary)
+
+events.connect('command_entry_command',
+ function(text) -- 'find next' for incremental search
+ if find.incremental then
+ find.incremental_start = buffer.current_pos + 1
+ find_incremental(text)
+ return true
+ end
+ end, 1) -- place before command_entry.lua's handler (if necessary)
+
+-- Replaces found text.
+-- 'find_' is called first, to select any found text. The selected text is then
+-- replaced by the specified replacement text.
+-- This function ignores 'Find in Files'.
+-- @param rtext The text to replace found text with. It can contain both Lua
+-- capture items (%n where 1 <= n <= 9) for Lua pattern searches and %()
+-- sequences for embedding Lua code for any search.
+-- @see find
+local function replace(rtext)
+ if #buffer:get_sel_text() == 0 then return end
+ if find.in_files then find.in_files = false end
+ local buffer = buffer
+ buffer:target_from_selection()
+ rtext = rtext:gsub('%%%%', '\\037') -- escape '%%'
+ if find.captures then
+ for i, v in ipairs(find.captures) do
+ v = v:gsub('%%', '%%%%') -- escape '%' for gsub
+ rtext = rtext:gsub('%%'..i, v)
+ end
+ end
+ local ret, rtext = pcall(rtext.gsub, rtext, '%%(%b())',
+ function(code)
+ local ret, val = pcall(loadstring('return '..code))
+ if not ret then
+ gui.dialog('ok-msgbox',
+ '--title', locale.FIND_ERROR_DIALOG_TITLE,
+ '--text', locale.FIND_ERROR_DIALOG_TEXT,
+ '--informative-text', val:gsub('"', '\\"'),
+ '--no-cancel')
+ error()
+ end
+ return val
+ end)
+ if ret then
+ rtext = rtext:gsub('\\037', '%%') -- unescape '%'
+ buffer:replace_target(rtext:gsub('\\[abfnrtv\\]', escapes))
+ buffer:goto_pos(buffer.target_end) -- 'find' text after this replacement
+ else
+ -- Since find is called after replace returns, have it 'find' the current
+ -- text again, rather than the next occurance so the user can fix the error.
+ buffer:goto_pos(buffer.current_pos)
+ end
+end
+events.connect('replace', replace)
+
+-- Replaces all found text.
+-- If any text is selected, all found text in that selection is replaced.
+-- This function ignores 'Find in Files'.
+-- @param ftext The text to find.
+-- @param rtext The text to replace found text with.
+-- @param flags The number mask identical to the one in 'find'.
+-- @see find
+local function replace_all(ftext, rtext, flags)
+ if #ftext == 0 then return end
+ if find.in_files then find.in_files = false end
+ local buffer = buffer
+ buffer:begin_undo_action()
+ local count = 0
+ if #buffer:get_sel_text() == 0 then
+ buffer:goto_pos(0)
+ while(find_(ftext, true, flags, true) ~= -1) do
+ replace(rtext)
+ count = count + 1
+ end
+ else
+ local anchor, current_pos = buffer.anchor, buffer.current_pos
+ local s, e = anchor, current_pos
+ if s > e then s, e = e, s end
+ buffer:insert_text(e, '\n')
+ local end_marker =
+ buffer:marker_add(buffer:line_from_position(e + 1), MARK_FIND)
+ buffer:goto_pos(s)
+ local pos = find_(ftext, true, flags, true)
+ while pos ~= -1 and
+ pos < buffer:position_from_line(
+ buffer:marker_line_from_handle(end_marker)) do
+ replace(rtext)
+ count = count + 1
+ pos = find_(ftext, true, flags, true)
+ end
+ e = buffer:position_from_line(buffer:marker_line_from_handle(end_marker))
+ buffer:goto_pos(e)
+ buffer:delete_back() -- delete '\n' added
+ if s == current_pos then anchor = e - 1 else current_pos = e - 1 end
+ buffer:set_sel(anchor, current_pos)
+ buffer:marker_delete_handle(end_marker)
+ end
+ gui.statusbar_text =
+ string.format(locale.FIND_REPLACEMENTS_MADE, tostring(count))
+ buffer:end_undo_action()
+end
+events.connect('replace_all', replace_all)
+
+-- When the user double-clicks a found file, go to the line in the file the text
+-- was found at.
+-- @param pos The position of the caret.
+-- @param line_num The line double-clicked.
+local function goto_file(pos, line_num)
+ if buffer._type == locale.FIND_FILES_FOUND_BUFFER then
+ line = buffer:get_line(line_num)
+ local file, file_line_num = line:match('^(.+):(%d+):.+$')
+ if file and file_line_num then
+ buffer:marker_delete_all(MARK_FIND)
+ buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR)
+ buffer:marker_add(line_num, MARK_FIND)
+ buffer:goto_pos(buffer.current_pos)
+ if #_VIEWS == 1 then
+ _, previous_view = view:split(false) -- horizontal
+ else
+ local clicked_view = view
+ if previous_view then previous_view:focus() end
+ if buffer._type == locale.FIND_FILES_FOUND_BUFFER then
+ -- there are at least two find in files views; find one of those views
+ -- that the file was not selected from and focus it
+ for _, v in ipairs(_VIEWS) do
+ if v ~= clicked_view then
+ previous_view = v
+ v:focus()
+ break
+ end
+ end
+ end
+ end
+ io.open_file(file)
+ buffer:ensure_visible_enforce_policy(file_line_num - 1)
+ buffer:goto_line(file_line_num - 1)
+ end
+ end
+end
+events.connect('double_click', goto_file)
+
+-- LuaDoc is in core/.find.lua.
+function find.goto_file_in_list(next)
+ local orig_view = view
+ for _, buffer in ipairs(_BUFFERS) do
+ if buffer._type == locale.FIND_FILES_FOUND_BUFFER then
+ for _, view in ipairs(_VIEWS) do
+ if view.doc_pointer == buffer.doc_pointer then
+ view:focus()
+ local orig_line = buffer:line_from_position(buffer.current_pos)
+ local line = orig_line
+ while true do
+ line = line + (next and 1 or -1)
+ if line > buffer.line_count - 1 then line = 0 end
+ if line < 0 then line = buffer.line_count - 1 end
+ if line == orig_line then -- prevent infinite loops
+ orig_view:focus()
+ return
+ end
+ if buffer:get_line(line):match('^(.+):(%d+):.+$') then
+ buffer:goto_line(line)
+ goto_file(buffer.current_pos, line)
+ return
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+if buffer then buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR) end
+events.connect('view_new',
+ function() buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR) end)