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