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