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