Skip to content

Commit f211049

Browse files
committed
Add fallback for too many modified lines with TWS
Tracking all modified lines with trailing whitespace inherently gives rise to pathological cases like `:%s/$/ /`. To gracefully handle them, without hitting recursion limits or massive memory consumption, keep track of the number of lines stored and if it gets too high, fallback to treating every line as modified. Since the probability that one would introduce TWS to >100 new lines while already having TWS in the file originally is small, this is a good trade-off. Other approaches considered: * Timing based: Say tree becomes horrendously unbalanced. Then adding new nodes would be fast, but (assuming no recursion limit) trying to access the deepest node could still freeze. * Catch max recursion depth reached error: Will not protect against unbalanced tree consuming too much memory. In the future, storing intervals instead of discrete line numbers could be something to look into, if it does not add too much complexity.
1 parent 42ed507 commit f211049

File tree

2 files changed

+42
-7
lines changed

2 files changed

+42
-7
lines changed

plugin/strip_trailing_whitespace.vim

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ command -bar -range=% StripTrailingWhitespace keeppatterns <line1>,<line2>substi
1212

1313
const s:null = {} " Sentinel value indicating null.
1414

15+
if !exists('g:strip_trailing_whitespace_max_lines')
16+
" The maximum number of modified lines with trailing whitespace to keep
17+
" track of before falling back to stripping the entire file.
18+
let g:strip_trailing_whitespace_max_lines = &maxfuncdepth - 5
19+
endif
20+
1521
function s:RotateRight(n) abort
1622
let x = a:n.left
1723
let a:n.left = x.right
@@ -32,6 +38,9 @@ function s:RotateLeft(n) abort
3238
return x
3339
endfunction
3440

41+
" Return the number of nodes in tree {n}.
42+
let s:NodeCount = {n -> n is s:null ? 0 : 1 + s:NodeCount(n.left) + s:NodeCount(n.right)}
43+
3544
" Splay {key} in the tree rooted at the node {n}.
3645
"
3746
" If a node with that key exists, it is splayed to the root of the tree. If it
@@ -81,6 +90,7 @@ function s:Put(key) abort
8190
if b:stw_root is s:null
8291
" Splay key to root
8392
let b:stw_root = {'key': a:key, 'left': s:null, 'right': s:null}
93+
let b:stw_count += 1
8494
return
8595
endif
8696

@@ -94,12 +104,14 @@ function s:Put(key) abort
94104
if n.left isnot s:null | let n.left.key += b:stw_root.key - n.key | endif
95105
let n.right.key -= n.key
96106
let b:stw_root = n
107+
let b:stw_count += 1
97108
elseif cmp > 0
98109
let n = {'key': a:key, 'left': b:stw_root, 'right': b:stw_root.right}
99110
let b:stw_root.right = s:null
100111
if n.right isnot s:null | let n.right.key += b:stw_root.key - n.key | endif
101112
let n.left.key -= n.key
102113
let b:stw_root = n
114+
let b:stw_count += 1
103115
else
104116
" Duplicate key
105117
endif
@@ -110,6 +122,7 @@ function s:Remove(key) abort
110122
let b:stw_root = s:Splay(b:stw_root, a:key)
111123
" Check if key was in the tree
112124
if a:key != b:stw_root.key | return | endif
125+
let b:stw_count -= 1
113126

114127
if b:stw_root.left is s:null
115128
let b:stw_root = b:stw_root.right
@@ -124,7 +137,7 @@ function s:Remove(key) abort
124137
endif
125138
endfunction
126139

127-
" Removes the specified range of keys from the tree.
140+
" Remove the specified range of keys from the tree.
128141
"
129142
" {min} and {max} are inclusive line numbers defining the range to delete
130143
function s:RemoveRange(min, max) abort
@@ -135,13 +148,15 @@ function s:RemoveRange(min, max) abort
135148
if b:stw_root.key >= a:min && b:stw_root.key <= a:max
136149
if b:stw_root.left isnot s:null | let b:stw_root.left.key += b:stw_root.key | endif
137150
let b:stw_root = b:stw_root.left
151+
let b:stw_count -= 1
138152
endif
139153
else
140154
" Do modified Hibbard deletion
141155
if b:stw_root.key >= a:min && b:stw_root.key <= a:max " Should remove root node but keep left subtree
142156
let rootkey = b:stw_root.key
143157
let x = b:stw_root.left
144158
let b:stw_root = s:Splay(b:stw_root.right, a:max - rootkey + 1)
159+
let b:stw_count -= 1 + s:NodeCount(b:stw_root.left)
145160
let b:stw_root.left = x
146161

