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