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