aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/textadept/history.lua
blob: db23c4a461387450dd0f2ea20d5ed6e0a2c41a1a (plain)
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