##// END OF EJS Templates
crecord: enable search hotkeys (issue6834)...
Jordi Gutiérrez Hermoso -
r52280:0e063a9b default
parent child Browse files
Show More
@@ -1,2124 +1,2133 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 712 self.regex = None
713 713
714 714 def uparrowevent(self):
715 715 """
716 716 try to select the previous item to the current item that has the
717 717 most-indented level. for example, if a hunk is selected, try to select
718 718 the last hunkline of the hunk prior to the selected hunk. or, if
719 719 the first hunkline of a hunk is currently selected, then select the
720 720 hunk itself.
721 721 """
722 722 currentitem = self.currentselecteditem
723 723
724 724 nextitem = currentitem.previtem()
725 725
726 726 if nextitem is None:
727 727 # if no parent item (i.e. currentitem is the first header), then
728 728 # no change...
729 729 nextitem = currentitem
730 730
731 731 self.currentselecteditem = nextitem
732 732
733 733 def uparrowshiftevent(self):
734 734 """
735 735 select (if possible) the previous item on the same level as the
736 736 currently selected item. otherwise, select (if possible) the
737 737 parent-item of the currently selected item.
738 738 """
739 739 currentitem = self.currentselecteditem
740 740 nextitem = currentitem.prevsibling()
741 741 # if there's no previous sibling, try choosing the parent
742 742 if nextitem is None:
743 743 nextitem = currentitem.parentitem()
744 744 if nextitem is None:
745 745 # if no parent item (i.e. currentitem is the first header), then
746 746 # no change...
747 747 nextitem = currentitem
748 748
749 749 self.currentselecteditem = nextitem
750 750 self.recenterdisplayedarea()
751 751
752 752 def downarrowevent(self):
753 753 """
754 754 try to select the next item to the current item that has the
755 755 most-indented level. for example, if a hunk is selected, select
756 756 the first hunkline of the selected hunk. or, if the last hunkline of
757 757 a hunk is currently selected, then select the next hunk, if one exists,
758 758 or if not, the next header if one exists.
759 759 """
760 760 # self.startprintline += 1 #debug
761 761 currentitem = self.currentselecteditem
762 762
763 763 nextitem = currentitem.nextitem()
764 764 # if there's no next item, keep the selection as-is
765 765 if nextitem is None:
766 766 nextitem = currentitem
767 767
768 768 self.currentselecteditem = nextitem
769 769
770 770 def downarrowshiftevent(self):
771 771 """
772 772 select (if possible) the next item on the same level as the currently
773 773 selected item. otherwise, select (if possible) the next item on the
774 774 same level as the parent item of the currently selected item.
775 775 """
776 776 currentitem = self.currentselecteditem
777 777 nextitem = currentitem.nextsibling()
778 778 # if there's no next sibling, try choosing the parent's nextsibling
779 779 if nextitem is None:
780 780 try:
781 781 nextitem = currentitem.parentitem().nextsibling()
782 782 except AttributeError:
783 783 # parentitem returned None, so nextsibling() can't be called
784 784 nextitem = None
785 785 if nextitem is None:
786 786 # if parent has no next sibling, then no change...
787 787 nextitem = currentitem
788 788
789 789 self.currentselecteditem = nextitem
790 790 self.recenterdisplayedarea()
791 791
792 792 def nextsametype(self, test=False):
793 793 currentitem = self.currentselecteditem
794 794 sametype = lambda item: isinstance(item, type(currentitem))
795 795 nextitem = currentitem.nextitem()
796 796
797 797 while nextitem is not None and not sametype(nextitem):
798 798 nextitem = nextitem.nextitem()
799 799
800 800 if nextitem is None:
801 801 nextitem = currentitem
802 802 else:
803 803 parent = nextitem.parentitem()
804 804 if parent is not None and parent.folded:
805 805 self.togglefolded(parent)
806 806
807 807 self.currentselecteditem = nextitem
808 808 if not test:
809 809 self.recenterdisplayedarea()
810 810
811 811 def rightarrowevent(self):
812 812 """
813 813 select (if possible) the first of this item's child-items.
814 814 """
815 815 currentitem = self.currentselecteditem
816 816 nextitem = currentitem.firstchild()
817 817
818 818 # turn off folding if we want to show a child-item
819 819 if currentitem.folded:
820 820 self.togglefolded(currentitem)
821 821
822 822 if nextitem is None:
823 823 # if no next item on parent-level, then no change...
824 824 nextitem = currentitem
825 825
826 826 self.currentselecteditem = nextitem
827 827
828 828 def leftarrowevent(self):
829 829 """
830 830 if the current item can be folded (i.e. it is an unfolded header or
831 831 hunk), then fold it. otherwise try select (if possible) the parent
832 832 of this item.
833 833 """
834 834 currentitem = self.currentselecteditem
835 835
836 836 # try to fold the item
837 837 if not isinstance(currentitem, uihunkline):
838 838 if not currentitem.folded:
839 839 self.togglefolded(item=currentitem)
840 840 return
841 841
842 842 # if it can't be folded, try to select the parent item
843 843 nextitem = currentitem.parentitem()
844 844
845 845 if nextitem is None:
846 846 # if no item on parent-level, then no change...
847 847 nextitem = currentitem
848 848 if not nextitem.folded:
849 849 self.togglefolded(item=nextitem)
850 850
851 851 self.currentselecteditem = nextitem
852 852
853 853 def leftarrowshiftevent(self):
854 854 """
855 855 select the header of the current item (or fold current item if the
856 856 current item is already a header).
857 857 """
858 858 currentitem = self.currentselecteditem
859 859
860 860 if isinstance(currentitem, uiheader):
861 861 if not currentitem.folded:
862 862 self.togglefolded(item=currentitem)
863 863 return
864 864
865 865 # select the parent item recursively until we're at a header
866 866 while True:
867 867 nextitem = currentitem.parentitem()
868 868 if nextitem is None:
869 869 break
870 870 else:
871 871 currentitem = nextitem
872 872
873 873 self.currentselecteditem = currentitem
874 874
875 875 def updatescroll(self):
876 876 """scroll the screen to fully show the currently-selected"""
877 877 selstart = self.selecteditemstartline
878 878 selend = self.selecteditemendline
879 879
880 880 padstart = self.firstlineofpadtoprint
881 881 padend = padstart + self.yscreensize - self.numstatuslines - 1
882 882 # 'buffered' pad start/end values which scroll with a certain
883 883 # top/bottom context margin
884 884 padstartbuffered = padstart + 3
885 885 padendbuffered = padend - 3
886 886
887 887 if selend > padendbuffered:
888 888 self.scrolllines(selend - padendbuffered)
889 889 elif selstart < padstartbuffered:
890 890 # negative values scroll in pgup direction
891 891 self.scrolllines(selstart - padstartbuffered)
892 892
893 893 def scrolllines(self, numlines):
894 894 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
895 895 self.firstlineofpadtoprint += numlines
896 896 if self.firstlineofpadtoprint < 0:
897 897 self.firstlineofpadtoprint = 0
898 898 if self.firstlineofpadtoprint > self.numpadlines - 1:
899 899 self.firstlineofpadtoprint = self.numpadlines - 1
900 900
901 901 def toggleapply(self, item=None):
902 902 """
903 903 toggle the applied flag of the specified item. if no item is specified,
904 904 toggle the flag of the currently selected item.
905 905 """
906 906 if item is None:
907 907 item = self.currentselecteditem
908 908 # Only set this when NOT using 'toggleall'
909 909 self.lastapplieditem = item
910 910
911 911 item.applied = not item.applied
912 912
913 913 if isinstance(item, uiheader):
914 914 item.partial = False
915 915 if item.applied:
916 916 # apply all its hunks
917 917 for hnk in item.hunks:
918 918 hnk.applied = True
919 919 # apply all their hunklines
920 920 for hunkline in hnk.changedlines:
921 921 hunkline.applied = True
922 922 else:
923 923 # un-apply all its hunks
924 924 for hnk in item.hunks:
925 925 hnk.applied = False
926 926 hnk.partial = False
927 927 # un-apply all their hunklines
928 928 for hunkline in hnk.changedlines:
929 929 hunkline.applied = False
930 930 elif isinstance(item, uihunk):
931 931 item.partial = False
932 932 # apply all it's hunklines
933 933 for hunkline in item.changedlines:
934 934 hunkline.applied = item.applied
935 935
936 936 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
937 937 allsiblingsapplied = not (False in siblingappliedstatus)
938 938 nosiblingsapplied = not (True in siblingappliedstatus)
939 939
940 940 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
941 941 somesiblingspartial = True in siblingspartialstatus
942 942
943 943 # cases where applied or partial should be removed from header
944 944
945 945 # if no 'sibling' hunks are applied (including this hunk)
946 946 if nosiblingsapplied:
947 947 if not item.header.special():
948 948 item.header.applied = False
949 949 item.header.partial = False
950 950 else: # some/all parent siblings are applied
951 951 item.header.applied = True
952 952 item.header.partial = (
953 953 somesiblingspartial or not allsiblingsapplied
954 954 )
955 955
956 956 elif isinstance(item, uihunkline):
957 957 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
958 958 allsiblingsapplied = not (False in siblingappliedstatus)
959 959 nosiblingsapplied = not (True in siblingappliedstatus)
960 960
961 961 # if no 'sibling' lines are applied
962 962 if nosiblingsapplied:
963 963 item.hunk.applied = False
964 964 item.hunk.partial = False
965 965 elif allsiblingsapplied:
966 966 item.hunk.applied = True
967 967 item.hunk.partial = False
968 968 else: # some siblings applied
969 969 item.hunk.applied = True
970 970 item.hunk.partial = True
971 971
972 972 parentsiblingsapplied = [
973 973 hnk.applied for hnk in item.hunk.header.hunks
974 974 ]
975 975 noparentsiblingsapplied = not (True in parentsiblingsapplied)
976 976 allparentsiblingsapplied = not (False in parentsiblingsapplied)
977 977
978 978 parentsiblingspartial = [
979 979 hnk.partial for hnk in item.hunk.header.hunks
980 980 ]
981 981 someparentsiblingspartial = True in parentsiblingspartial
982 982
983 983 # if all parent hunks are not applied, un-apply header
984 984 if noparentsiblingsapplied:
985 985 if not item.hunk.header.special():
986 986 item.hunk.header.applied = False
987 987 item.hunk.header.partial = False
988 988 # set the applied and partial status of the header if needed
989 989 else: # some/all parent siblings are applied
990 990 item.hunk.header.applied = True
991 991 item.hunk.header.partial = (
992 992 someparentsiblingspartial or not allparentsiblingsapplied
993 993 )
994 994
995 995 def toggleall(self):
996 996 """toggle the applied flag of all items."""
997 997 if self.waslasttoggleallapplied: # then unapply them this time
998 998 for item in self.headerlist:
999 999 if item.applied:
1000 1000 self.toggleapply(item)
1001 1001 else:
1002 1002 for item in self.headerlist:
1003 1003 if not item.applied:
1004 1004 self.toggleapply(item)
1005 1005 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
1006 1006
1007 1007 def flipselections(self):
1008 1008 """
1009 1009 Flip all selections. Every selected line is unselected and vice
1010 1010 versa.
1011 1011 """
1012 1012 for header in self.headerlist:
1013 1013 for hunk in header.allchildren():
1014 1014 for line in hunk.allchildren():
1015 1015 self.toggleapply(line)
1016 1016
1017 1017 def toggleallbetween(self):
1018 1018 """toggle applied on or off for all items in range [lastapplied,
1019 1019 current]."""
1020 1020 if (
1021 1021 not self.lastapplieditem
1022 1022 or self.currentselecteditem == self.lastapplieditem
1023 1023 ):
1024 1024 # Treat this like a normal 'x'/' '
1025 1025 self.toggleapply()
1026 1026 return
1027 1027
1028 1028 startitem = self.lastapplieditem
1029 1029 enditem = self.currentselecteditem
1030 1030 # Verify that enditem is "after" startitem, otherwise swap them.
1031 1031 for direction in [b'forward', b'reverse']:
1032 1032 nextitem = startitem.nextitem()
1033 1033 while nextitem and nextitem != enditem:
1034 1034 nextitem = nextitem.nextitem()
1035 1035 if nextitem:
1036 1036 break
1037 1037 # Looks like we went the wrong direction :)
1038 1038 startitem, enditem = enditem, startitem
1039 1039
1040 1040 if not nextitem:
1041 1041 # We didn't find a path going either forward or backward? Don't know
1042 1042 # how this can happen, let's not crash though.
1043 1043 return
1044 1044
1045 1045 nextitem = startitem
1046 1046 # Switch all items to be the opposite state of the currently selected
1047 1047 # item. Specifically:
1048 1048 # [ ] startitem
1049 1049 # [x] middleitem
1050 1050 # [ ] enditem <-- currently selected
1051 1051 # This will turn all three on, since the currently selected item is off.
1052 1052 # This does *not* invert each item (i.e. middleitem stays marked/on)
1053 1053 desiredstate = not self.currentselecteditem.applied
1054 1054 while nextitem != enditem.nextitem():
1055 1055 if nextitem.applied != desiredstate:
1056 1056 self.toggleapply(item=nextitem)
1057 1057 nextitem = nextitem.nextitem()
1058 1058
1059 1059 def togglefolded(self, item=None, foldparent=False):
1060 1060 """toggle folded flag of specified item (defaults to currently
1061 1061 selected)"""
1062 1062 if item is None:
1063 1063 item = self.currentselecteditem
1064 1064 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1065 1065 if not isinstance(item, uiheader):
1066 1066 # we need to select the parent item in this case
1067 1067 self.currentselecteditem = item = item.parentitem()
1068 1068 elif item.neverunfolded:
1069 1069 item.neverunfolded = False
1070 1070
1071 1071 # also fold any foldable children of the parent/current item
1072 1072 if isinstance(item, uiheader): # the original or 'new' item
1073 1073 for child in item.allchildren():
1074 1074 child.folded = not item.folded
1075 1075
1076 1076 if isinstance(item, (uiheader, uihunk)):
1077 1077 item.folded = not item.folded
1078 1078
1079 1079 def alignstring(self, instr, window):
1080 1080 """
1081 1081 add whitespace to the end of a string in order to make it fill
1082 1082 the screen in the x direction. the current cursor position is
1083 1083 taken into account when making this calculation. the string can span
1084 1084 multiple lines.
1085 1085 """
1086 1086 y, xstart = window.getyx()
1087 1087 width = self.xscreensize
1088 1088 # turn tabs into spaces
1089 1089 instr = instr.expandtabs(4)
1090 1090 strwidth = encoding.colwidth(instr)
1091 1091 numspaces = width - ((strwidth + xstart) % width)
1092 1092 return instr + b" " * numspaces
1093 1093
1094 1094 def printstring(
1095 1095 self,
1096 1096 window,
1097 1097 text,
1098 1098 fgcolor=None,
1099 1099 bgcolor=None,
1100 1100 pair=None,
1101 1101 pairname=None,
1102 1102 attrlist=None,
1103 1103 towin=True,
1104 1104 align=True,
1105 1105 showwhtspc=False,
1106 1106 ):
1107 1107 """
1108 1108 print the string, text, with the specified colors and attributes, to
1109 1109 the specified curses window object.
1110 1110
1111 1111 the foreground and background colors are of the form
1112 1112 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1113 1113 magenta, red, white, yellow]. if pairname is provided, a color
1114 1114 pair will be looked up in the self.colorpairnames dictionary.
1115 1115
1116 1116 attrlist is a list containing text attributes in the form of
1117 1117 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1118 1118 underline].
1119 1119
1120 1120 if align == True, whitespace is added to the printed string such that
1121 1121 the string stretches to the right border of the window.
1122 1122
1123 1123 if showwhtspc == True, trailing whitespace of a string is highlighted.
1124 1124 """
1125 1125 # preprocess the text, converting tabs to spaces
1126 1126 text = text.expandtabs(4)
1127 1127 # strip \n, and convert control characters to ^[char] representation
1128 1128 text = re.sub(
1129 1129 br'[\x00-\x08\x0a-\x1f]',
1130 1130 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1131 1131 text.strip(b'\n'),
1132 1132 )
1133 1133
1134 1134 if pair is not None:
1135 1135 colorpair = pair
1136 1136 elif pairname is not None:
1137 1137 colorpair = self.colorpairnames[pairname]
1138 1138 else:
1139 1139 if fgcolor is None:
1140 1140 fgcolor = -1
1141 1141 if bgcolor is None:
1142 1142 bgcolor = -1
1143 1143 if (fgcolor, bgcolor) in self.colorpairs:
1144 1144 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1145 1145 else:
1146 1146 colorpair = self.getcolorpair(fgcolor, bgcolor)
1147 1147 # add attributes if possible
1148 1148 if attrlist is None:
1149 1149 attrlist = []
1150 1150 if colorpair < 256:
1151 1151 # then it is safe to apply all attributes
1152 1152 for textattr in attrlist:
1153 1153 colorpair |= textattr
1154 1154 else:
1155 1155 # just apply a select few (safe?) attributes
1156 1156 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1157 1157 if textattr in attrlist:
1158 1158 colorpair |= textattr
1159 1159
1160 1160 y, xstart = self.chunkpad.getyx()
1161 1161 t = b"" # variable for counting lines printed
1162 1162 # if requested, show trailing whitespace
1163 1163 if showwhtspc:
1164 1164 origlen = len(text)
1165 1165 text = text.rstrip(b' \n') # tabs have already been expanded
1166 1166 strippedlen = len(text)
1167 1167 numtrailingspaces = origlen - strippedlen
1168 1168
1169 1169 if towin:
1170 1170 window.addstr(encoding.strfromlocal(text), colorpair)
1171 1171 t += text
1172 1172
1173 1173 if showwhtspc:
1174 1174 wscolorpair = colorpair | curses.A_REVERSE
1175 1175 if towin:
1176 1176 for i in range(numtrailingspaces):
1177 1177 window.addch(curses.ACS_CKBOARD, wscolorpair)
1178 1178 t += b" " * numtrailingspaces
1179 1179
1180 1180 if align:
1181 1181 if towin:
1182 1182 extrawhitespace = self.alignstring(b"", window)
1183 1183 window.addstr(extrawhitespace, colorpair)
1184 1184 else:
1185 1185 # need to use t, since the x position hasn't incremented
1186 1186 extrawhitespace = self.alignstring(t, window)
1187 1187 t += extrawhitespace
1188 1188
1189 1189 # is reset to 0 at the beginning of printitem()
1190 1190
1191 1191 linesprinted = (xstart + len(t)) // self.xscreensize
1192 1192 self.linesprintedtopadsofar += linesprinted
1193 1193 return t
1194 1194
1195 1195 def _getstatuslinesegments(self):
1196 1196 """-> [str]. return segments"""
1197 1197 selected = self.currentselecteditem.applied
1198 1198 spaceselect = _(b'space/enter: select')
1199 1199 spacedeselect = _(b'space/enter: deselect')
1200 1200 # Format the selected label into a place as long as the longer of the
1201 1201 # two possible labels. This may vary by language.
1202 1202 spacelen = max(len(spaceselect), len(spacedeselect))
1203 1203 selectedlabel = b'%-*s' % (
1204 1204 spacelen,
1205 1205 spacedeselect if selected else spaceselect,
1206 1206 )
1207 1207 segments = [
1208 1208 _headermessages[self.operation],
1209 1209 b'-',
1210 1210 _(b'[x]=selected **=collapsed'),
1211 1211 _(b'c: confirm'),
1212 1212 _(b'q: abort'),
1213 1213 _(b'arrow keys: move/expand/collapse'),
1214 1214 selectedlabel,
1215 1215 _(b'?: help'),
1216 1216 ]
1217 1217 return segments
1218 1218
1219 1219 def _getstatuslines(self):
1220 1220 """() -> [str]. return short help used in the top status window"""
1221 1221 if self.errorstr is not None:
1222 1222 lines = [self.errorstr, _(b'Press any key to continue')]
1223 1223 else:
1224 1224 # wrap segments to lines
1225 1225 segments = self._getstatuslinesegments()
1226 1226 width = self.xscreensize
1227 1227 lines = []
1228 1228 lastwidth = width
1229 1229 for s in segments:
1230 1230 w = encoding.colwidth(s)
1231 1231 sep = b' ' * (1 + (s and s[0] not in b'-['))
1232 1232 if lastwidth + w + len(sep) >= width:
1233 1233 lines.append(s)
1234 1234 lastwidth = w
1235 1235 else:
1236 1236 lines[-1] += sep + s
1237 1237 lastwidth += w + len(sep)
1238 1238 if len(lines) != self.numstatuslines:
1239 1239 self.numstatuslines = len(lines)
1240 1240 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1241 1241 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1242 1242
1243 1243 def updatescreen(self):
1244 1244 self.statuswin.erase()
1245 1245 self.chunkpad.erase()
1246 1246
1247 1247 printstring = self.printstring
1248 1248
1249 1249 # print out the status lines at the top
1250 1250 try:
1251 1251 for line in self._getstatuslines():
1252 1252 printstring(self.statuswin, line, pairname=b"legend")
1253 1253 self.statuswin.refresh()
1254 1254 except curses.error:
1255 1255 pass
1256 1256 if self.errorstr is not None:
1257 1257 return
1258 1258
1259 1259 # print out the patch in the remaining part of the window
1260 1260 try:
1261 1261 self.printitem()
1262 1262 self.updatescroll()
1263 1263 self.chunkpad.refresh(
1264 1264 self.firstlineofpadtoprint,
1265 1265 0,
1266 1266 self.numstatuslines,
1267 1267 0,
1268 1268 self.yscreensize - self.numstatuslines,
1269 1269 self.xscreensize - 1,
1270 1270 )
1271 1271 except curses.error:
1272 1272 pass
1273 1273
1274 1274 def getstatusprefixstring(self, item):
1275 1275 """
1276 1276 create a string to prefix a line with which indicates whether 'item'
1277 1277 is applied and/or folded.
1278 1278 """
1279 1279
1280 1280 # create checkbox string
1281 1281 if item.applied:
1282 1282 if not isinstance(item, uihunkline) and item.partial:
1283 1283 checkbox = b"[~]"
1284 1284 else:
1285 1285 checkbox = b"[x]"
1286 1286 else:
1287 1287 checkbox = b"[ ]"
1288 1288
1289 1289 try:
1290 1290 if item.folded:
1291 1291 checkbox += b"**"
1292 1292 if isinstance(item, uiheader):
1293 1293 # one of "m", "a", or "d" (modified, added, deleted)
1294 1294 filestatus = item.changetype
1295 1295
1296 1296 checkbox += filestatus + b" "
1297 1297 else:
1298 1298 checkbox += b" "
1299 1299 if isinstance(item, uiheader):
1300 1300 # add two more spaces for headers
1301 1301 checkbox += b" "
1302 1302 except AttributeError: # not foldable
1303 1303 checkbox += b" "
1304 1304
1305 1305 return checkbox
1306 1306
1307 1307 def printheader(
1308 1308 self, header, selected=False, towin=True, ignorefolding=False
1309 1309 ):
1310 1310 """
1311 1311 print the header to the pad. if countlines is True, don't print
1312 1312 anything, but just count the number of lines which would be printed.
1313 1313 """
1314 1314
1315 1315 outstr = b""
1316 1316 text = header.prettystr()
1317 1317 chunkindex = self.chunklist.index(header)
1318 1318
1319 1319 if chunkindex != 0 and not header.folded:
1320 1320 # add separating line before headers
1321 1321 outstr += self.printstring(
1322 1322 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1323 1323 )
1324 1324 # select color-pair based on if the header is selected
1325 1325 colorpair = self.getcolorpair(
1326 1326 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1327 1327 )
1328 1328
1329 1329 # print out each line of the chunk, expanding it to screen width
1330 1330
1331 1331 # number of characters to indent lines on this level by
1332 1332 indentnumchars = 0
1333 1333 checkbox = self.getstatusprefixstring(header)
1334 1334 if not header.folded or ignorefolding:
1335 1335 textlist = text.split(b"\n")
1336 1336 linestr = checkbox + textlist[0]
1337 1337 else:
1338 1338 linestr = checkbox + header.filename()
1339 1339 outstr += self.printstring(
1340 1340 self.chunkpad, linestr, pair=colorpair, towin=towin
1341 1341 )
1342 1342 if not header.folded or ignorefolding:
1343 1343 if len(textlist) > 1:
1344 1344 for line in textlist[1:]:
1345 1345 linestr = b" " * (indentnumchars + len(checkbox)) + line
1346 1346 outstr += self.printstring(
1347 1347 self.chunkpad, linestr, pair=colorpair, towin=towin
1348 1348 )
1349 1349
1350 1350 return outstr
1351 1351
1352 1352 def printhunklinesbefore(
1353 1353 self, hunk, selected=False, towin=True, ignorefolding=False
1354 1354 ):
1355 1355 """includes start/end line indicator"""
1356 1356 outstr = b""
1357 1357 # where hunk is in list of siblings
1358 1358 hunkindex = hunk.header.hunks.index(hunk)
1359 1359
1360 1360 if hunkindex != 0:
1361 1361 # add separating line before headers
1362 1362 outstr += self.printstring(
1363 1363 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1364 1364 )
1365 1365
1366 1366 colorpair = self.getcolorpair(
1367 1367 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1368 1368 )
1369 1369
1370 1370 # print out from-to line with checkbox
1371 1371 checkbox = self.getstatusprefixstring(hunk)
1372 1372
1373 1373 lineprefix = b" " * self.hunkindentnumchars + checkbox
1374 1374 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1375 1375
1376 1376 outstr += self.printstring(
1377 1377 self.chunkpad, lineprefix, towin=towin, align=False
1378 1378 ) # add uncolored checkbox/indent
1379 1379 outstr += self.printstring(
1380 1380 self.chunkpad, frtoline, pair=colorpair, towin=towin
1381 1381 )
1382 1382
1383 1383 if hunk.folded and not ignorefolding:
1384 1384 # skip remainder of output
1385 1385 return outstr
1386 1386
1387 1387 # print out lines of the chunk preceeding changed-lines
1388 1388 for line in hunk.before:
1389 1389 linestr = (
1390 1390 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1391 1391 )
1392 1392 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1393 1393
1394 1394 return outstr
1395 1395
1396 1396 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1397 1397 outstr = b""
1398 1398 if hunk.folded and not ignorefolding:
1399 1399 return outstr
1400 1400
1401 1401 # a bit superfluous, but to avoid hard-coding indent amount
1402 1402 checkbox = self.getstatusprefixstring(hunk)
1403 1403 for line in hunk.after:
1404 1404 linestr = (
1405 1405 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1406 1406 )
1407 1407 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1408 1408
1409 1409 return outstr
1410 1410
1411 1411 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1412 1412 outstr = b""
1413 1413 checkbox = self.getstatusprefixstring(hunkline)
1414 1414
1415 1415 linestr = hunkline.prettystr().strip(b"\n")
1416 1416
1417 1417 # select color-pair based on whether line is an addition/removal
1418 1418 if selected:
1419 1419 colorpair = self.getcolorpair(name=b"selected")
1420 1420 elif linestr.startswith(b"+"):
1421 1421 colorpair = self.getcolorpair(name=b"addition")
1422 1422 elif linestr.startswith(b"-"):
1423 1423 colorpair = self.getcolorpair(name=b"deletion")
1424 1424 elif linestr.startswith(b"\\"):
1425 1425 colorpair = self.getcolorpair(name=b"normal")
1426 1426
1427 1427 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1428 1428 outstr += self.printstring(
1429 1429 self.chunkpad, lineprefix, towin=towin, align=False
1430 1430 ) # add uncolored checkbox/indent
1431 1431 outstr += self.printstring(
1432 1432 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1433 1433 )
1434 1434 return outstr
1435 1435
1436 1436 def printitem(
1437 1437 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1438 1438 ):
1439 1439 """
1440 1440 use __printitem() to print the the specified item.applied.
1441 1441 if item is not specified, then print the entire patch.
1442 1442 (hiding folded elements, etc. -- see __printitem() docstring)
1443 1443 """
1444 1444
1445 1445 if item is None:
1446 1446 item = self.headerlist
1447 1447 if recursechildren:
1448 1448 self.linesprintedtopadsofar = 0
1449 1449
1450 1450 outstr = []
1451 1451 self.__printitem(
1452 1452 item, ignorefolding, recursechildren, outstr, towin=towin
1453 1453 )
1454 1454 return b''.join(outstr)
1455 1455
1456 1456 def outofdisplayedarea(self):
1457 1457 y, _ = self.chunkpad.getyx() # cursor location
1458 1458 # * 2 here works but an optimization would be the max number of
1459 1459 # consecutive non selectable lines
1460 1460 # i.e the max number of context line for any hunk in the patch
1461 1461 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1462 1462 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1463 1463 return y < miny or y > maxy
1464 1464
1465 1465 def handleselection(self, item, recursechildren):
1466 1466 selected = item is self.currentselecteditem
1467 1467 if selected and recursechildren:
1468 1468 # assumes line numbering starting from line 0
1469 1469 self.selecteditemstartline = self.linesprintedtopadsofar
1470 1470 selecteditemlines = self.getnumlinesdisplayed(
1471 1471 item, recursechildren=False
1472 1472 )
1473 1473 self.selecteditemendline = (
1474 1474 self.selecteditemstartline + selecteditemlines - 1
1475 1475 )
1476 1476 return selected
1477 1477
1478 1478 def __printitem(
1479 1479 self, item, ignorefolding, recursechildren, outstr, towin=True
1480 1480 ):
1481 1481 """
1482 1482 recursive method for printing out patch/header/hunk/hunk-line data to
1483 1483 screen. also returns a string with all of the content of the displayed
1484 1484 patch (not including coloring, etc.).
1485 1485
1486 1486 if ignorefolding is True, then folded items are printed out.
1487 1487
1488 1488 if recursechildren is False, then only print the item without its
1489 1489 child items.
1490 1490 """
1491 1491
1492 1492 if towin and self.outofdisplayedarea():
1493 1493 return
1494 1494
1495 1495 selected = self.handleselection(item, recursechildren)
1496 1496
1497 1497 # patch object is a list of headers
1498 1498 if isinstance(item, patch):
1499 1499 if recursechildren:
1500 1500 for hdr in item:
1501 1501 self.__printitem(
1502 1502 hdr, ignorefolding, recursechildren, outstr, towin
1503 1503 )
1504 1504 # todo: eliminate all isinstance() calls
1505 1505 if isinstance(item, uiheader):
1506 1506 outstr.append(
1507 1507 self.printheader(
1508 1508 item, selected, towin=towin, ignorefolding=ignorefolding
1509 1509 )
1510 1510 )
1511 1511 if recursechildren:
1512 1512 for hnk in item.hunks:
1513 1513 self.__printitem(
1514 1514 hnk, ignorefolding, recursechildren, outstr, towin
1515 1515 )
1516 1516 elif isinstance(item, uihunk) and (
1517 1517 (not item.header.folded) or ignorefolding
1518 1518 ):
1519 1519 # print the hunk data which comes before the changed-lines
1520 1520 outstr.append(
1521 1521 self.printhunklinesbefore(
1522 1522 item, selected, towin=towin, ignorefolding=ignorefolding
1523 1523 )
1524 1524 )
1525 1525 if recursechildren:
1526 1526 for l in item.changedlines:
1527 1527 self.__printitem(
1528 1528 l, ignorefolding, recursechildren, outstr, towin
1529 1529 )
1530 1530 outstr.append(
1531 1531 self.printhunklinesafter(
1532 1532 item, towin=towin, ignorefolding=ignorefolding
1533 1533 )
1534 1534 )
1535 1535 elif isinstance(item, uihunkline) and (
1536 1536 (not item.hunk.folded) or ignorefolding
1537 1537 ):
1538 1538 outstr.append(
1539 1539 self.printhunkchangedline(item, selected, towin=towin)
1540 1540 )
1541 1541
1542 1542 return outstr
1543 1543
1544 1544 def getnumlinesdisplayed(
1545 1545 self, item=None, ignorefolding=False, recursechildren=True
1546 1546 ):
1547 1547 """
1548 1548 return the number of lines which would be displayed if the item were
1549 1549 to be printed to the display. the item will not be printed to the
1550 1550 display (pad).
1551 1551 if no item is given, assume the entire patch.
1552 1552 if ignorefolding is True, folded items will be unfolded when counting
1553 1553 the number of lines.
1554 1554 """
1555 1555
1556 1556 # temporarily disable printing to windows by printstring
1557 1557 patchdisplaystring = self.printitem(
1558 1558 item, ignorefolding, recursechildren, towin=False
1559 1559 )
1560 1560 numlines = len(patchdisplaystring) // self.xscreensize
1561 1561 return numlines
1562 1562
1563 1563 def sigwinchhandler(self, n, frame):
1564 1564 """handle window resizing"""
1565 1565 try:
1566 1566 curses.endwin()
1567 1567 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1568 1568 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1569 1569 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1570 1570 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1571 1571 except curses.error:
1572 1572 pass
1573 1573
1574 1574 def getcolorpair(
1575 1575 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1576 1576 ):
1577 1577 """
1578 1578 get a curses color pair, adding it to self.colorpairs if it is not
1579 1579 already defined. an optional string, name, can be passed as a shortcut
1580 1580 for referring to the color-pair. by default, if no arguments are
1581 1581 specified, the white foreground / black background color-pair is
1582 1582 returned.
1583 1583
1584 1584 it is expected that this function will be used exclusively for
1585 1585 initializing color pairs, and not curses.init_pair().
1586 1586
1587 1587 attrlist is used to 'flavor' the returned color-pair. this information
1588 1588 is not stored in self.colorpairs. it contains attribute values like
1589 1589 curses.A_BOLD.
1590 1590 """
1591 1591
1592 1592 if (name is not None) and name in self.colorpairnames:
1593 1593 # then get the associated color pair and return it
1594 1594 colorpair = self.colorpairnames[name]
1595 1595 else:
1596 1596 if fgcolor is None:
1597 1597 fgcolor = -1
1598 1598 if bgcolor is None:
1599 1599 bgcolor = -1
1600 1600 if (fgcolor, bgcolor) in self.colorpairs:
1601 1601 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1602 1602 else:
1603 1603 pairindex = len(self.colorpairs) + 1
1604 1604 if self.usecolor:
1605 1605 curses.init_pair(pairindex, fgcolor, bgcolor)
1606 1606 colorpair = self.colorpairs[
1607 1607 (fgcolor, bgcolor)
1608 1608 ] = curses.color_pair(pairindex)
1609 1609 if name is not None:
1610 1610 self.colorpairnames[name] = curses.color_pair(pairindex)
1611 1611 else:
1612 1612 cval = 0
1613 1613 if name is not None:
1614 1614 if name == b'selected':
1615 1615 cval = curses.A_REVERSE
1616 1616 self.colorpairnames[name] = cval
1617 1617 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1618 1618
1619 1619 # add attributes if possible
1620 1620 if attrlist is None:
1621 1621 attrlist = []
1622 1622 if colorpair < 256:
1623 1623 # then it is safe to apply all attributes
1624 1624 for textattr in attrlist:
1625 1625 colorpair |= textattr
1626 1626 else:
1627 1627 # just apply a select few (safe?) attributes
1628 1628 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1629 1629 if textattrib in attrlist:
1630 1630 colorpair |= textattrib
1631 1631 return colorpair
1632 1632
1633 1633 def initcolorpair(self, *args, **kwargs):
1634 1634 """same as getcolorpair."""
1635 1635 self.getcolorpair(*args, **kwargs)
1636 1636
1637 1637 def helpwindow(self):
1638 1638 """print a help window to the screen. exit after any keypress."""
1639 1639 helptext = _(
1640 1640 b""" [press any key to return to the patch-display]
1641 1641
1642 1642 The curses hunk selector allows you to interactively choose among the
1643 1643 changes you have made, and confirm only those changes you select for
1644 1644 further processing by the command you are running (such as commit,
1645 1645 shelve, or revert). After confirming the selected changes, the
1646 1646 unselected changes are still present in your working copy, so you can
1647 1647 use the hunk selector multiple times to split large changes into
1648 1648 smaller changesets. the following are valid keystrokes:
1649 1649
1650 1650 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1651 1651 [enter] : (un-)select item and go to next item of same type
1652 1652 A : (un-)select all items
1653 1653 X : (un-)select all items between current and most-recent
1654 1654 up/down-arrow [k/j] : go to previous/next unfolded item
1655 1655 pgup/pgdn [K/J] : go to previous/next item of same type
1656 1656 right/left-arrow [l/h] : go to child item / parent item
1657 1657 shift-left-arrow [H] : go to parent header / fold selected header
1658 1658 g : go to the top
1659 1659 G : go to the bottom
1660 1660 f : fold / unfold item, hiding/revealing its children
1661 1661 F : fold / unfold parent item and all of its ancestors
1662 1662 ctrl-l : scroll the selected line to the top of the screen
1663 1663 m : edit / resume editing the commit message
1664 1664 e : edit the currently selected hunk
1665 1665 a : toggle all selections
1666 1666 c : confirm selected changes
1667 1667 r : review/edit and confirm selected changes
1668 / : regex search for code or filename
1669 n : next search result for code or filename
1670 N : previous search result for code or filename
1668 1671 q : quit without confirming (no changes will be made)
1669 1672 ? : help (what you're currently reading)"""
1670 1673 )
1671 1674
1672 1675 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1673 1676 helplines = helptext.split(b"\n")
1674 1677 helplines = helplines + [b" "] * (
1675 1678 self.yscreensize - self.numstatuslines - len(helplines) - 1
1676 1679 )
1677 1680 try:
1678 1681 for line in helplines:
1679 1682 self.printstring(helpwin, line, pairname=b"legend")
1680 1683 except curses.error:
1681 1684 pass
1682 1685 helpwin.refresh()
1683 1686 try:
1684 1687 with self.ui.timeblockedsection(b'crecord'):
1685 1688 helpwin.getkey()
1686 1689 except curses.error:
1687 1690 pass
1688 1691
1689 1692 def commitMessageWindow(self):
1690 1693 """Create a temporary commit message editing window on the screen."""
1691 1694
1692 1695 curses.raw()
1693 1696 curses.def_prog_mode()
1694 1697 curses.endwin()
1695 1698 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1696 1699 curses.cbreak()
1697 1700 self.stdscr.refresh()
1698 1701 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1699 1702
1700 1703 def handlefirstlineevent(self):
1701 1704 """
1702 1705 Handle 'g' to navigate to the top most file in the ncurses window.
1703 1706 """
1704 1707 self.currentselecteditem = self.headerlist[0]
1705 1708 currentitem = self.currentselecteditem
1706 1709 # select the parent item recursively until we're at a header
1707 1710 while True:
1708 1711 nextitem = currentitem.parentitem()
1709 1712 if nextitem is None:
1710 1713 break
1711 1714 else:
1712 1715 currentitem = nextitem
1713 1716
1714 1717 self.currentselecteditem = currentitem
1715 1718
1716 1719 def handlelastlineevent(self):
1717 1720 """
1718 1721 Handle 'G' to navigate to the bottom most file/hunk/line depending
1719 1722 on the whether the fold is active or not.
1720 1723
1721 1724 If the bottom most file is folded, it navigates to that file and
1722 1725 stops there. If the bottom most file is unfolded, it navigates to
1723 1726 the bottom most hunk in that file and stops there. If the bottom most
1724 1727 hunk is unfolded, it navigates to the bottom most line in that hunk.
1725 1728 """
1726 1729 currentitem = self.currentselecteditem
1727 1730 nextitem = currentitem.nextitem()
1728 1731 # select the child item recursively until we're at a footer
1729 1732 while nextitem is not None:
1730 1733 nextitem = currentitem.nextitem()
1731 1734 if nextitem is None:
1732 1735 break
1733 1736 else:
1734 1737 currentitem = nextitem
1735 1738
1736 1739 self.currentselecteditem = currentitem
1737 1740 self.recenterdisplayedarea()
1738 1741
1739 1742 def confirmationwindow(self, windowtext):
1740 1743 """display an informational window, then wait for and return a
1741 1744 keypress."""
1742 1745
1743 1746 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1744 1747 try:
1745 1748 lines = windowtext.split(b"\n")
1746 1749 for line in lines:
1747 1750 self.printstring(confirmwin, line, pairname=b"selected")
1748 1751 except curses.error:
1749 1752 pass
1750 1753 self.stdscr.refresh()
1751 1754 confirmwin.refresh()
1752 1755 try:
1753 1756 with self.ui.timeblockedsection(b'crecord'):
1754 1757 response = chr(self.stdscr.getch())
1755 1758 except ValueError:
1756 1759 response = None
1757 1760
1758 1761 return response
1759 1762
1760 1763 def reviewcommit(self):
1761 1764 """ask for 'y' to be pressed to confirm selected. return True if
1762 1765 confirmed."""
1763 1766 confirmtext = _(
1764 1767 b"""If you answer yes to the following, your currently chosen patch chunks
1765 1768 will be loaded into an editor. To modify the patch, make the changes in your
1766 1769 editor and save. To accept the current patch as-is, close the editor without
1767 1770 saving.
1768 1771
1769 1772 note: don't add/remove lines unless you also modify the range information.
1770 1773 failing to follow this rule will result in the commit aborting.
1771 1774
1772 1775 are you sure you want to review/edit and confirm the selected changes [yn]?
1773 1776 """
1774 1777 )
1775 1778 with self.ui.timeblockedsection(b'crecord'):
1776 1779 response = self.confirmationwindow(confirmtext)
1777 1780 if response is None:
1778 1781 response = "n"
1779 1782 if response.lower().startswith("y"):
1780 1783 return True
1781 1784 else:
1782 1785 return False
1783 1786
1784 1787 def recenterdisplayedarea(self):
1785 1788 """
1786 1789 once we scrolled with pg up pg down we can be pointing outside of the
1787 1790 display zone. we print the patch with towin=False to compute the
1788 1791 location of the selected item even though it is outside of the displayed
1789 1792 zone and then update the scroll.
1790 1793 """
1791 1794 self.printitem(towin=False)
1792 1795 self.updatescroll()
1793 1796
1794 1797 def toggleedit(self, item=None, test=False):
1795 1798 """
1796 1799 edit the currently selected chunk
1797 1800 """
1798 1801
1799 1802 def updateui(self):
1800 1803 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1801 1804 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1802 1805 self.updatescroll()
1803 1806 self.stdscr.refresh()
1804 1807 self.statuswin.refresh()
1805 1808 self.stdscr.keypad(1)
1806 1809
1807 1810 def editpatchwitheditor(self, chunk):
1808 1811 if chunk is None:
1809 1812 self.ui.write(_(b'cannot edit patch for whole file'))
1810 1813 self.ui.write(b"\n")
1811 1814 return None
1812 1815 if chunk.header.binary():
1813 1816 self.ui.write(_(b'cannot edit patch for binary file'))
1814 1817 self.ui.write(b"\n")
1815 1818 return None
1816 1819
1817 1820 # write the initial patch
1818 1821 patch = stringio()
1819 1822 patch.write(diffhelptext + hunkhelptext)
1820 1823 chunk.header.write(patch)
1821 1824 chunk.write(patch)
1822 1825
1823 1826 # start the editor and wait for it to complete
1824 1827 try:
1825 1828 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1826 1829 except error.Abort as exc:
1827 1830 self.errorstr = exc.message
1828 1831 return None
1829 1832 finally:
1830 1833 self.stdscr.clear()
1831 1834 self.stdscr.refresh()
1832 1835
1833 1836 # remove comment lines
1834 1837 patch = [
1835 1838 line + b'\n'
1836 1839 for line in patch.splitlines()
1837 1840 if not line.startswith(b'#')
1838 1841 ]
1839 1842 return patchmod.parsepatch(patch)
1840 1843
1841 1844 if item is None:
1842 1845 item = self.currentselecteditem
1843 1846 if isinstance(item, uiheader):
1844 1847 return
1845 1848 if isinstance(item, uihunkline):
1846 1849 item = item.parentitem()
1847 1850 if not isinstance(item, uihunk):
1848 1851 return
1849 1852
1850 1853 # To go back to that hunk or its replacement at the end of the edit
1851 1854 itemindex = item.parentitem().hunks.index(item)
1852 1855
1853 1856 beforeadded, beforeremoved = item.added, item.removed
1854 1857 newpatches = editpatchwitheditor(self, item)
1855 1858 if newpatches is None:
1856 1859 if not test:
1857 1860 updateui(self)
1858 1861 return
1859 1862 header = item.header
1860 1863 editedhunkindex = header.hunks.index(item)
1861 1864 hunksbefore = header.hunks[:editedhunkindex]
1862 1865 hunksafter = header.hunks[editedhunkindex + 1 :]
1863 1866 newpatchheader = newpatches[0]
1864 1867 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1865 1868 newadded = sum([h.added for h in newhunks])
1866 1869 newremoved = sum([h.removed for h in newhunks])
1867 1870 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1868 1871
1869 1872 for h in hunksafter:
1870 1873 h.toline += offset
1871 1874 for h in newhunks:
1872 1875 h.folded = False
1873 1876 header.hunks = hunksbefore + newhunks + hunksafter
1874 1877 if self.emptypatch():
1875 1878 header.hunks = hunksbefore + [item] + hunksafter
1876 1879 self.currentselecteditem = header
1877 1880 if len(header.hunks) > itemindex:
1878 1881 self.currentselecteditem = header.hunks[itemindex]
1879 1882
1880 1883 if not test:
1881 1884 updateui(self)
1882 1885
1883 1886 def emptypatch(self):
1884 1887 item = self.headerlist
1885 1888 if not item:
1886 1889 return True
1887 1890 for header in item:
1888 1891 if header.hunks:
1889 1892 return False
1890 1893 return True
1891 1894
1892 1895 def handlesearch(self):
1893 1896 win = curses.newwin(1, self.xscreensize, self.yscreensize - 1, 0)
1894 1897 win.echochar("/")
1895 1898
1896 1899 curses.echo()
1897 1900 curses.curs_set(1)
1898 1901 self.regex = win.getstr() or None
1899 1902 curses.noecho()
1900 1903 curses.curs_set(0)
1901 1904
1902 1905 if not self.showsearch(self.regex):
1903 1906 self.printstring(
1904 1907 win,
1905 1908 _(b"Pattern not found (press ENTER)"),
1906 1909 pairname=b"legend",
1907 1910 align=False,
1908 1911 )
1909 1912 while win.getkey() not in ["\n", "KEY_ENTER"]:
1910 1913 pass
1911 1914 del win
1912 1915
1913 1916 self.stdscr.clear()
1914 1917 self.stdscr.refresh()
1915 1918
1916 1919 def showsearch(self, regex, forward=True):
1917 1920 if not regex:
1918 1921 return
1919 1922
1920 1923 moveattr = 'nextitem' if forward else 'previtem'
1921 1924 currentitem = getattr(self.currentselecteditem, moveattr)(
1922 1925 skipfolded=False
1923 1926 )
1924 1927
1925 1928 matches = None
1926 1929 regex = re.compile(regex)
1927 1930 while currentitem:
1928 1931 matches = regex.search(currentitem.content)
1929 1932 if matches:
1930 1933 self.currentselecteditem = currentitem
1931 1934 break
1932 1935 currentitem = getattr(currentitem, moveattr)(skipfolded=False)
1933 1936
1934 1937 # Whatever is selected should now be visible
1935 1938 unfolded = self.currentselecteditem
1936 1939 while matches and unfolded:
1937 1940 unfolded.folded = False
1938 1941 unfolded = unfolded.parentitem()
1939 1942
1940 1943 return matches
1941 1944
1942 1945 def searchdirection(self, failuremsg, forward=True):
1943 1946 if not self.regex:
1944 1947 return
1945 1948
1946 1949 if not self.showsearch(self.regex, forward=forward):
1947 1950 win = curses.newwin(1, self.xscreensize, self.yscreensize - 1, 0)
1948 1951 self.printstring(win, failuremsg, pairname=b"legend", align=False)
1949 1952 while win.getkey() not in ["\n", "KEY_ENTER"]:
1950 1953 pass
1951 1954 del win
1952 1955
1953 1956 self.stdscr.clear()
1954 1957 self.stdscr.refresh()
1955 1958
1956 1959 def handlenextsearch(self):
1957 1960 self.searchdirection(
1958 1961 _(b"Next pattern not found (press ENTER)"), forward=True
1959 1962 )
1960 1963
1961 1964 def handleprevsearch(self):
1962 1965 self.searchdirection(
1963 1966 _(b"Previous pattern not found (press ENTER)"), forward=False
1964 1967 )
1965 1968
1966 1969 def handlekeypressed(self, keypressed, test=False):
1967 1970 """
1968 1971 Perform actions based on pressed keys.
1969 1972
1970 1973 Return true to exit the main loop.
1971 1974 """
1972 1975 if keypressed in ["k", "KEY_UP"]:
1973 1976 self.uparrowevent()
1974 1977 elif keypressed in ["K", "KEY_PPAGE"]:
1975 1978 self.uparrowshiftevent()
1976 1979 elif keypressed in ["j", "KEY_DOWN"]:
1977 1980 self.downarrowevent()
1978 1981 elif keypressed in ["J", "KEY_NPAGE"]:
1979 1982 self.downarrowshiftevent()
1980 1983 elif keypressed in ["l", "KEY_RIGHT"]:
1981 1984 self.rightarrowevent()
1982 1985 elif keypressed in ["h", "KEY_LEFT"]:
1983 1986 self.leftarrowevent()
1984 1987 elif keypressed in ["H", "KEY_SLEFT"]:
1985 1988 self.leftarrowshiftevent()
1986 1989 elif keypressed in ["q"]:
1987 1990 raise error.CanceledError(_(b'user quit'))
1988 1991 elif keypressed in ['a']:
1989 1992 self.flipselections()
1990 1993 elif keypressed in ["c"]:
1991 1994 return True
1992 1995 elif keypressed in ["r"]:
1993 1996 if self.reviewcommit():
1994 1997 self.opts[b'review'] = True
1995 1998 return True
1996 1999 elif test and keypressed in ["R"]:
1997 2000 self.opts[b'review'] = True
1998 2001 return True
1999 2002 elif keypressed in [" ", "x"]:
2000 2003 self.toggleapply()
2001 2004 elif keypressed in ["\n", "KEY_ENTER"]:
2002 2005 self.toggleapply()
2003 2006 self.nextsametype(test=test)
2004 2007 elif keypressed in ["X"]:
2005 2008 self.toggleallbetween()
2006 2009 elif keypressed in ["A"]:
2007 2010 self.toggleall()
2008 2011 elif keypressed in ["e"]:
2009 2012 self.toggleedit(test=test)
2010 2013 elif keypressed in ["f"]:
2011 2014 self.togglefolded()
2012 2015 elif keypressed in ["F"]:
2013 2016 self.togglefolded(foldparent=True)
2014 2017 elif keypressed in ["m"]:
2015 2018 self.commitMessageWindow()
2019 elif keypressed in ["/"]:
2020 self.handlesearch()
2021 elif keypressed in ["n"]:
2022 self.handlenextsearch()
2023 elif keypressed in ["N"]:
2024 self.handleprevsearch()
2016 2025 elif keypressed in ["g", "KEY_HOME"]:
2017 2026 self.handlefirstlineevent()
2018 2027 elif keypressed in ["G", "KEY_END"]:
2019 2028 self.handlelastlineevent()
2020 2029 elif keypressed in ["?"]:
2021 2030 self.helpwindow()
2022 2031 self.stdscr.clear()
2023 2032 self.stdscr.refresh()
2024 2033 elif keypressed in [curses.ascii.ctrl("L")]:
2025 2034 # scroll the current line to the top of the screen, and redraw
2026 2035 # everything
2027 2036 self.scrolllines(self.selecteditemstartline)
2028 2037 self.stdscr.clear()
2029 2038 self.stdscr.refresh()
2030 2039
2031 2040 def main(self, stdscr):
2032 2041 """
2033 2042 method to be wrapped by curses.wrapper() for selecting chunks.
2034 2043 """
2035 2044
2036 2045 origsigwinch = sentinel = object()
2037 2046 if hasattr(signal, 'SIGWINCH'):
2038 2047 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
2039 2048 try:
2040 2049 return self._main(stdscr)
2041 2050 finally:
2042 2051 if origsigwinch is not sentinel:
2043 2052 signal.signal(signal.SIGWINCH, origsigwinch)
2044 2053
2045 2054 def _main(self, stdscr):
2046 2055 self.stdscr = stdscr
2047 2056 # error during initialization, cannot be printed in the curses
2048 2057 # interface, it should be printed by the calling code
2049 2058 self.initexc = None
2050 2059 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
2051 2060
2052 2061 curses.start_color()
2053 2062 try:
2054 2063 curses.use_default_colors()
2055 2064 except curses.error:
2056 2065 self.usecolor = False
2057 2066
2058 2067 # In some situations we may have some cruft left on the "alternate
2059 2068 # screen" from another program (or previous iterations of ourself), and
2060 2069 # we won't clear it if the scroll region is small enough to comfortably
2061 2070 # fit on the terminal.
2062 2071 self.stdscr.clear()
2063 2072
2064 2073 # don't display the cursor
2065 2074 try:
2066 2075 curses.curs_set(0)
2067 2076 except curses.error:
2068 2077 pass
2069 2078
2070 2079 # available colors: black, blue, cyan, green, magenta, white, yellow
2071 2080 # init_pair(color_id, foreground_color, background_color)
2072 2081 self.initcolorpair(None, None, name=b"normal")
2073 2082 self.initcolorpair(
2074 2083 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
2075 2084 )
2076 2085 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
2077 2086 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
2078 2087 self.initcolorpair(
2079 2088 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
2080 2089 )
2081 2090 # newwin([height, width,] begin_y, begin_x)
2082 2091 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
2083 2092 self.statuswin.keypad(True) # interpret arrow-key, etc. esc sequences
2084 2093
2085 2094 # figure out how much space to allocate for the chunk-pad which is
2086 2095 # used for displaying the patch
2087 2096
2088 2097 # stupid hack to prevent getnumlinesdisplayed from failing
2089 2098 self.chunkpad = curses.newpad(1, self.xscreensize)
2090 2099
2091 2100 # add 1 so to account for last line text reaching end of line
2092 2101 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2093 2102
2094 2103 try:
2095 2104 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2096 2105 except curses.error:
2097 2106 self.initexc = fallbackerror(
2098 2107 _(b'this diff is too large to be displayed')
2099 2108 )
2100 2109 return
2101 2110 # initialize selecteditemendline (initial start-line is 0)
2102 2111 self.selecteditemendline = self.getnumlinesdisplayed(
2103 2112 self.currentselecteditem, recursechildren=False
2104 2113 )
2105 2114
2106 2115 while True:
2107 2116 self.updatescreen()
2108 2117 try:
2109 2118 with self.ui.timeblockedsection(b'crecord'):
2110 2119 keypressed = self.statuswin.getkey()
2111 2120 if self.errorstr is not None:
2112 2121 self.errorstr = None
2113 2122 continue
2114 2123 except curses.error:
2115 2124 keypressed = b"foobar"
2116 2125 if self.handlekeypressed(keypressed):
2117 2126 break
2118 2127
2119 2128 if self.commenttext != b"":
2120 2129 whitespaceremoved = re.sub(
2121 2130 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2122 2131 )
2123 2132 if whitespaceremoved != b"":
2124 2133 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now