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