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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
-- 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, text, length)
local buffer = buffer
-- Only interested in text insertion or deletion.
if mod & buffer.MOD_INSERTTEXT > 0 then
if length == buffer.length then return end -- ignore file loading
position = position + length
elseif mod & buffer.MOD_DELETETEXT > 0 then
if buffer.length == 0 then return end -- ignore replacing buffer contents
else
return
end
-- Ignore undo/redo.
if mod & (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 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
|