147162
if x isnot s:null | let x.key -= b:stw_root.key | endif
@@ -150,13 +165,15 @@ function s:RemoveRange(min, max) abort
150165
call s:Remove(a:max) " Root could still be less than max
151166
else " Should keep root node and left subtree
152167
let b:stw_root.right = s:Splay(b:stw_root.right, a:max - b:stw_root.key + 1)
168+
let b:stw_count -= s:NodeCount(b:stw_root.right.left)
153169
if b:stw_root.right.key < a:max
154170
let b:stw_root.right.left = s:null
155171
else
156172
if b:stw_root.right.right isnot s:null
157173
let b:stw_root.right.right.key += b:stw_root.right.key
158174
endif
159175
let b:stw_root.right = b:stw_root.right.right
176+
let b:stw_count -= 1
160177
endif
161178
endif
162179
endif
@@ -168,7 +185,7 @@ endfunction
168185
let s:is_stripping = 0
169186

170187
function StripTrailingWhitespaceListener(bufnr, start, end, added, changes) abort
171-
if s:is_stripping | return | endif
188+
if s:is_stripping || b:stw_count > g:strip_trailing_whitespace_max_lines | return | endif
172189

173190
" Remove existing in range
174191
if a:start < a:end
@@ -191,14 +208,21 @@ function StripTrailingWhitespaceListener(bufnr, start, end, added, changes) abor
191208
let has_trailing_ws = getline(lnum) =~# '\s$'
192209
if has_trailing_ws
193210
call s:Put(lnum)
211+
if b:stw_count > g:strip_trailing_whitespace_max_lines
212+
" Max count since unable to recommence (might have missed changes)
213+
let [b:stw_root, b:stw_count] = [s:null, 1 / 0]
214+
echohl WarningMsg | echo 'Falling back to stripping entire file: Too many TWS'
215+
\ '(use `:let b:strip_trailing_whitespace_enabled = 0` to skip)' | echohl None
216+
break
217+
endif
194218
endif
195219
endfor
196220
endfunction
197221

198222
function s:OnBufEnter() abort
199223
if exists('b:stw_root') | return | endif
200224

201-
let b:stw_root = s:null
225+
let [b:stw_root, b:stw_count] = [s:null, 0]
202226
if has('nvim')
203227
lua vim.api.nvim_buf_attach(0, false, {
204228
\ on_lines = function(_, bufnr, _, firstline, lastline, new_lastline)
@@ -209,7 +233,7 @@ function s:OnBufEnter() abort
209233
endif
210234
endfunction
211235

212-
" Recursively strips lines in the specified tree.
236+
" Recursively strip lines in the specified tree.
213237
function s:StripTree(n, offset) abort
214238
silent execute (a:n.key + a:offset) 'StripTrailingWhitespace'
215239

@@ -225,11 +249,15 @@ function s:OnWrite() abort
225249
let s:is_stripping = 1
226250
let save_cursor = getcurpos()
227251
try
228-
if b:stw_root isnot s:null | call s:StripTree(b:stw_root, 0) | endif
252+
if b:stw_count > g:strip_trailing_whitespace_max_lines
253+
silent StripTrailingWhitespace
254+
else
255+
if b:stw_root isnot s:null | call s:StripTree(b:stw_root, 0) | endif
256+
let [b:stw_root, b:stw_count] = [s:null, 0]
257+
endif
229258
finally
230259
call setpos('.', save_cursor)
231260
let s:is_stripping = 0
232-
let b:stw_root = s:null
233261
endtry
234262
endfunction
235263

test/test.vim

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ function s:TestEdits(original, EditCb, expected) abort
22
let fname = tempname()
33
call writefile(a:original, fname)
44
silent execute 'edit!' fname
5-
call a:EditCb()
5+
silent call a:EditCb()
66
silent write
77
call assert_equal(a:expected, readfile(fname))
88
endfunction
@@ -51,3 +51,10 @@ function Test_DisabledForMarkdown() abort
5151
endfunction
5252
call s:TestEdits(['line '], function('s:EditCb'), ['fine '])
5353
endfunction
54+
55+
function Test_HandleManyLinesWithTWS() abort
56+
function! s:EditCb() abort
57+
execute 'normal! 100o '
58+
endfunction
59+
call s:TestEdits(['line '], function('s:EditCb'), extend(['line'], repeat([''], 100)))
60+
endfunction

0 commit comments

Comments
 (0)