##// END OF EJS Templates
revert: fix interactive reverting of end-of-file newline changes...
Rodrigo Damazio Bovendorp -
r45713:8b6a4465 default
parent child Browse files
Show More
@@ -0,0 +1,55 b''
1 #require tic
2
3 Revert interactive tests with the Curses interface
4
5 $ cat <<EOF >> $HGRCPATH
6 > [ui]
7 > interactive = true
8 > interface = curses
9 > [experimental]
10 > crecordtest = testModeCommands
11 > EOF
12
13 TODO: Make a curses version of the other tests from test-revert-interactive.t.
14
15 When a line without EOL is selected during "revert -i"
16
17 $ hg init $TESTTMP/revert-i-curses-eol
18 $ cd $TESTTMP/revert-i-curses-eol
19 $ echo 0 > a
20 $ hg ci -qAm 0
21 $ printf 1 >> a
22 $ hg ci -qAm 1
23 $ cat a
24 0
25 1 (no-eol)
26
27 $ cat <<EOF >testModeCommands
28 > c
29 > EOF
30
31 $ hg revert -ir'.^'
32 reverting a
33 $ cat a
34 0
35
36 When a selected line is reverted to have no EOL
37
38 $ hg init $TESTTMP/revert-i-curses-eol2
39 $ cd $TESTTMP/revert-i-curses-eol2
40 $ printf 0 > a
41 $ hg ci -qAm 0
42 $ echo 0 > a
43 $ hg ci -qAm 1
44 $ cat a
45 0
46
47 $ cat <<EOF >testModeCommands
48 > c
49 > EOF
50
51 $ hg revert -ir'.^'
52 reverting a
53 $ cat a
54 0 (no-eol)
55
@@ -1,2026 +1,2033 b''
1 1 # stuff related specifically to patch manipulation / parsing
2 2 #
3 3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # This code is based on the Mark Edgington's crecord extension.
9 9 # (Itself based on Bryan O'Sullivan's record extension.)
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import os
14 14 import re
15 15 import signal
16 16
17 17 from .i18n import _
18 18 from .pycompat import (
19 19 getattr,
20 20 open,
21 21 )
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 patch as patchmod,
26 26 pycompat,
27 27 scmutil,
28 28 util,
29 29 )
30 30 from .utils import stringutil
31 31
32 32 stringio = util.stringio
33 33
34 34 # patch comments based on the git one
35 35 diffhelptext = _(
36 36 """# To remove '-' lines, make them ' ' lines (context).
37 37 # To remove '+' lines, delete them.
38 38 # Lines starting with # will be removed from the patch.
39 39 """
40 40 )
41 41
42 42 hunkhelptext = _(
43 43 """#
44 44 # If the patch applies cleanly, the edited hunk will immediately be
45 45 # added to the record list. If it does not apply cleanly, a rejects file
46 46 # will be generated. You can use that when you try again. If all lines
47 47 # of the hunk are removed, then the edit is aborted and the hunk is left
48 48 # unchanged.
49 49 """
50 50 )
51 51
52 52 patchhelptext = _(
53 53 """#
54 54 # If the patch applies cleanly, the edited patch will immediately
55 55 # be finalised. If it does not apply cleanly, rejects files will be
56 56 # generated. You can use those when you try again.
57 57 """
58 58 )
59 59
60 60 try:
61 61 import curses
62 62 import curses.ascii
63 63
64 64 curses.error
65 65 except (ImportError, AttributeError):
66 66 curses = False
67 67
68 68
69 69 class fallbackerror(error.Abort):
70 70 """Error that indicates the client should try to fallback to text mode."""
71 71
72 72 # Inherits from error.Abort so that existing behavior is preserved if the
73 73 # calling code does not know how to fallback.
74 74
75 75
76 76 def checkcurses(ui):
77 77 """Return True if the user wants to use curses
78 78
79 79 This method returns True if curses is found (and that python is built with
80 80 it) and that the user has the correct flag for the ui.
81 81 """
82 82 return curses and ui.interface(b"chunkselector") == b"curses"
83 83
84 84
85 85 class patchnode(object):
86 86 """abstract class for patch graph nodes
87 87 (i.e. patchroot, header, hunk, hunkline)
88 88 """
89 89
90 90 def firstchild(self):
91 91 raise NotImplementedError(b"method must be implemented by subclass")
92 92
93 93 def lastchild(self):
94 94 raise NotImplementedError(b"method must be implemented by subclass")
95 95
96 96 def allchildren(self):
97 97 """Return a list of all of the direct children of this node"""
98 98 raise NotImplementedError(b"method must be implemented by subclass")
99 99
100 100 def nextsibling(self):
101 101 """
102 102 Return the closest next item of the same type where there are no items
103 103 of different types between the current item and this closest item.
104 104 If no such item exists, return None.
105 105 """
106 106 raise NotImplementedError(b"method must be implemented by subclass")
107 107
108 108 def prevsibling(self):
109 109 """
110 110 Return the closest previous item of the same type where there are no
111 111 items of different types between the current item and this closest item.
112 112 If no such item exists, return None.
113 113 """
114 114 raise NotImplementedError(b"method must be implemented by subclass")
115 115
116 116 def parentitem(self):
117 117 raise NotImplementedError(b"method must be implemented by subclass")
118 118
119 119 def nextitem(self, skipfolded=True):
120 120 """
121 121 Try to return the next item closest to this item, regardless of item's
122 122 type (header, hunk, or hunkline).
123 123
124 124 If skipfolded == True, and the current item is folded, then the child
125 125 items that are hidden due to folding will be skipped when determining
126 126 the next item.
127 127
128 128 If it is not possible to get the next item, return None.
129 129 """
130 130 try:
131 131 itemfolded = self.folded
132 132 except AttributeError:
133 133 itemfolded = False
134 134 if skipfolded and itemfolded:
135 135 nextitem = self.nextsibling()
136 136 if nextitem is None:
137 137 try:
138 138 nextitem = self.parentitem().nextsibling()
139 139 except AttributeError:
140 140 nextitem = None
141 141 return nextitem
142 142 else:
143 143 # try child
144 144 item = self.firstchild()
145 145 if item is not None:
146 146 return item
147 147
148 148 # else try next sibling
149 149 item = self.nextsibling()
150 150 if item is not None:
151 151 return item
152 152
153 153 try:
154 154 # else try parent's next sibling
155 155 item = self.parentitem().nextsibling()
156 156 if item is not None:
157 157 return item
158 158
159 159 # else return grandparent's next sibling (or None)
160 160 return self.parentitem().parentitem().nextsibling()
161 161
162 162 except AttributeError: # parent and/or grandparent was None
163 163 return None
164 164
165 165 def previtem(self):
166 166 """
167 167 Try to return the previous item closest to this item, regardless of
168 168 item's type (header, hunk, or hunkline).
169 169
170 170 If it is not possible to get the previous item, return None.
171 171 """
172 172 # try previous sibling's last child's last child,
173 173 # else try previous sibling's last child, else try previous sibling
174 174 prevsibling = self.prevsibling()
175 175 if prevsibling is not None:
176 176 prevsiblinglastchild = prevsibling.lastchild()
177 177 if (prevsiblinglastchild is not None) and not prevsibling.folded:
178 178 prevsiblinglclc = prevsiblinglastchild.lastchild()
179 179 if (
180 180 prevsiblinglclc is not None
181 181 ) and not prevsiblinglastchild.folded:
182 182 return prevsiblinglclc
183 183 else:
184 184 return prevsiblinglastchild
185 185 else:
186 186 return prevsibling
187 187
188 188 # try parent (or None)
189 189 return self.parentitem()
190 190
191 191
192 192 class patch(patchnode, list): # todo: rename patchroot
193 193 """
194 194 list of header objects representing the patch.
195 195 """
196 196
197 197 def __init__(self, headerlist):
198 198 self.extend(headerlist)
199 199 # add parent patch object reference to each header
200 200 for header in self:
201 201 header.patch = self
202 202
203 203
204 204 class uiheader(patchnode):
205 205 """patch header
206 206
207 207 xxx shouldn't we move this to mercurial/patch.py ?
208 208 """
209 209
210 210 def __init__(self, header):
211 211 self.nonuiheader = header
212 212 # flag to indicate whether to apply this chunk
213 213 self.applied = True
214 214 # flag which only affects the status display indicating if a node's
215 215 # children are partially applied (i.e. some applied, some not).
216 216 self.partial = False
217 217
218 218 # flag to indicate whether to display as folded/unfolded to user
219 219 self.folded = True
220 220
221 221 # list of all headers in patch
222 222 self.patch = None
223 223
224 224 # flag is False if this header was ever unfolded from initial state
225 225 self.neverunfolded = True
226 226 self.hunks = [uihunk(h, self) for h in self.hunks]
227 227
228 228 def prettystr(self):
229 229 x = stringio()
230 230 self.pretty(x)
231 231 return x.getvalue()
232 232
233 233 def nextsibling(self):
234 234 numheadersinpatch = len(self.patch)
235 235 indexofthisheader = self.patch.index(self)
236 236
237 237 if indexofthisheader < numheadersinpatch - 1:
238 238 nextheader = self.patch[indexofthisheader + 1]
239 239 return nextheader
240 240 else:
241 241 return None
242 242
243 243 def prevsibling(self):
244 244 indexofthisheader = self.patch.index(self)
245 245 if indexofthisheader > 0:
246 246 previousheader = self.patch[indexofthisheader - 1]
247 247 return previousheader
248 248 else:
249 249 return None
250 250
251 251 def parentitem(self):
252 252 """
253 253 there is no 'real' parent item of a header that can be selected,
254 254 so return None.
255 255 """
256 256 return None
257 257
258 258 def firstchild(self):
259 259 """return the first child of this item, if one exists. otherwise
260 260 None."""
261 261 if len(self.hunks) > 0:
262 262 return self.hunks[0]
263 263 else:
264 264 return None
265 265
266 266 def lastchild(self):
267 267 """return the last child of this item, if one exists. otherwise
268 268 None."""
269 269 if len(self.hunks) > 0:
270 270 return self.hunks[-1]
271 271 else:
272 272 return None
273 273
274 274 def allchildren(self):
275 275 """return a list of all of the direct children of this node"""
276 276 return self.hunks
277 277
278 278 def __getattr__(self, name):
279 279 return getattr(self.nonuiheader, name)
280 280
281 281
282 282 class uihunkline(patchnode):
283 283 """represents a changed line in a hunk"""
284 284
285 285 def __init__(self, linetext, hunk):
286 286 self.linetext = linetext
287 287 self.applied = True
288 288 # the parent hunk to which this line belongs
289 289 self.hunk = hunk
290 290 # folding lines currently is not used/needed, but this flag is needed
291 291 # in the previtem method.
292 292 self.folded = False
293 293
294 294 def prettystr(self):
295 295 return self.linetext
296 296
297 297 def nextsibling(self):
298 298 numlinesinhunk = len(self.hunk.changedlines)
299 299 indexofthisline = self.hunk.changedlines.index(self)
300 300
301 301 if indexofthisline < numlinesinhunk - 1:
302 302 nextline = self.hunk.changedlines[indexofthisline + 1]
303 303 return nextline
304 304 else:
305 305 return None
306 306
307 307 def prevsibling(self):
308 308 indexofthisline = self.hunk.changedlines.index(self)
309 309 if indexofthisline > 0:
310 310 previousline = self.hunk.changedlines[indexofthisline - 1]
311 311 return previousline
312 312 else:
313 313 return None
314 314
315 315 def parentitem(self):
316 316 """return the parent to the current item"""
317 317 return self.hunk
318 318
319 319 def firstchild(self):
320 320 """return the first child of this item, if one exists. otherwise
321 321 None."""
322 322 # hunk-lines don't have children
323 323 return None
324 324
325 325 def lastchild(self):
326 326 """return the last child of this item, if one exists. otherwise
327 327 None."""
328 328 # hunk-lines don't have children
329 329 return None
330 330
331 331
332 332 class uihunk(patchnode):
333 333 """ui patch hunk, wraps a hunk and keep track of ui behavior """
334 334
335 335 maxcontext = 3
336 336
337 337 def __init__(self, hunk, header):
338 338 self._hunk = hunk
339 339 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
340 340 self.header = header
341 341 # used at end for detecting how many removed lines were un-applied
342 342 self.originalremoved = self.removed
343 343
344 344 # flag to indicate whether to display as folded/unfolded to user
345 345 self.folded = True
346 346 # flag to indicate whether to apply this chunk
347 347 self.applied = True
348 348 # flag which only affects the status display indicating if a node's
349 349 # children are partially applied (i.e. some applied, some not).
350 350 self.partial = False
351 351
352 352 def nextsibling(self):
353 353 numhunksinheader = len(self.header.hunks)
354 354 indexofthishunk = self.header.hunks.index(self)
355 355
356 356 if indexofthishunk < numhunksinheader - 1:
357 357 nexthunk = self.header.hunks[indexofthishunk + 1]
358 358 return nexthunk
359 359 else:
360 360 return None
361 361
362 362 def prevsibling(self):
363 363 indexofthishunk = self.header.hunks.index(self)
364 364 if indexofthishunk > 0:
365 365 previoushunk = self.header.hunks[indexofthishunk - 1]
366 366 return previoushunk
367 367 else:
368 368 return None
369 369
370 370 def parentitem(self):
371 371 """return the parent to the current item"""
372 372 return self.header
373 373
374 374 def firstchild(self):
375 375 """return the first child of this item, if one exists. otherwise
376 376 None."""
377 377 if len(self.changedlines) > 0:
378 378 return self.changedlines[0]
379 379 else:
380 380 return None
381 381
382 382 def lastchild(self):
383 383 """return the last child of this item, if one exists. otherwise
384 384 None."""
385 385 if len(self.changedlines) > 0:
386 386 return self.changedlines[-1]
387 387 else:
388 388 return None
389 389
390 390 def allchildren(self):
391 391 """return a list of all of the direct children of this node"""
392 392 return self.changedlines
393 393
394 394 def countchanges(self):
395 395 """changedlines -> (n+,n-)"""
396 396 add = len(
397 397 [
398 398 l
399 399 for l in self.changedlines
400 400 if l.applied and l.prettystr().startswith(b'+')
401 401 ]
402 402 )
403 403 rem = len(
404 404 [
405 405 l
406 406 for l in self.changedlines
407 407 if l.applied and l.prettystr().startswith(b'-')
408 408 ]
409 409 )
410 410 return add, rem
411 411
412 412 def getfromtoline(self):
413 413 # calculate the number of removed lines converted to context lines
414 414 removedconvertedtocontext = self.originalremoved - self.removed
415 415
416 416 contextlen = (
417 417 len(self.before) + len(self.after) + removedconvertedtocontext
418 418 )
419 419 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
420 420 contextlen -= 1
421 421 fromlen = contextlen + self.removed
422 422 tolen = contextlen + self.added
423 423
424 424 # diffutils manual, section "2.2.2.2 detailed description of unified
425 425 # format": "an empty hunk is considered to end at the line that
426 426 # precedes the hunk."
427 427 #
428 428 # so, if either of hunks is empty, decrease its line start. --immerrr
429 429 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
430 430 fromline, toline = self.fromline, self.toline
431 431 if fromline != 0:
432 432 if fromlen == 0:
433 433 fromline -= 1
434 434 if tolen == 0 and toline > 0:
435 435 toline -= 1
436 436
437 437 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
438 438 fromline,
439 439 fromlen,
440 440 toline,
441 441 tolen,
442 442 self.proc and (b' ' + self.proc),
443 443 )
444 444 return fromtoline
445 445
446 446 def write(self, fp):
447 447 # updated self.added/removed, which are used by getfromtoline()
448 448 self.added, self.removed = self.countchanges()
449 449 fp.write(self.getfromtoline())
450 450
451 451 hunklinelist = []
452 452 # add the following to the list: (1) all applied lines, and
453 453 # (2) all unapplied removal lines (convert these to context lines)
454 454 for changedline in self.changedlines:
455 455 changedlinestr = changedline.prettystr()
456 456 if changedline.applied:
457 457 hunklinelist.append(changedlinestr)
458 458 elif changedlinestr.startswith(b"-"):
459 459 hunklinelist.append(b" " + changedlinestr[1:])
460 460
461 461 fp.write(b''.join(self.before + hunklinelist + self.after))
462 462
463 463 pretty = write
464 464
465 465 def prettystr(self):
466 466 x = stringio()
467 467 self.pretty(x)
468 468 return x.getvalue()
469 469
470 470 def reversehunk(self):
471 471 """return a recordhunk which is the reverse of the hunk
472 472
473 473 Assuming the displayed patch is diff(A, B) result. The returned hunk is
474 474 intended to be applied to B, instead of A.
475 475
476 476 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
477 477 the user made the following selection:
478 478
479 479 0
480 480 [x] -1 [x]: selected
481 481 [ ] -2 [ ]: not selected
482 482 [x] +3
483 483 [ ] +4
484 484 [x] +5
485 485 6
486 486
487 487 This function returns a hunk like:
488 488
489 489 0
490 490 -3
491 491 -4
492 492 -5
493 493 +1
494 494 +4
495 495 6
496 496
497 497 Note "4" was first deleted then added. That's because "4" exists in B
498 498 side and "-4" must exist between "-3" and "-5" to make the patch
499 499 applicable to B.
500 500 """
501 501 dels = []
502 502 adds = []
503 noeol = False
503 504 for line in self.changedlines:
504 505 text = line.linetext
506 if line.linetext == b'\\ No newline at end of file\n':
507 noeol = True
508 break
505 509 if line.applied:
506 510 if text.startswith(b'+'):
507 511 dels.append(text[1:])
508 512 elif text.startswith(b'-'):
509 513 adds.append(text[1:])
510 514 elif text.startswith(b'+'):
511 515 dels.append(text[1:])
512 516 adds.append(text[1:])
513 517 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
518 if noeol and hunk:
519 # Remove the newline from the end of the hunk.
520 hunk[-1] = hunk[-1][:-1]
514 521 h = self._hunk
515 522 return patchmod.recordhunk(
516 523 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
517 524 )
518 525
519 526 def __getattr__(self, name):
520 527 return getattr(self._hunk, name)
521 528
522 529 def __repr__(self):
523 530 return '<hunk %r@%d>' % (self.filename(), self.fromline)
524 531
525 532
526 533 def filterpatch(ui, chunks, chunkselector, operation=None):
527 534 """interactively filter patch chunks into applied-only chunks"""
528 535 chunks = list(chunks)
529 536 # convert chunks list into structure suitable for displaying/modifying
530 537 # with curses. create a list of headers only.
531 538 headers = [c for c in chunks if isinstance(c, patchmod.header)]
532 539
533 540 # if there are no changed files
534 541 if len(headers) == 0:
535 542 return [], {}
536 543 uiheaders = [uiheader(h) for h in headers]
537 544 # let user choose headers/hunks/lines, and mark their applied flags
538 545 # accordingly
539 546 ret = chunkselector(ui, uiheaders, operation=operation)
540 547 appliedhunklist = []
541 548 for hdr in uiheaders:
542 549 if hdr.applied and (
543 550 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
544 551 ):
545 552 appliedhunklist.append(hdr)
546 553 fixoffset = 0
547 554 for hnk in hdr.hunks:
548 555 if hnk.applied:
549 556 appliedhunklist.append(hnk)
550 557 # adjust the 'to'-line offset of the hunk to be correct
551 558 # after de-activating some of the other hunks for this file
552 559 if fixoffset:
553 560 # hnk = copy.copy(hnk) # necessary??
554 561 hnk.toline += fixoffset
555 562 else:
556 563 fixoffset += hnk.removed - hnk.added
557 564
558 565 return (appliedhunklist, ret)
559 566
560 567
561 568 def chunkselector(ui, headerlist, operation=None):
562 569 """
563 570 curses interface to get selection of chunks, and mark the applied flags
564 571 of the chosen chunks.
565 572 """
566 573 ui.write(_(b'starting interactive selection\n'))
567 574 chunkselector = curseschunkselector(headerlist, ui, operation)
568 575 origsigtstp = sentinel = object()
569 576 if util.safehasattr(signal, b'SIGTSTP'):
570 577 origsigtstp = signal.getsignal(signal.SIGTSTP)
571 578 try:
572 579 with util.with_lc_ctype():
573 580 curses.wrapper(chunkselector.main)
574 581 if chunkselector.initexc is not None:
575 582 raise chunkselector.initexc
576 583 # ncurses does not restore signal handler for SIGTSTP
577 584 finally:
578 585 if origsigtstp is not sentinel:
579 586 signal.signal(signal.SIGTSTP, origsigtstp)
580 587 return chunkselector.opts
581 588
582 589
583 590 def testdecorator(testfn, f):
584 591 def u(*args, **kwargs):
585 592 return f(testfn, *args, **kwargs)
586 593
587 594 return u
588 595
589 596
590 597 def testchunkselector(testfn, ui, headerlist, operation=None):
591 598 """
592 599 test interface to get selection of chunks, and mark the applied flags
593 600 of the chosen chunks.
594 601 """
595 602 chunkselector = curseschunkselector(headerlist, ui, operation)
596 603
597 604 class dummystdscr(object):
598 605 def clear(self):
599 606 pass
600 607
601 608 def refresh(self):
602 609 pass
603 610
604 611 chunkselector.stdscr = dummystdscr()
605 612 if testfn and os.path.exists(testfn):
606 613 testf = open(testfn, 'r')
607 614 testcommands = [x.rstrip('\n') for x in testf.readlines()]
608 615 testf.close()
609 616 while True:
610 617 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
611 618 break
612 619 return chunkselector.opts
613 620
614 621
615 622 _headermessages = { # {operation: text}
616 623 b'apply': _(b'Select hunks to apply'),
617 624 b'discard': _(b'Select hunks to discard'),
618 625 b'keep': _(b'Select hunks to keep'),
619 626 None: _(b'Select hunks to record'),
620 627 }
621 628
622 629
623 630 class curseschunkselector(object):
624 631 def __init__(self, headerlist, ui, operation=None):
625 632 # put the headers into a patch object
626 633 self.headerlist = patch(headerlist)
627 634
628 635 self.ui = ui
629 636 self.opts = {}
630 637
631 638 self.errorstr = None
632 639 # list of all chunks
633 640 self.chunklist = []
634 641 for h in headerlist:
635 642 self.chunklist.append(h)
636 643 self.chunklist.extend(h.hunks)
637 644
638 645 # dictionary mapping (fgcolor, bgcolor) pairs to the
639 646 # corresponding curses color-pair value.
640 647 self.colorpairs = {}
641 648 # maps custom nicknames of color-pairs to curses color-pair values
642 649 self.colorpairnames = {}
643 650
644 651 # Honor color setting of ui section. Keep colored setup as
645 652 # long as not explicitly set to a falsy value - especially,
646 653 # when not set at all. This is to stay most compatible with
647 654 # previous (color only) behaviour.
648 655 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
649 656 self.usecolor = uicolor is not False
650 657
651 658 # the currently selected header, hunk, or hunk-line
652 659 self.currentselecteditem = self.headerlist[0]
653 660 self.lastapplieditem = None
654 661
655 662 # updated when printing out patch-display -- the 'lines' here are the
656 663 # line positions *in the pad*, not on the screen.
657 664 self.selecteditemstartline = 0
658 665 self.selecteditemendline = None
659 666
660 667 # define indentation levels
661 668 self.headerindentnumchars = 0
662 669 self.hunkindentnumchars = 3
663 670 self.hunklineindentnumchars = 6
664 671
665 672 # the first line of the pad to print to the screen
666 673 self.firstlineofpadtoprint = 0
667 674
668 675 # keeps track of the number of lines in the pad
669 676 self.numpadlines = None
670 677
671 678 self.numstatuslines = 1
672 679
673 680 # keep a running count of the number of lines printed to the pad
674 681 # (used for determining when the selected item begins/ends)
675 682 self.linesprintedtopadsofar = 0
676 683
677 684 # stores optional text for a commit comment provided by the user
678 685 self.commenttext = b""
679 686
680 687 # if the last 'toggle all' command caused all changes to be applied
681 688 self.waslasttoggleallapplied = True
682 689
683 690 # affects some ui text
684 691 if operation not in _headermessages:
685 692 raise error.ProgrammingError(
686 693 b'unexpected operation: %s' % operation
687 694 )
688 695 self.operation = operation
689 696
690 697 def uparrowevent(self):
691 698 """
692 699 try to select the previous item to the current item that has the
693 700 most-indented level. for example, if a hunk is selected, try to select
694 701 the last hunkline of the hunk prior to the selected hunk. or, if
695 702 the first hunkline of a hunk is currently selected, then select the
696 703 hunk itself.
697 704 """
698 705 currentitem = self.currentselecteditem
699 706
700 707 nextitem = currentitem.previtem()
701 708
702 709 if nextitem is None:
703 710 # if no parent item (i.e. currentitem is the first header), then
704 711 # no change...
705 712 nextitem = currentitem
706 713
707 714 self.currentselecteditem = nextitem
708 715
709 716 def uparrowshiftevent(self):
710 717 """
711 718 select (if possible) the previous item on the same level as the
712 719 currently selected item. otherwise, select (if possible) the
713 720 parent-item of the currently selected item.
714 721 """
715 722 currentitem = self.currentselecteditem
716 723 nextitem = currentitem.prevsibling()
717 724 # if there's no previous sibling, try choosing the parent
718 725 if nextitem is None:
719 726 nextitem = currentitem.parentitem()
720 727 if nextitem is None:
721 728 # if no parent item (i.e. currentitem is the first header), then
722 729 # no change...
723 730 nextitem = currentitem
724 731
725 732 self.currentselecteditem = nextitem
726 733 self.recenterdisplayedarea()
727 734
728 735 def downarrowevent(self):
729 736 """
730 737 try to select the next item to the current item that has the
731 738 most-indented level. for example, if a hunk is selected, select
732 739 the first hunkline of the selected hunk. or, if the last hunkline of
733 740 a hunk is currently selected, then select the next hunk, if one exists,
734 741 or if not, the next header if one exists.
735 742 """
736 743 # self.startprintline += 1 #debug
737 744 currentitem = self.currentselecteditem
738 745
739 746 nextitem = currentitem.nextitem()
740 747 # if there's no next item, keep the selection as-is
741 748 if nextitem is None:
742 749 nextitem = currentitem
743 750
744 751 self.currentselecteditem = nextitem
745 752
746 753 def downarrowshiftevent(self):
747 754 """
748 755 select (if possible) the next item on the same level as the currently
749 756 selected item. otherwise, select (if possible) the next item on the
750 757 same level as the parent item of the currently selected item.
751 758 """
752 759 currentitem = self.currentselecteditem
753 760 nextitem = currentitem.nextsibling()
754 761 # if there's no next sibling, try choosing the parent's nextsibling
755 762 if nextitem is None:
756 763 try:
757 764 nextitem = currentitem.parentitem().nextsibling()
758 765 except AttributeError:
759 766 # parentitem returned None, so nextsibling() can't be called
760 767 nextitem = None
761 768 if nextitem is None:
762 769 # if parent has no next sibling, then no change...
763 770 nextitem = currentitem
764 771
765 772 self.currentselecteditem = nextitem
766 773 self.recenterdisplayedarea()
767 774
768 775 def nextsametype(self, test=False):
769 776 currentitem = self.currentselecteditem
770 777 sametype = lambda item: isinstance(item, type(currentitem))
771 778 nextitem = currentitem.nextitem()
772 779
773 780 while nextitem is not None and not sametype(nextitem):
774 781 nextitem = nextitem.nextitem()
775 782
776 783 if nextitem is None:
777 784 nextitem = currentitem
778 785 else:
779 786 parent = nextitem.parentitem()
780 787 if parent is not None and parent.folded:
781 788 self.togglefolded(parent)
782 789
783 790 self.currentselecteditem = nextitem
784 791 if not test:
785 792 self.recenterdisplayedarea()
786 793
787 794 def rightarrowevent(self):
788 795 """
789 796 select (if possible) the first of this item's child-items.
790 797 """
791 798 currentitem = self.currentselecteditem
792 799 nextitem = currentitem.firstchild()
793 800
794 801 # turn off folding if we want to show a child-item
795 802 if currentitem.folded:
796 803 self.togglefolded(currentitem)
797 804
798 805 if nextitem is None:
799 806 # if no next item on parent-level, then no change...
800 807 nextitem = currentitem
801 808
802 809 self.currentselecteditem = nextitem
803 810
804 811 def leftarrowevent(self):
805 812 """
806 813 if the current item can be folded (i.e. it is an unfolded header or
807 814 hunk), then fold it. otherwise try select (if possible) the parent
808 815 of this item.
809 816 """
810 817 currentitem = self.currentselecteditem
811 818
812 819 # try to fold the item
813 820 if not isinstance(currentitem, uihunkline):
814 821 if not currentitem.folded:
815 822 self.togglefolded(item=currentitem)
816 823 return
817 824
818 825 # if it can't be folded, try to select the parent item
819 826 nextitem = currentitem.parentitem()
820 827
821 828 if nextitem is None:
822 829 # if no item on parent-level, then no change...
823 830 nextitem = currentitem
824 831 if not nextitem.folded:
825 832 self.togglefolded(item=nextitem)
826 833
827 834 self.currentselecteditem = nextitem
828 835
829 836 def leftarrowshiftevent(self):
830 837 """
831 838 select the header of the current item (or fold current item if the
832 839 current item is already a header).
833 840 """
834 841 currentitem = self.currentselecteditem
835 842
836 843 if isinstance(currentitem, uiheader):
837 844 if not currentitem.folded:
838 845 self.togglefolded(item=currentitem)
839 846 return
840 847
841 848 # select the parent item recursively until we're at a header
842 849 while True:
843 850 nextitem = currentitem.parentitem()
844 851 if nextitem is None:
845 852 break
846 853 else:
847 854 currentitem = nextitem
848 855
849 856 self.currentselecteditem = currentitem
850 857
851 858 def updatescroll(self):
852 859 """scroll the screen to fully show the currently-selected"""
853 860 selstart = self.selecteditemstartline
854 861 selend = self.selecteditemendline
855 862
856 863 padstart = self.firstlineofpadtoprint
857 864 padend = padstart + self.yscreensize - self.numstatuslines - 1
858 865 # 'buffered' pad start/end values which scroll with a certain
859 866 # top/bottom context margin
860 867 padstartbuffered = padstart + 3
861 868 padendbuffered = padend - 3
862 869
863 870 if selend > padendbuffered:
864 871 self.scrolllines(selend - padendbuffered)
865 872 elif selstart < padstartbuffered:
866 873 # negative values scroll in pgup direction
867 874 self.scrolllines(selstart - padstartbuffered)
868 875
869 876 def scrolllines(self, numlines):
870 877 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
871 878 self.firstlineofpadtoprint += numlines
872 879 if self.firstlineofpadtoprint < 0:
873 880 self.firstlineofpadtoprint = 0
874 881 if self.firstlineofpadtoprint > self.numpadlines - 1:
875 882 self.firstlineofpadtoprint = self.numpadlines - 1
876 883
877 884 def toggleapply(self, item=None):
878 885 """
879 886 toggle the applied flag of the specified item. if no item is specified,
880 887 toggle the flag of the currently selected item.
881 888 """
882 889 if item is None:
883 890 item = self.currentselecteditem
884 891 # Only set this when NOT using 'toggleall'
885 892 self.lastapplieditem = item
886 893
887 894 item.applied = not item.applied
888 895
889 896 if isinstance(item, uiheader):
890 897 item.partial = False
891 898 if item.applied:
892 899 # apply all its hunks
893 900 for hnk in item.hunks:
894 901 hnk.applied = True
895 902 # apply all their hunklines
896 903 for hunkline in hnk.changedlines:
897 904 hunkline.applied = True
898 905 else:
899 906 # un-apply all its hunks
900 907 for hnk in item.hunks:
901 908 hnk.applied = False
902 909 hnk.partial = False
903 910 # un-apply all their hunklines
904 911 for hunkline in hnk.changedlines:
905 912 hunkline.applied = False
906 913 elif isinstance(item, uihunk):
907 914 item.partial = False
908 915 # apply all it's hunklines
909 916 for hunkline in item.changedlines:
910 917 hunkline.applied = item.applied
911 918
912 919 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
913 920 allsiblingsapplied = not (False in siblingappliedstatus)
914 921 nosiblingsapplied = not (True in siblingappliedstatus)
915 922
916 923 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
917 924 somesiblingspartial = True in siblingspartialstatus
918 925
919 926 # cases where applied or partial should be removed from header
920 927
921 928 # if no 'sibling' hunks are applied (including this hunk)
922 929 if nosiblingsapplied:
923 930 if not item.header.special():
924 931 item.header.applied = False
925 932 item.header.partial = False
926 933 else: # some/all parent siblings are applied
927 934 item.header.applied = True
928 935 item.header.partial = (
929 936 somesiblingspartial or not allsiblingsapplied
930 937 )
931 938
932 939 elif isinstance(item, uihunkline):
933 940 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
934 941 allsiblingsapplied = not (False in siblingappliedstatus)
935 942 nosiblingsapplied = not (True in siblingappliedstatus)
936 943
937 944 # if no 'sibling' lines are applied
938 945 if nosiblingsapplied:
939 946 item.hunk.applied = False
940 947 item.hunk.partial = False
941 948 elif allsiblingsapplied:
942 949 item.hunk.applied = True
943 950 item.hunk.partial = False
944 951 else: # some siblings applied
945 952 item.hunk.applied = True
946 953 item.hunk.partial = True
947 954
948 955 parentsiblingsapplied = [
949 956 hnk.applied for hnk in item.hunk.header.hunks
950 957 ]
951 958 noparentsiblingsapplied = not (True in parentsiblingsapplied)
952 959 allparentsiblingsapplied = not (False in parentsiblingsapplied)
953 960
954 961 parentsiblingspartial = [
955 962 hnk.partial for hnk in item.hunk.header.hunks
956 963 ]
957 964 someparentsiblingspartial = True in parentsiblingspartial
958 965
959 966 # if all parent hunks are not applied, un-apply header
960 967 if noparentsiblingsapplied:
961 968 if not item.hunk.header.special():
962 969 item.hunk.header.applied = False
963 970 item.hunk.header.partial = False
964 971 # set the applied and partial status of the header if needed
965 972 else: # some/all parent siblings are applied
966 973 item.hunk.header.applied = True
967 974 item.hunk.header.partial = (
968 975 someparentsiblingspartial or not allparentsiblingsapplied
969 976 )
970 977
971 978 def toggleall(self):
972 979 """toggle the applied flag of all items."""
973 980 if self.waslasttoggleallapplied: # then unapply them this time
974 981 for item in self.headerlist:
975 982 if item.applied:
976 983 self.toggleapply(item)
977 984 else:
978 985 for item in self.headerlist:
979 986 if not item.applied:
980 987 self.toggleapply(item)
981 988 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
982 989
983 990 def flipselections(self):
984 991 """
985 992 Flip all selections. Every selected line is unselected and vice
986 993 versa.
987 994 """
988 995 for header in self.headerlist:
989 996 for hunk in header.allchildren():
990 997 for line in hunk.allchildren():
991 998 self.toggleapply(line)
992 999
993 1000 def toggleallbetween(self):
994 1001 """toggle applied on or off for all items in range [lastapplied,
995 1002 current]. """
996 1003 if (
997 1004 not self.lastapplieditem
998 1005 or self.currentselecteditem == self.lastapplieditem
999 1006 ):
1000 1007 # Treat this like a normal 'x'/' '
1001 1008 self.toggleapply()
1002 1009 return
1003 1010
1004 1011 startitem = self.lastapplieditem
1005 1012 enditem = self.currentselecteditem
1006 1013 # Verify that enditem is "after" startitem, otherwise swap them.
1007 1014 for direction in [b'forward', b'reverse']:
1008 1015 nextitem = startitem.nextitem()
1009 1016 while nextitem and nextitem != enditem:
1010 1017 nextitem = nextitem.nextitem()
1011 1018 if nextitem:
1012 1019 break
1013 1020 # Looks like we went the wrong direction :)
1014 1021 startitem, enditem = enditem, startitem
1015 1022
1016 1023 if not nextitem:
1017 1024 # We didn't find a path going either forward or backward? Don't know
1018 1025 # how this can happen, let's not crash though.
1019 1026 return
1020 1027
1021 1028 nextitem = startitem
1022 1029 # Switch all items to be the opposite state of the currently selected
1023 1030 # item. Specifically:
1024 1031 # [ ] startitem
1025 1032 # [x] middleitem
1026 1033 # [ ] enditem <-- currently selected
1027 1034 # This will turn all three on, since the currently selected item is off.
1028 1035 # This does *not* invert each item (i.e. middleitem stays marked/on)
1029 1036 desiredstate = not self.currentselecteditem.applied
1030 1037 while nextitem != enditem.nextitem():
1031 1038 if nextitem.applied != desiredstate:
1032 1039 self.toggleapply(item=nextitem)
1033 1040 nextitem = nextitem.nextitem()
1034 1041
1035 1042 def togglefolded(self, item=None, foldparent=False):
1036 1043 """toggle folded flag of specified item (defaults to currently
1037 1044 selected)"""
1038 1045 if item is None:
1039 1046 item = self.currentselecteditem
1040 1047 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1041 1048 if not isinstance(item, uiheader):
1042 1049 # we need to select the parent item in this case
1043 1050 self.currentselecteditem = item = item.parentitem()
1044 1051 elif item.neverunfolded:
1045 1052 item.neverunfolded = False
1046 1053
1047 1054 # also fold any foldable children of the parent/current item
1048 1055 if isinstance(item, uiheader): # the original or 'new' item
1049 1056 for child in item.allchildren():
1050 1057 child.folded = not item.folded
1051 1058
1052 1059 if isinstance(item, (uiheader, uihunk)):
1053 1060 item.folded = not item.folded
1054 1061
1055 1062 def alignstring(self, instr, window):
1056 1063 """
1057 1064 add whitespace to the end of a string in order to make it fill
1058 1065 the screen in the x direction. the current cursor position is
1059 1066 taken into account when making this calculation. the string can span
1060 1067 multiple lines.
1061 1068 """
1062 1069 y, xstart = window.getyx()
1063 1070 width = self.xscreensize
1064 1071 # turn tabs into spaces
1065 1072 instr = instr.expandtabs(4)
1066 1073 strwidth = encoding.colwidth(instr)
1067 1074 numspaces = width - ((strwidth + xstart) % width)
1068 1075 return instr + b" " * numspaces
1069 1076
1070 1077 def printstring(
1071 1078 self,
1072 1079 window,
1073 1080 text,
1074 1081 fgcolor=None,
1075 1082 bgcolor=None,
1076 1083 pair=None,
1077 1084 pairname=None,
1078 1085 attrlist=None,
1079 1086 towin=True,
1080 1087 align=True,
1081 1088 showwhtspc=False,
1082 1089 ):
1083 1090 """
1084 1091 print the string, text, with the specified colors and attributes, to
1085 1092 the specified curses window object.
1086 1093
1087 1094 the foreground and background colors are of the form
1088 1095 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1089 1096 magenta, red, white, yellow]. if pairname is provided, a color
1090 1097 pair will be looked up in the self.colorpairnames dictionary.
1091 1098
1092 1099 attrlist is a list containing text attributes in the form of
1093 1100 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1094 1101 underline].
1095 1102
1096 1103 if align == True, whitespace is added to the printed string such that
1097 1104 the string stretches to the right border of the window.
1098 1105
1099 1106 if showwhtspc == True, trailing whitespace of a string is highlighted.
1100 1107 """
1101 1108 # preprocess the text, converting tabs to spaces
1102 1109 text = text.expandtabs(4)
1103 1110 # strip \n, and convert control characters to ^[char] representation
1104 1111 text = re.sub(
1105 1112 br'[\x00-\x08\x0a-\x1f]',
1106 1113 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1107 1114 text.strip(b'\n'),
1108 1115 )
1109 1116
1110 1117 if pair is not None:
1111 1118 colorpair = pair
1112 1119 elif pairname is not None:
1113 1120 colorpair = self.colorpairnames[pairname]
1114 1121 else:
1115 1122 if fgcolor is None:
1116 1123 fgcolor = -1
1117 1124 if bgcolor is None:
1118 1125 bgcolor = -1
1119 1126 if (fgcolor, bgcolor) in self.colorpairs:
1120 1127 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1121 1128 else:
1122 1129 colorpair = self.getcolorpair(fgcolor, bgcolor)
1123 1130 # add attributes if possible
1124 1131 if attrlist is None:
1125 1132 attrlist = []
1126 1133 if colorpair < 256:
1127 1134 # then it is safe to apply all attributes
1128 1135 for textattr in attrlist:
1129 1136 colorpair |= textattr
1130 1137 else:
1131 1138 # just apply a select few (safe?) attributes
1132 1139 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1133 1140 if textattr in attrlist:
1134 1141 colorpair |= textattr
1135 1142
1136 1143 y, xstart = self.chunkpad.getyx()
1137 1144 t = b"" # variable for counting lines printed
1138 1145 # if requested, show trailing whitespace
1139 1146 if showwhtspc:
1140 1147 origlen = len(text)
1141 1148 text = text.rstrip(b' \n') # tabs have already been expanded
1142 1149 strippedlen = len(text)
1143 1150 numtrailingspaces = origlen - strippedlen
1144 1151
1145 1152 if towin:
1146 1153 window.addstr(text, colorpair)
1147 1154 t += text
1148 1155
1149 1156 if showwhtspc:
1150 1157 wscolorpair = colorpair | curses.A_REVERSE
1151 1158 if towin:
1152 1159 for i in range(numtrailingspaces):
1153 1160 window.addch(curses.ACS_CKBOARD, wscolorpair)
1154 1161 t += b" " * numtrailingspaces
1155 1162
1156 1163 if align:
1157 1164 if towin:
1158 1165 extrawhitespace = self.alignstring(b"", window)
1159 1166 window.addstr(extrawhitespace, colorpair)
1160 1167 else:
1161 1168 # need to use t, since the x position hasn't incremented
1162 1169 extrawhitespace = self.alignstring(t, window)
1163 1170 t += extrawhitespace
1164 1171
1165 1172 # is reset to 0 at the beginning of printitem()
1166 1173
1167 1174 linesprinted = (xstart + len(t)) // self.xscreensize
1168 1175 self.linesprintedtopadsofar += linesprinted
1169 1176 return t
1170 1177
1171 1178 def _getstatuslinesegments(self):
1172 1179 """-> [str]. return segments"""
1173 1180 selected = self.currentselecteditem.applied
1174 1181 spaceselect = _(b'space/enter: select')
1175 1182 spacedeselect = _(b'space/enter: deselect')
1176 1183 # Format the selected label into a place as long as the longer of the
1177 1184 # two possible labels. This may vary by language.
1178 1185 spacelen = max(len(spaceselect), len(spacedeselect))
1179 1186 selectedlabel = b'%-*s' % (
1180 1187 spacelen,
1181 1188 spacedeselect if selected else spaceselect,
1182 1189 )
1183 1190 segments = [
1184 1191 _headermessages[self.operation],
1185 1192 b'-',
1186 1193 _(b'[x]=selected **=collapsed'),
1187 1194 _(b'c: confirm'),
1188 1195 _(b'q: abort'),
1189 1196 _(b'arrow keys: move/expand/collapse'),
1190 1197 selectedlabel,
1191 1198 _(b'?: help'),
1192 1199 ]
1193 1200 return segments
1194 1201
1195 1202 def _getstatuslines(self):
1196 1203 """() -> [str]. return short help used in the top status window"""
1197 1204 if self.errorstr is not None:
1198 1205 lines = [self.errorstr, _(b'Press any key to continue')]
1199 1206 else:
1200 1207 # wrap segments to lines
1201 1208 segments = self._getstatuslinesegments()
1202 1209 width = self.xscreensize
1203 1210 lines = []
1204 1211 lastwidth = width
1205 1212 for s in segments:
1206 1213 w = encoding.colwidth(s)
1207 1214 sep = b' ' * (1 + (s and s[0] not in b'-['))
1208 1215 if lastwidth + w + len(sep) >= width:
1209 1216 lines.append(s)
1210 1217 lastwidth = w
1211 1218 else:
1212 1219 lines[-1] += sep + s
1213 1220 lastwidth += w + len(sep)
1214 1221 if len(lines) != self.numstatuslines:
1215 1222 self.numstatuslines = len(lines)
1216 1223 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1217 1224 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1218 1225
1219 1226 def updatescreen(self):
1220 1227 self.statuswin.erase()
1221 1228 self.chunkpad.erase()
1222 1229
1223 1230 printstring = self.printstring
1224 1231
1225 1232 # print out the status lines at the top
1226 1233 try:
1227 1234 for line in self._getstatuslines():
1228 1235 printstring(self.statuswin, line, pairname=b"legend")
1229 1236 self.statuswin.refresh()
1230 1237 except curses.error:
1231 1238 pass
1232 1239 if self.errorstr is not None:
1233 1240 return
1234 1241
1235 1242 # print out the patch in the remaining part of the window
1236 1243 try:
1237 1244 self.printitem()
1238 1245 self.updatescroll()
1239 1246 self.chunkpad.refresh(
1240 1247 self.firstlineofpadtoprint,
1241 1248 0,
1242 1249 self.numstatuslines,
1243 1250 0,
1244 1251 self.yscreensize - self.numstatuslines,
1245 1252 self.xscreensize,
1246 1253 )
1247 1254 except curses.error:
1248 1255 pass
1249 1256
1250 1257 def getstatusprefixstring(self, item):
1251 1258 """
1252 1259 create a string to prefix a line with which indicates whether 'item'
1253 1260 is applied and/or folded.
1254 1261 """
1255 1262
1256 1263 # create checkbox string
1257 1264 if item.applied:
1258 1265 if not isinstance(item, uihunkline) and item.partial:
1259 1266 checkbox = b"[~]"
1260 1267 else:
1261 1268 checkbox = b"[x]"
1262 1269 else:
1263 1270 checkbox = b"[ ]"
1264 1271
1265 1272 try:
1266 1273 if item.folded:
1267 1274 checkbox += b"**"
1268 1275 if isinstance(item, uiheader):
1269 1276 # one of "m", "a", or "d" (modified, added, deleted)
1270 1277 filestatus = item.changetype
1271 1278
1272 1279 checkbox += filestatus + b" "
1273 1280 else:
1274 1281 checkbox += b" "
1275 1282 if isinstance(item, uiheader):
1276 1283 # add two more spaces for headers
1277 1284 checkbox += b" "
1278 1285 except AttributeError: # not foldable
1279 1286 checkbox += b" "
1280 1287
1281 1288 return checkbox
1282 1289
1283 1290 def printheader(
1284 1291 self, header, selected=False, towin=True, ignorefolding=False
1285 1292 ):
1286 1293 """
1287 1294 print the header to the pad. if countlines is True, don't print
1288 1295 anything, but just count the number of lines which would be printed.
1289 1296 """
1290 1297
1291 1298 outstr = b""
1292 1299 text = header.prettystr()
1293 1300 chunkindex = self.chunklist.index(header)
1294 1301
1295 1302 if chunkindex != 0 and not header.folded:
1296 1303 # add separating line before headers
1297 1304 outstr += self.printstring(
1298 1305 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1299 1306 )
1300 1307 # select color-pair based on if the header is selected
1301 1308 colorpair = self.getcolorpair(
1302 1309 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1303 1310 )
1304 1311
1305 1312 # print out each line of the chunk, expanding it to screen width
1306 1313
1307 1314 # number of characters to indent lines on this level by
1308 1315 indentnumchars = 0
1309 1316 checkbox = self.getstatusprefixstring(header)
1310 1317 if not header.folded or ignorefolding:
1311 1318 textlist = text.split(b"\n")
1312 1319 linestr = checkbox + textlist[0]
1313 1320 else:
1314 1321 linestr = checkbox + header.filename()
1315 1322 outstr += self.printstring(
1316 1323 self.chunkpad, linestr, pair=colorpair, towin=towin
1317 1324 )
1318 1325 if not header.folded or ignorefolding:
1319 1326 if len(textlist) > 1:
1320 1327 for line in textlist[1:]:
1321 1328 linestr = b" " * (indentnumchars + len(checkbox)) + line
1322 1329 outstr += self.printstring(
1323 1330 self.chunkpad, linestr, pair=colorpair, towin=towin
1324 1331 )
1325 1332
1326 1333 return outstr
1327 1334
1328 1335 def printhunklinesbefore(
1329 1336 self, hunk, selected=False, towin=True, ignorefolding=False
1330 1337 ):
1331 1338 """includes start/end line indicator"""
1332 1339 outstr = b""
1333 1340 # where hunk is in list of siblings
1334 1341 hunkindex = hunk.header.hunks.index(hunk)
1335 1342
1336 1343 if hunkindex != 0:
1337 1344 # add separating line before headers
1338 1345 outstr += self.printstring(
1339 1346 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1340 1347 )
1341 1348
1342 1349 colorpair = self.getcolorpair(
1343 1350 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1344 1351 )
1345 1352
1346 1353 # print out from-to line with checkbox
1347 1354 checkbox = self.getstatusprefixstring(hunk)
1348 1355
1349 1356 lineprefix = b" " * self.hunkindentnumchars + checkbox
1350 1357 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1351 1358
1352 1359 outstr += self.printstring(
1353 1360 self.chunkpad, lineprefix, towin=towin, align=False
1354 1361 ) # add uncolored checkbox/indent
1355 1362 outstr += self.printstring(
1356 1363 self.chunkpad, frtoline, pair=colorpair, towin=towin
1357 1364 )
1358 1365
1359 1366 if hunk.folded and not ignorefolding:
1360 1367 # skip remainder of output
1361 1368 return outstr
1362 1369
1363 1370 # print out lines of the chunk preceeding changed-lines
1364 1371 for line in hunk.before:
1365 1372 linestr = (
1366 1373 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1367 1374 )
1368 1375 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1369 1376
1370 1377 return outstr
1371 1378
1372 1379 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1373 1380 outstr = b""
1374 1381 if hunk.folded and not ignorefolding:
1375 1382 return outstr
1376 1383
1377 1384 # a bit superfluous, but to avoid hard-coding indent amount
1378 1385 checkbox = self.getstatusprefixstring(hunk)
1379 1386 for line in hunk.after:
1380 1387 linestr = (
1381 1388 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1382 1389 )
1383 1390 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1384 1391
1385 1392 return outstr
1386 1393
1387 1394 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1388 1395 outstr = b""
1389 1396 checkbox = self.getstatusprefixstring(hunkline)
1390 1397
1391 1398 linestr = hunkline.prettystr().strip(b"\n")
1392 1399
1393 1400 # select color-pair based on whether line is an addition/removal
1394 1401 if selected:
1395 1402 colorpair = self.getcolorpair(name=b"selected")
1396 1403 elif linestr.startswith(b"+"):
1397 1404 colorpair = self.getcolorpair(name=b"addition")
1398 1405 elif linestr.startswith(b"-"):
1399 1406 colorpair = self.getcolorpair(name=b"deletion")
1400 1407 elif linestr.startswith(b"\\"):
1401 1408 colorpair = self.getcolorpair(name=b"normal")
1402 1409
1403 1410 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1404 1411 outstr += self.printstring(
1405 1412 self.chunkpad, lineprefix, towin=towin, align=False
1406 1413 ) # add uncolored checkbox/indent
1407 1414 outstr += self.printstring(
1408 1415 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1409 1416 )
1410 1417 return outstr
1411 1418
1412 1419 def printitem(
1413 1420 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1414 1421 ):
1415 1422 """
1416 1423 use __printitem() to print the the specified item.applied.
1417 1424 if item is not specified, then print the entire patch.
1418 1425 (hiding folded elements, etc. -- see __printitem() docstring)
1419 1426 """
1420 1427
1421 1428 if item is None:
1422 1429 item = self.headerlist
1423 1430 if recursechildren:
1424 1431 self.linesprintedtopadsofar = 0
1425 1432
1426 1433 outstr = []
1427 1434 self.__printitem(
1428 1435 item, ignorefolding, recursechildren, outstr, towin=towin
1429 1436 )
1430 1437 return b''.join(outstr)
1431 1438
1432 1439 def outofdisplayedarea(self):
1433 1440 y, _ = self.chunkpad.getyx() # cursor location
1434 1441 # * 2 here works but an optimization would be the max number of
1435 1442 # consecutive non selectable lines
1436 1443 # i.e the max number of context line for any hunk in the patch
1437 1444 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1438 1445 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1439 1446 return y < miny or y > maxy
1440 1447
1441 1448 def handleselection(self, item, recursechildren):
1442 1449 selected = item is self.currentselecteditem
1443 1450 if selected and recursechildren:
1444 1451 # assumes line numbering starting from line 0
1445 1452 self.selecteditemstartline = self.linesprintedtopadsofar
1446 1453 selecteditemlines = self.getnumlinesdisplayed(
1447 1454 item, recursechildren=False
1448 1455 )
1449 1456 self.selecteditemendline = (
1450 1457 self.selecteditemstartline + selecteditemlines - 1
1451 1458 )
1452 1459 return selected
1453 1460
1454 1461 def __printitem(
1455 1462 self, item, ignorefolding, recursechildren, outstr, towin=True
1456 1463 ):
1457 1464 """
1458 1465 recursive method for printing out patch/header/hunk/hunk-line data to
1459 1466 screen. also returns a string with all of the content of the displayed
1460 1467 patch (not including coloring, etc.).
1461 1468
1462 1469 if ignorefolding is True, then folded items are printed out.
1463 1470
1464 1471 if recursechildren is False, then only print the item without its
1465 1472 child items.
1466 1473 """
1467 1474
1468 1475 if towin and self.outofdisplayedarea():
1469 1476 return
1470 1477
1471 1478 selected = self.handleselection(item, recursechildren)
1472 1479
1473 1480 # patch object is a list of headers
1474 1481 if isinstance(item, patch):
1475 1482 if recursechildren:
1476 1483 for hdr in item:
1477 1484 self.__printitem(
1478 1485 hdr, ignorefolding, recursechildren, outstr, towin
1479 1486 )
1480 1487 # todo: eliminate all isinstance() calls
1481 1488 if isinstance(item, uiheader):
1482 1489 outstr.append(
1483 1490 self.printheader(
1484 1491 item, selected, towin=towin, ignorefolding=ignorefolding
1485 1492 )
1486 1493 )
1487 1494 if recursechildren:
1488 1495 for hnk in item.hunks:
1489 1496 self.__printitem(
1490 1497 hnk, ignorefolding, recursechildren, outstr, towin
1491 1498 )
1492 1499 elif isinstance(item, uihunk) and (
1493 1500 (not item.header.folded) or ignorefolding
1494 1501 ):
1495 1502 # print the hunk data which comes before the changed-lines
1496 1503 outstr.append(
1497 1504 self.printhunklinesbefore(
1498 1505 item, selected, towin=towin, ignorefolding=ignorefolding
1499 1506 )
1500 1507 )
1501 1508 if recursechildren:
1502 1509 for l in item.changedlines:
1503 1510 self.__printitem(
1504 1511 l, ignorefolding, recursechildren, outstr, towin
1505 1512 )
1506 1513 outstr.append(
1507 1514 self.printhunklinesafter(
1508 1515 item, towin=towin, ignorefolding=ignorefolding
1509 1516 )
1510 1517 )
1511 1518 elif isinstance(item, uihunkline) and (
1512 1519 (not item.hunk.folded) or ignorefolding
1513 1520 ):
1514 1521 outstr.append(
1515 1522 self.printhunkchangedline(item, selected, towin=towin)
1516 1523 )
1517 1524
1518 1525 return outstr
1519 1526
1520 1527 def getnumlinesdisplayed(
1521 1528 self, item=None, ignorefolding=False, recursechildren=True
1522 1529 ):
1523 1530 """
1524 1531 return the number of lines which would be displayed if the item were
1525 1532 to be printed to the display. the item will not be printed to the
1526 1533 display (pad).
1527 1534 if no item is given, assume the entire patch.
1528 1535 if ignorefolding is True, folded items will be unfolded when counting
1529 1536 the number of lines.
1530 1537 """
1531 1538
1532 1539 # temporarily disable printing to windows by printstring
1533 1540 patchdisplaystring = self.printitem(
1534 1541 item, ignorefolding, recursechildren, towin=False
1535 1542 )
1536 1543 numlines = len(patchdisplaystring) // self.xscreensize
1537 1544 return numlines
1538 1545
1539 1546 def sigwinchhandler(self, n, frame):
1540 1547 """handle window resizing"""
1541 1548 try:
1542 1549 curses.endwin()
1543 1550 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1544 1551 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1545 1552 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1546 1553 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1547 1554 except curses.error:
1548 1555 pass
1549 1556
1550 1557 def getcolorpair(
1551 1558 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1552 1559 ):
1553 1560 """
1554 1561 get a curses color pair, adding it to self.colorpairs if it is not
1555 1562 already defined. an optional string, name, can be passed as a shortcut
1556 1563 for referring to the color-pair. by default, if no arguments are
1557 1564 specified, the white foreground / black background color-pair is
1558 1565 returned.
1559 1566
1560 1567 it is expected that this function will be used exclusively for
1561 1568 initializing color pairs, and not curses.init_pair().
1562 1569
1563 1570 attrlist is used to 'flavor' the returned color-pair. this information
1564 1571 is not stored in self.colorpairs. it contains attribute values like
1565 1572 curses.A_BOLD.
1566 1573 """
1567 1574
1568 1575 if (name is not None) and name in self.colorpairnames:
1569 1576 # then get the associated color pair and return it
1570 1577 colorpair = self.colorpairnames[name]
1571 1578 else:
1572 1579 if fgcolor is None:
1573 1580 fgcolor = -1
1574 1581 if bgcolor is None:
1575 1582 bgcolor = -1
1576 1583 if (fgcolor, bgcolor) in self.colorpairs:
1577 1584 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1578 1585 else:
1579 1586 pairindex = len(self.colorpairs) + 1
1580 1587 if self.usecolor:
1581 1588 curses.init_pair(pairindex, fgcolor, bgcolor)
1582 1589 colorpair = self.colorpairs[
1583 1590 (fgcolor, bgcolor)
1584 1591 ] = curses.color_pair(pairindex)
1585 1592 if name is not None:
1586 1593 self.colorpairnames[name] = curses.color_pair(pairindex)
1587 1594 else:
1588 1595 cval = 0
1589 1596 if name is not None:
1590 1597 if name == b'selected':
1591 1598 cval = curses.A_REVERSE
1592 1599 self.colorpairnames[name] = cval
1593 1600 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1594 1601
1595 1602 # add attributes if possible
1596 1603 if attrlist is None:
1597 1604 attrlist = []
1598 1605 if colorpair < 256:
1599 1606 # then it is safe to apply all attributes
1600 1607 for textattr in attrlist:
1601 1608 colorpair |= textattr
1602 1609 else:
1603 1610 # just apply a select few (safe?) attributes
1604 1611 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1605 1612 if textattrib in attrlist:
1606 1613 colorpair |= textattrib
1607 1614 return colorpair
1608 1615
1609 1616 def initcolorpair(self, *args, **kwargs):
1610 1617 """same as getcolorpair."""
1611 1618 self.getcolorpair(*args, **kwargs)
1612 1619
1613 1620 def helpwindow(self):
1614 1621 """print a help window to the screen. exit after any keypress."""
1615 1622 helptext = _(
1616 1623 """ [press any key to return to the patch-display]
1617 1624
1618 1625 The curses hunk selector allows you to interactively choose among the
1619 1626 changes you have made, and confirm only those changes you select for
1620 1627 further processing by the command you are running (such as commit,
1621 1628 shelve, or revert). After confirming the selected changes, the
1622 1629 unselected changes are still present in your working copy, so you can
1623 1630 use the hunk selector multiple times to split large changes into
1624 1631 smaller changesets. the following are valid keystrokes:
1625 1632
1626 1633 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1627 1634 [enter] : (un-)select item and go to next item of same type
1628 1635 A : (un-)select all items
1629 1636 X : (un-)select all items between current and most-recent
1630 1637 up/down-arrow [k/j] : go to previous/next unfolded item
1631 1638 pgup/pgdn [K/J] : go to previous/next item of same type
1632 1639 right/left-arrow [l/h] : go to child item / parent item
1633 1640 shift-left-arrow [H] : go to parent header / fold selected header
1634 1641 g : go to the top
1635 1642 G : go to the bottom
1636 1643 f : fold / unfold item, hiding/revealing its children
1637 1644 F : fold / unfold parent item and all of its ancestors
1638 1645 ctrl-l : scroll the selected line to the top of the screen
1639 1646 m : edit / resume editing the commit message
1640 1647 e : edit the currently selected hunk
1641 1648 a : toggle all selections
1642 1649 c : confirm selected changes
1643 1650 r : review/edit and confirm selected changes
1644 1651 q : quit without confirming (no changes will be made)
1645 1652 ? : help (what you're currently reading)"""
1646 1653 )
1647 1654
1648 1655 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1649 1656 helplines = helptext.split(b"\n")
1650 1657 helplines = helplines + [b" "] * (
1651 1658 self.yscreensize - self.numstatuslines - len(helplines) - 1
1652 1659 )
1653 1660 try:
1654 1661 for line in helplines:
1655 1662 self.printstring(helpwin, line, pairname=b"legend")
1656 1663 except curses.error:
1657 1664 pass
1658 1665 helpwin.refresh()
1659 1666 try:
1660 1667 with self.ui.timeblockedsection(b'crecord'):
1661 1668 helpwin.getkey()
1662 1669 except curses.error:
1663 1670 pass
1664 1671
1665 1672 def commitMessageWindow(self):
1666 1673 """Create a temporary commit message editing window on the screen."""
1667 1674
1668 1675 curses.raw()
1669 1676 curses.def_prog_mode()
1670 1677 curses.endwin()
1671 1678 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1672 1679 curses.cbreak()
1673 1680 self.stdscr.refresh()
1674 1681 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1675 1682
1676 1683 def handlefirstlineevent(self):
1677 1684 """
1678 1685 Handle 'g' to navigate to the top most file in the ncurses window.
1679 1686 """
1680 1687 self.currentselecteditem = self.headerlist[0]
1681 1688 currentitem = self.currentselecteditem
1682 1689 # select the parent item recursively until we're at a header
1683 1690 while True:
1684 1691 nextitem = currentitem.parentitem()
1685 1692 if nextitem is None:
1686 1693 break
1687 1694 else:
1688 1695 currentitem = nextitem
1689 1696
1690 1697 self.currentselecteditem = currentitem
1691 1698
1692 1699 def handlelastlineevent(self):
1693 1700 """
1694 1701 Handle 'G' to navigate to the bottom most file/hunk/line depending
1695 1702 on the whether the fold is active or not.
1696 1703
1697 1704 If the bottom most file is folded, it navigates to that file and
1698 1705 stops there. If the bottom most file is unfolded, it navigates to
1699 1706 the bottom most hunk in that file and stops there. If the bottom most
1700 1707 hunk is unfolded, it navigates to the bottom most line in that hunk.
1701 1708 """
1702 1709 currentitem = self.currentselecteditem
1703 1710 nextitem = currentitem.nextitem()
1704 1711 # select the child item recursively until we're at a footer
1705 1712 while nextitem is not None:
1706 1713 nextitem = currentitem.nextitem()
1707 1714 if nextitem is None:
1708 1715 break
1709 1716 else:
1710 1717 currentitem = nextitem
1711 1718
1712 1719 self.currentselecteditem = currentitem
1713 1720 self.recenterdisplayedarea()
1714 1721
1715 1722 def confirmationwindow(self, windowtext):
1716 1723 """display an informational window, then wait for and return a
1717 1724 keypress."""
1718 1725
1719 1726 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1720 1727 try:
1721 1728 lines = windowtext.split(b"\n")
1722 1729 for line in lines:
1723 1730 self.printstring(confirmwin, line, pairname=b"selected")
1724 1731 except curses.error:
1725 1732 pass
1726 1733 self.stdscr.refresh()
1727 1734 confirmwin.refresh()
1728 1735 try:
1729 1736 with self.ui.timeblockedsection(b'crecord'):
1730 1737 response = chr(self.stdscr.getch())
1731 1738 except ValueError:
1732 1739 response = None
1733 1740
1734 1741 return response
1735 1742
1736 1743 def reviewcommit(self):
1737 1744 """ask for 'y' to be pressed to confirm selected. return True if
1738 1745 confirmed."""
1739 1746 confirmtext = _(
1740 1747 """If you answer yes to the following, your currently chosen patch chunks
1741 1748 will be loaded into an editor. To modify the patch, make the changes in your
1742 1749 editor and save. To accept the current patch as-is, close the editor without
1743 1750 saving.
1744 1751
1745 1752 note: don't add/remove lines unless you also modify the range information.
1746 1753 failing to follow this rule will result in the commit aborting.
1747 1754
1748 1755 are you sure you want to review/edit and confirm the selected changes [yn]?
1749 1756 """
1750 1757 )
1751 1758 with self.ui.timeblockedsection(b'crecord'):
1752 1759 response = self.confirmationwindow(confirmtext)
1753 1760 if response is None:
1754 1761 response = "n"
1755 1762 if response.lower().startswith("y"):
1756 1763 return True
1757 1764 else:
1758 1765 return False
1759 1766
1760 1767 def recenterdisplayedarea(self):
1761 1768 """
1762 1769 once we scrolled with pg up pg down we can be pointing outside of the
1763 1770 display zone. we print the patch with towin=False to compute the
1764 1771 location of the selected item even though it is outside of the displayed
1765 1772 zone and then update the scroll.
1766 1773 """
1767 1774 self.printitem(towin=False)
1768 1775 self.updatescroll()
1769 1776
1770 1777 def toggleedit(self, item=None, test=False):
1771 1778 """
1772 1779 edit the currently selected chunk
1773 1780 """
1774 1781
1775 1782 def updateui(self):
1776 1783 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1777 1784 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1778 1785 self.updatescroll()
1779 1786 self.stdscr.refresh()
1780 1787 self.statuswin.refresh()
1781 1788 self.stdscr.keypad(1)
1782 1789
1783 1790 def editpatchwitheditor(self, chunk):
1784 1791 if chunk is None:
1785 1792 self.ui.write(_(b'cannot edit patch for whole file'))
1786 1793 self.ui.write(b"\n")
1787 1794 return None
1788 1795 if chunk.header.binary():
1789 1796 self.ui.write(_(b'cannot edit patch for binary file'))
1790 1797 self.ui.write(b"\n")
1791 1798 return None
1792 1799
1793 1800 # write the initial patch
1794 1801 patch = stringio()
1795 1802 patch.write(diffhelptext + hunkhelptext)
1796 1803 chunk.header.write(patch)
1797 1804 chunk.write(patch)
1798 1805
1799 1806 # start the editor and wait for it to complete
1800 1807 try:
1801 1808 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1802 1809 except error.Abort as exc:
1803 1810 self.errorstr = stringutil.forcebytestr(exc)
1804 1811 return None
1805 1812 finally:
1806 1813 self.stdscr.clear()
1807 1814 self.stdscr.refresh()
1808 1815
1809 1816 # remove comment lines
1810 1817 patch = [
1811 1818 line + b'\n'
1812 1819 for line in patch.splitlines()
1813 1820 if not line.startswith(b'#')
1814 1821 ]
1815 1822 return patchmod.parsepatch(patch)
1816 1823
1817 1824 if item is None:
1818 1825 item = self.currentselecteditem
1819 1826 if isinstance(item, uiheader):
1820 1827 return
1821 1828 if isinstance(item, uihunkline):
1822 1829 item = item.parentitem()
1823 1830 if not isinstance(item, uihunk):
1824 1831 return
1825 1832
1826 1833 # To go back to that hunk or its replacement at the end of the edit
1827 1834 itemindex = item.parentitem().hunks.index(item)
1828 1835
1829 1836 beforeadded, beforeremoved = item.added, item.removed
1830 1837 newpatches = editpatchwitheditor(self, item)
1831 1838 if newpatches is None:
1832 1839 if not test:
1833 1840 updateui(self)
1834 1841 return
1835 1842 header = item.header
1836 1843 editedhunkindex = header.hunks.index(item)
1837 1844 hunksbefore = header.hunks[:editedhunkindex]
1838 1845 hunksafter = header.hunks[editedhunkindex + 1 :]
1839 1846 newpatchheader = newpatches[0]
1840 1847 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1841 1848 newadded = sum([h.added for h in newhunks])
1842 1849 newremoved = sum([h.removed for h in newhunks])
1843 1850 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1844 1851
1845 1852 for h in hunksafter:
1846 1853 h.toline += offset
1847 1854 for h in newhunks:
1848 1855 h.folded = False
1849 1856 header.hunks = hunksbefore + newhunks + hunksafter
1850 1857 if self.emptypatch():
1851 1858 header.hunks = hunksbefore + [item] + hunksafter
1852 1859 self.currentselecteditem = header
1853 1860 if len(header.hunks) > itemindex:
1854 1861 self.currentselecteditem = header.hunks[itemindex]
1855 1862
1856 1863 if not test:
1857 1864 updateui(self)
1858 1865
1859 1866 def emptypatch(self):
1860 1867 item = self.headerlist
1861 1868 if not item:
1862 1869 return True
1863 1870 for header in item:
1864 1871 if header.hunks:
1865 1872 return False
1866 1873 return True
1867 1874
1868 1875 def handlekeypressed(self, keypressed, test=False):
1869 1876 """
1870 1877 Perform actions based on pressed keys.
1871 1878
1872 1879 Return true to exit the main loop.
1873 1880 """
1874 1881 if keypressed in ["k", "KEY_UP"]:
1875 1882 self.uparrowevent()
1876 1883 elif keypressed in ["K", "KEY_PPAGE"]:
1877 1884 self.uparrowshiftevent()
1878 1885 elif keypressed in ["j", "KEY_DOWN"]:
1879 1886 self.downarrowevent()
1880 1887 elif keypressed in ["J", "KEY_NPAGE"]:
1881 1888 self.downarrowshiftevent()
1882 1889 elif keypressed in ["l", "KEY_RIGHT"]:
1883 1890 self.rightarrowevent()
1884 1891 elif keypressed in ["h", "KEY_LEFT"]:
1885 1892 self.leftarrowevent()
1886 1893 elif keypressed in ["H", "KEY_SLEFT"]:
1887 1894 self.leftarrowshiftevent()
1888 1895 elif keypressed in ["q"]:
1889 1896 raise error.Abort(_(b'user quit'))
1890 1897 elif keypressed in ['a']:
1891 1898 self.flipselections()
1892 1899 elif keypressed in ["c"]:
1893 1900 return True
1894 1901 elif keypressed in ["r"]:
1895 1902 if self.reviewcommit():
1896 1903 self.opts[b'review'] = True
1897 1904 return True
1898 1905 elif test and keypressed in ["R"]:
1899 1906 self.opts[b'review'] = True
1900 1907 return True
1901 1908 elif keypressed in [" ", "x"]:
1902 1909 self.toggleapply()
1903 1910 elif keypressed in ["\n", "KEY_ENTER"]:
1904 1911 self.toggleapply()
1905 1912 self.nextsametype(test=test)
1906 1913 elif keypressed in ["X"]:
1907 1914 self.toggleallbetween()
1908 1915 elif keypressed in ["A"]:
1909 1916 self.toggleall()
1910 1917 elif keypressed in ["e"]:
1911 1918 self.toggleedit(test=test)
1912 1919 elif keypressed in ["f"]:
1913 1920 self.togglefolded()
1914 1921 elif keypressed in ["F"]:
1915 1922 self.togglefolded(foldparent=True)
1916 1923 elif keypressed in ["m"]:
1917 1924 self.commitMessageWindow()
1918 1925 elif keypressed in ["g", "KEY_HOME"]:
1919 1926 self.handlefirstlineevent()
1920 1927 elif keypressed in ["G", "KEY_END"]:
1921 1928 self.handlelastlineevent()
1922 1929 elif keypressed in ["?"]:
1923 1930 self.helpwindow()
1924 1931 self.stdscr.clear()
1925 1932 self.stdscr.refresh()
1926 1933 elif keypressed in [curses.ascii.ctrl("L")]:
1927 1934 # scroll the current line to the top of the screen, and redraw
1928 1935 # everything
1929 1936 self.scrolllines(self.selecteditemstartline)
1930 1937 self.stdscr.clear()
1931 1938 self.stdscr.refresh()
1932 1939
1933 1940 def main(self, stdscr):
1934 1941 """
1935 1942 method to be wrapped by curses.wrapper() for selecting chunks.
1936 1943 """
1937 1944
1938 1945 origsigwinch = sentinel = object()
1939 1946 if util.safehasattr(signal, b'SIGWINCH'):
1940 1947 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1941 1948 try:
1942 1949 return self._main(stdscr)
1943 1950 finally:
1944 1951 if origsigwinch is not sentinel:
1945 1952 signal.signal(signal.SIGWINCH, origsigwinch)
1946 1953
1947 1954 def _main(self, stdscr):
1948 1955 self.stdscr = stdscr
1949 1956 # error during initialization, cannot be printed in the curses
1950 1957 # interface, it should be printed by the calling code
1951 1958 self.initexc = None
1952 1959 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1953 1960
1954 1961 curses.start_color()
1955 1962 try:
1956 1963 curses.use_default_colors()
1957 1964 except curses.error:
1958 1965 self.usecolor = False
1959 1966
1960 1967 # In some situations we may have some cruft left on the "alternate
1961 1968 # screen" from another program (or previous iterations of ourself), and
1962 1969 # we won't clear it if the scroll region is small enough to comfortably
1963 1970 # fit on the terminal.
1964 1971 self.stdscr.clear()
1965 1972
1966 1973 # don't display the cursor
1967 1974 try:
1968 1975 curses.curs_set(0)
1969 1976 except curses.error:
1970 1977 pass
1971 1978
1972 1979 # available colors: black, blue, cyan, green, magenta, white, yellow
1973 1980 # init_pair(color_id, foreground_color, background_color)
1974 1981 self.initcolorpair(None, None, name=b"normal")
1975 1982 self.initcolorpair(
1976 1983 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1977 1984 )
1978 1985 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1979 1986 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1980 1987 self.initcolorpair(
1981 1988 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1982 1989 )
1983 1990 # newwin([height, width,] begin_y, begin_x)
1984 1991 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1985 1992 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1986 1993
1987 1994 # figure out how much space to allocate for the chunk-pad which is
1988 1995 # used for displaying the patch
1989 1996
1990 1997 # stupid hack to prevent getnumlinesdisplayed from failing
1991 1998 self.chunkpad = curses.newpad(1, self.xscreensize)
1992 1999
1993 2000 # add 1 so to account for last line text reaching end of line
1994 2001 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1995 2002
1996 2003 try:
1997 2004 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1998 2005 except curses.error:
1999 2006 self.initexc = fallbackerror(
2000 2007 _(b'this diff is too large to be displayed')
2001 2008 )
2002 2009 return
2003 2010 # initialize selecteditemendline (initial start-line is 0)
2004 2011 self.selecteditemendline = self.getnumlinesdisplayed(
2005 2012 self.currentselecteditem, recursechildren=False
2006 2013 )
2007 2014
2008 2015 while True:
2009 2016 self.updatescreen()
2010 2017 try:
2011 2018 with self.ui.timeblockedsection(b'crecord'):
2012 2019 keypressed = self.statuswin.getkey()
2013 2020 if self.errorstr is not None:
2014 2021 self.errorstr = None
2015 2022 continue
2016 2023 except curses.error:
2017 2024 keypressed = b"foobar"
2018 2025 if self.handlekeypressed(keypressed):
2019 2026 break
2020 2027
2021 2028 if self.commenttext != b"":
2022 2029 whitespaceremoved = re.sub(
2023 2030 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2024 2031 )
2025 2032 if whitespaceremoved != b"":
2026 2033 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now