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