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