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