aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept
diff options
context:
space:
mode:
Diffstat (limited to 'modules/textadept')
-rw-r--r--modules/textadept/history.lua175
-rw-r--r--modules/textadept/init.lua1
-rw-r--r--modules/textadept/keys.lua81
-rw-r--r--modules/textadept/macros.lua2
-rw-r--r--modules/textadept/menu.lua8
5 files changed, 230 insertions, 37 deletions
diff --git a/modules/textadept/history.lua b/modules/textadept/history.lua
new file mode 100644
index 00000000..4729322c
--- /dev/null
+++ b/modules/textadept/history.lua
@@ -0,0 +1,175 @@
+-- Copyright 2019-2020 Mitchell. See LICENSE.
+
+local M = {}
+
+--[[ This comment is for LuaDoc.
+---
+-- Records buffer positions within Textadept views over time and allows for
+-- navigating through that history.
+--
+-- This module listens for text edit events and buffer switch events. Each time
+-- an insertion or deletion occurs, its location is recorded in the current
+-- view's location history. If the edit is close enough to the previous record,
+-- the previous record is amended. Each time a buffer switch occurs, the before
+-- and after locations are also recorded.
+-- @field minimum_line_distance (number)
+-- The minimum number of lines between distinct history records.
+-- The default value is `3`.
+-- @field maximum_history_size (number)
+-- The maximum number of history records to keep per view.
+-- The default value is `100`.
+module('textadept.history')]]
+
+M.minimum_line_distance = 3
+M.maximum_history_size = 100
+
+-- Map of views to their history records.
+-- Each record has a `pos` field that points to the current history position in
+-- the associated view.
+-- @class table
+-- @name view_history
+local view_history = setmetatable({}, {__index = function(t, view)
+ t[view] = {pos = 0}
+ return t[view]
+end})
+
+-- Listens for text insertion and deletion events and records their locations.
+events.connect(events.MODIFIED, function(position, mod_type, text, length)
+ local buffer = buffer
+ -- Only interested in text insertion or deletion.
+ if mod_type & buffer.MOD_INSERTTEXT > 0 then
+ if length == buffer.length then return end -- ignore file loading
+ position = position + length
+ elseif mod_type & buffer.MOD_DELETETEXT > 0 then
+ if buffer.length == 0 then return end -- ignore replacing buffer contents
+ else
+ return
+ end
+ -- Ignore undo/redo.
+ if mod_type & (buffer.PERFORMED_UNDO | buffer.PERFORMED_REDO) > 0 then
+ return
+ end
+ M.record(nil, buffer:line_from_position(position), buffer.column[position])
+end)
+
+-- Do not record positions during buffer switches when jumping backwards or
+-- forwards.
+local jumping = false
+
+-- Jumps to the current position in the current view's history after adjusting
+-- that position backwards or forwards.
+local function goto_record()
+ jumping = true
+ local history = view_history[view]
+ local record = history[history.pos]
+ local filename, line, column = record.filename, record.line, record.column
+ if lfs.attributes(filename) then
+ io.open_file(filename)
+ else
+ for _, buffer in ipairs(_BUFFERS) do
+ if buffer.filename == filename or buffer._type == filename or
+ not buffer.filename and not buffer._type and
+ filename == _L['Untitled'] then
+ view:goto_buffer(buffer)
+ break
+ end
+ end
+ end
+ buffer:goto_pos(buffer:find_column(line, column))
+ jumping = false
+end
+
+---
+-- Navigates backwards through the current view's history.
+-- @name back
+function M.back()
+ local history = view_history[view]
+ if #history == 0 then return end -- nothing to do
+ local record = history[history.pos]
+ local line = buffer:line_from_position(buffer.current_pos)
+ if buffer.filename ~= record.filename or
+ math.abs(record.line - line) > M.minimum_line_distance then
+ -- When navigated away from the most recent record, and if that record is
+ -- not a soft record, jump back to it first, then navigate backwards.
+ if not record.soft then goto_record() return end
+ -- Otherwise, update the soft record with the current position and
+ -- immediately navigate backwards.
+ M.record(record.filename, nil, nil, record.soft)
+ end
+ if history.pos > 1 then history.pos = history.pos - 1 end
+ goto_record()
+end
+
+---
+-- Navigates forwards through the current view's history.
+-- @name forward
+function M.forward()
+ local history = view_history[view]
+ if history.pos == #history then return end -- nothing to do
+ local record = history[history.pos]
+ if record.soft then M.record(record.filename, nil, nil, record.soft) end
+ history.pos = history.pos + 1
+ goto_record()
+end
+
+---
+-- Records the given location in the current view's history.
+-- @param filename Optional string filename, buffer type, or identifier of the
+-- buffer to store. If `nil`, uses the current buffer.
+-- @param line Optional Integer line number to store. If `nil`, uses the current
+-- line.
+-- @param column Optional integer column number on line *line* to store. If
+-- `nil`, uses the current column.
+-- @param soft Optional flag that indicates whether or not this record should be
+-- skipped when navigating backward towards it, and updated when navigating
+-- away from it. The default value is `false`.
+-- @name record
+function M.record(filename, line, column, soft)
+ if not assert_type(filename, 'string/nil', 1) then
+ filename = buffer.filename or buffer._type or _L['Untitled']
+ end
+ if not assert_type(line, 'number/nil', 2) then
+ line = buffer:line_from_position(buffer.current_pos)
+ end
+ if not assert_type(column, 'number/nil', 3) then
+ column = buffer.column[buffer.current_pos]
+ end
+ local history = view_history[view]
+ if #history > 0 then
+ local record = history[history.pos]
+ if filename == record.filename and
+ (math.abs(record.line - line) <= M.minimum_line_distance or
+ record.soft) then
+ -- If the most recent record is close enough (distance-wise), or if that
+ -- record is a soft record, update it instead of recording a new one.
+ record.line, record.column = line, column
+ record.soft = soft and record.soft
+ return
+ end
+ end
+ if history.pos < #history then
+ for i = history.pos + 1, #history do history[i] = nil end -- clear forward
+ end
+ history[#history + 1] = {
+ filename = filename, line = line, column = column, soft = soft
+ }
+ if #history > M.maximum_history_size then table.remove(history, 1) end
+ history.pos = #history
+end
+
+-- Softly record positions when switching between buffers.
+local function record_switch()
+ if not jumping then M.record(nil, nil, nil, true) end
+end
+events.connect(events.BUFFER_BEFORE_SWITCH, record_switch)
+events.connect(events.BUFFER_AFTER_SWITCH, record_switch)
+events.connect(events.FILE_OPENED, record_switch)
+
+---
+-- Clears all view history.
+-- @name clear
+function M.clear()
+ for view in pairs(view_history) do view_history[view] = {pos = 0} end
+end
+
+return M
diff --git a/modules/textadept/init.lua b/modules/textadept/init.lua
index a70afb6a..435b34dd 100644
--- a/modules/textadept/init.lua
+++ b/modules/textadept/init.lua
@@ -14,6 +14,7 @@ require('textadept.command_entry')
M.editing = require('textadept.editing')
M.file_types = require('textadept.file_types')
require('textadept.find')
+M.history = require('textadept.history')
M.macros = require('textadept.macros')
M.run = require('textadept.run')
M.session = require('textadept.session')
diff --git a/modules/textadept/keys.lua b/modules/textadept/keys.lua
index e9cc2c29..25db537b 100644
--- a/modules/textadept/keys.lua
+++ b/modules/textadept/keys.lua
@@ -25,41 +25,45 @@ local M = {}
-- None |None |None |Load session...
-- None |None |None |Save session...
-- Ctrl+Q |⌘Q |^Q |Quit
--- **Edit** | | |
--- Ctrl+Z<br/>Alt+Bksp |⌘Z |^Z^(†)<br/>M-Z|Undo
--- Ctrl+Y<br/>Ctrl+Shift+Z |⌘⇧Z |^Y<br/>M-S-Z |Redo
--- Ctrl+X<br/>Shift+Del |⌘X<br/>⇧⌦|^X |Cut
--- Ctrl+C<br/>Ctrl+Ins |⌘C |^C |Copy
--- Ctrl+V<br/>Shift+Ins |⌘V |^V |Paste
--- Ctrl+Shift+V |⌘⇧V |M-V |Paste Reindent
--- Ctrl+D |⌘D |None |Duplicate line
--- Del |⌦<br/>^D |Del<br/>^D |Delete
--- Alt+Del |^⌦ |M-Del<br/>M-D |Delete word
--- Ctrl+A |⌘A |M-A |Select all
--- Ctrl+M |^M |M-M |Match brace
--- Ctrl+Enter |^Esc |M-Enter^(‡) |Complete word
--- Ctrl+/ |^/ |M-/ |Toggle block comment
--- Ctrl+T |^T |^T |Transpose characters
--- Ctrl+Shift+J |^J |M-J |Join lines
--- Ctrl+&#124; |⌘&#124; |^\ |Filter text through
--- Ctrl+Shift+M |^⇧M |M-S-M |Select between delimiters
--- Ctrl+< |⌘< |M-< |Select between XML tags
--- Ctrl+> |⌘> |None |Select in XML tag
--- Ctrl+Shift+D |⌘⇧D |M-S-W |Select word
--- Ctrl+Shift+N |⌘⇧N |M-S-N |Select line
--- Ctrl+Shift+P |⌘⇧P |M-S-P |Select paragraph
--- Ctrl+Alt+U |^U |M-^U |Upper case selection
--- Ctrl+Alt+Shift+U |^⇧U |M-^L |Lower case selection
--- Alt+< |^< |M-> |Enclose as XML tags
--- Alt+> |^> |None |Enclose as single XML tag
--- Alt+" |^" |None |Enclose in double quotes
--- Alt+' |^' |None |Enclose in single quotes
--- Alt+( |^( |M-) |Enclose in parentheses
--- Alt+[ |^[ |M-] |Enclose in brackets
--- Alt+{ |^{ |M-} |Enclose in braces
--- Ctrl+Shift+Up |^⇧⇡ |S-^Up |Move selected lines up
--- Ctrl+Shift+Down |^⇧⇣ |S-^Down |Move selected lines down
--- Ctrl+P |⌘, |M-~ |Preferences
+-- **Edit** | | |
+-- Ctrl+Z<br/>Alt+Bksp |⌘Z |^Z^(†)<br/>M-Z|Undo
+-- Ctrl+Y<br/>Ctrl+Shift+Z|⌘⇧Z |^Y<br/>M-S-Z |Redo
+-- Ctrl+X<br/>Shift+Del |⌘X<br/>⇧⌦|^X |Cut
+-- Ctrl+C<br/>Ctrl+Ins |⌘C |^C |Copy
+-- Ctrl+V<br/>Shift+Ins |⌘V |^V |Paste
+-- Ctrl+Shift+V |⌘⇧V |M-V |Paste Reindent
+-- Ctrl+D |⌘D |None |Duplicate line
+-- Del |⌦<br/>^D |Del<br/>^D |Delete
+-- Alt+Del |^⌦ |M-Del<br/>M-D |Delete word
+-- Ctrl+A |⌘A |M-A |Select all
+-- Ctrl+M |^M |M-M |Match brace
+-- Ctrl+Enter |^Esc |M-Enter^(‡) |Complete word
+-- Ctrl+/ |^/ |M-/ |Toggle block comment
+-- Ctrl+T |^T |^T |Transpose characters
+-- Ctrl+Shift+J |^J |M-J |Join lines
+-- Ctrl+&#124; |⌘&#124; |^\ |Filter text through
+-- Ctrl+Shift+M |^⇧M |M-S-M |Select between delimiters
+-- Ctrl+< |⌘< |M-< |Select between XML tags
+-- Ctrl+> |⌘> |None |Select in XML tag
+-- Ctrl+Shift+D |⌘⇧D |M-S-W |Select word
+-- Ctrl+Shift+N |⌘⇧N |M-S-N |Select line
+-- Ctrl+Shift+P |⌘⇧P |M-S-P |Select paragraph
+-- Ctrl+Alt+U |^U |M-^U |Upper case selection
+-- Ctrl+Alt+Shift+U |^⇧U |M-^L |Lower case selection
+-- Alt+< |^< |M-> |Enclose as XML tags
+-- Alt+> |^> |None |Enclose as single XML tag
+-- Alt+" |^" |None |Enclose in double quotes
+-- Alt+' |^' |None |Enclose in single quotes
+-- Alt+( |^( |M-) |Enclose in parentheses
+-- Alt+[ |^[ |M-] |Enclose in brackets
+-- Alt+{ |^{ |M-} |Enclose in braces
+-- Ctrl+Shift+Up |^⇧⇡ |S-^Up |Move selected lines up
+-- Ctrl+Shift+Down |^⇧⇣ |S-^Down |Move selected lines down
+-- Alt+, |^, |M-, |Navigate backward
+-- Alt+. |^. |M-. |Navigate forward
+-- None |None |None |Record location
+-- None |None |None |Clear navigation history
+-- Ctrl+P |⌘, |M-~ |Preferences
-- **Search** | | |
-- Ctrl+F |⌘F |M-F<br/>M-S-F|Find
-- Ctrl+G<br/>F3 |⌘G |M-G |Find next
@@ -336,7 +340,12 @@ local bindings = {
{'ctrl+shift+up', 'ctrl+shift+up', 'ctrl+shift+up'},
[buffer.move_selected_lines_down] =
{'ctrl+shift+down', 'ctrl+shift+down', 'ctrl+shift+down'},
- -- Preferences
+ -- History.
+ [textadept.history.back] = {'alt+,', 'ctrl+,', 'meta+,'},
+ [textadept.history.forward] = {'alt+.', 'ctrl+.', 'meta+.'},
+ -- TODO: textadept.history.record
+ -- TODO: textadept.history.clear
+ -- Preferences.
[m_edit[_L['Preferences']][2]] = {'ctrl+p', 'cmd+,', 'meta+~'},
-- Search.
diff --git a/modules/textadept/macros.lua b/modules/textadept/macros.lua
index 2a165c36..3428ef11 100644
--- a/modules/textadept/macros.lua
+++ b/modules/textadept/macros.lua
@@ -1,4 +1,4 @@
--- Copyright 2018 Mitchell. See LICENSE.
+-- Copyright 2018-2020 Mitchell. See LICENSE.
--[[ This comment is for LuaDoc.
---
diff --git a/modules/textadept/menu.lua b/modules/textadept/menu.lua
index b69a9f7b..1c7aa4b1 100644
--- a/modules/textadept/menu.lua
+++ b/modules/textadept/menu.lua
@@ -131,6 +131,14 @@ local default_menubar = {
{_L['Move Selected Lines Up'], buffer.move_selected_lines_up},
{_L['Move Selected Lines Down'], buffer.move_selected_lines_down}
},
+ {
+ title = _L['History'],
+ {_L['Navigate Backward'], textadept.history.back},
+ {_L['Navigate Forward'], textadept.history.forward},
+ {_L['Record Location'], textadept.history.record},
+ SEPARATOR,
+ {_L['Clear History'], textadept.history.clear}
+ },
SEPARATOR,
{_L['Preferences'], function() io.open_file(_USERHOME .. '/init.lua') end}
},