1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
-- Copyright 2019-2022 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
})
local INSERT, DELETE = buffer.MOD_INSERTTEXT, buffer.MOD_DELETETEXT
local UNDO, REDO = buffer.PERFORMED_UNDO, buffer.PERFORMED_REDO
-- Listens for text insertion and deletion events and records their locations.
events.connect(events.MODIFIED, function(position, mod, text, length)
if mod & (INSERT | DELETE) == 0 or buffer.length == (mod & INSERT > 0 and length or 0) then
return -- ignore non-insertion/deletion, file loading, and replacing buffer contents
end
if mod & INSERT > 0 then position = position + length end
if mod & (UNDO | REDO) > 0 then return end -- ignore undo/redo
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 given record in the current view's history.
-- @param record History record to jump to.
local function jump(record)
jumping = true
local filename = record.filename
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(record.line, record.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 and buffer._type ~= 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
jump(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
jump(history[history.pos])
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
jump(history[history.pos])
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
|