Repository navigation
Expand file tree
/
Copy pathscroll.lua
More file actions
201 lines (171 loc) · 6 KB
/
Copy pathscroll.lua
File metadata and controls
201 lines (171 loc) · 6 KB
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
local state = require('opencode.state')
local output_window = require('opencode.ui.output_window')
local M = {}
---@param text string
---@return integer
local function line_display_width(text)
-- Vimscript receives NUL-containing Lua strings as Blobs. Its string
-- representation of a buffer NUL is a newline, with the same display width.
local display_text = text:gsub('%z', '\n')
return math.max(1, vim.fn.strdisplaywidth(display_text))
end
---@param win integer
---@return boolean
local function window_wraps(win)
local ok, wrap = pcall(vim.api.nvim_get_option_value, 'wrap', { win = win })
return ok and wrap == true or false
end
---@param win integer
---@return integer
local function get_text_width(win)
local width = vim.api.nvim_win_get_width(win)
local ok, wininfo = pcall(vim.fn.getwininfo, win)
local textoff = ok and wininfo and wininfo[1] and wininfo[1].textoff or 0
return math.max(1, width - textoff)
end
---@param buf integer
---@param win integer
---@param target_line integer
---@return integer
local function get_bottom_aligned_topline(buf, win, target_line)
local height = vim.api.nvim_win_get_height(win)
local text_width = get_text_width(win)
return vim.api.nvim_win_call(win, function()
local rows = 0
local line = target_line
while line >= 1 and rows < height do
local fold_start = vim.fn.foldclosed(line)
if fold_start ~= -1 then
rows = rows + 1
line = fold_start - 1
else
local text = vim.api.nvim_buf_get_lines(buf, line - 1, line, false)[1] or ''
local display_width = line_display_width(text)
rows = rows + math.max(1, math.ceil(display_width / text_width))
line = line - 1
end
end
return math.max(1, line + 1)
end)
end
---@param buf integer
---@param win integer
---@param line integer
local function restore_view_with_line_at_bottom(buf, win, line)
output_window.restore_view_topline(win, get_bottom_aligned_topline(buf, win, line))
end
---@param buf integer
---@param win integer
---@param target_line integer
---@return boolean
local function end_of_target_line_fits_view(buf, win, target_line)
if not window_wraps(win) then
return true
end
local visible_top = output_window.get_visible_top_line(win)
local visible_bottom = output_window.get_visible_bottom_line(win)
if not visible_top or not visible_bottom or target_line < visible_top or target_line > visible_bottom then
return false
end
local height = vim.api.nvim_win_get_height(win)
local text_width = get_text_width(win)
local rows = 0
local line = visible_top
while line <= target_line do
local fold_start = vim.api.nvim_win_call(win, function()
return vim.fn.foldclosed(line)
end)
if fold_start ~= -1 then
rows = rows + 1
line = vim.api.nvim_win_call(win, function()
return vim.fn.foldclosedend(line) + 1
end)
else
local text = vim.api.nvim_buf_get_lines(buf, line - 1, line, false)[1] or ''
local display_width = line_display_width(text)
rows = rows + math.max(1, math.ceil(display_width / text_width))
line = line + 1
end
if rows > height then
return false
end
end
return true
end
---@return integer|nil
function M.get_output_win()
local windows = state.windows
local win = windows and windows.output_win
if not win or not vim.api.nvim_win_is_valid(win) then
return nil
end
return win
end
---Move the cursor in `win` to the last line of `buf` and scroll so it's visible.
---@param win integer
---@param buf integer
function M.scroll_win_to_bottom(win, buf)
local line_count = vim.api.nvim_buf_line_count(buf)
if line_count == 0 then
return
end
local target_line = output_window.get_scroll_bottom_line(buf, line_count)
if target_line <= 0 then
return
end
local target_text = vim.api.nvim_buf_get_lines(buf, target_line - 1, target_line, false)[1] or ''
local visible_bottom = output_window.get_visible_bottom_line(win)
vim.api.nvim_win_set_cursor(win, { target_line, #target_text })
state.ui.set_cursor_position('output', { target_line, #target_text })
local needs_bottom_align = not visible_bottom or target_line > visible_bottom
if not needs_bottom_align and window_wraps(win) then
needs_bottom_align = not end_of_target_line_fits_view(buf, win, target_line)
end
if needs_bottom_align then
restore_view_with_line_at_bottom(buf, win, target_line)
end
output_window._prev_line_count_by_win[win] = line_count
output_window._manual_scroll_by_win[win] = nil
output_window.record_view_baseline(win)
end
---@param buf integer|nil
---@return { win: integer, follow: boolean }|nil
function M.pre_flush(buf)
if not buf or not vim.api.nvim_buf_is_valid(buf) then
return nil
end
local win = M.get_output_win()
if not win or vim.api.nvim_win_get_buf(win) ~= buf then
return nil
end
-- Snapshot the current line count before the buffer write so that
-- is_at_bottom() can compare cursor position against it after the write.
local ok, line_count = pcall(vim.api.nvim_buf_line_count, buf)
if ok and line_count and line_count > 0 then
output_window._prev_line_count_by_win[win] = line_count
end
-- Navigation autocmds may not have run before a scheduled streaming flush.
output_window.on_user_navigation(win)
return {
win = win,
follow = output_window.is_at_bottom(win),
}
end
---@param snapshot { win: integer, follow: boolean }|nil
---@param buf integer|nil
function M.post_flush(snapshot, buf)
if not snapshot or not buf or not vim.api.nvim_buf_is_valid(buf) then
return
end
if not vim.api.nvim_win_is_valid(snapshot.win) or vim.api.nvim_win_get_buf(snapshot.win) ~= buf then
return
end
if snapshot.follow and not output_window._manual_scroll_by_win[snapshot.win] then
M.scroll_win_to_bottom(snapshot.win, buf)
return
end
-- The write may shift topline/cursor (e.g. a re-rendered part above the
-- viewport); rebaseline so that shift is not mistaken for user navigation.
output_window.record_view_baseline(snapshot.win)
end
return M