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