##// END OF EJS Templates
comments: save comments that are not rendered to be displayed as outdated....
marcink -
r1258:70c673b5 default
parent child Browse files
Show More
@@ -1,668 +1,687 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from rhodecode.lib.helpers import (
28 28 get_lexer_for_filenode, get_lexer_safe, html_escape)
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.lib.vcs.nodes import FileNode
31 31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 32 from rhodecode.lib.diffs import LimitedDiffContainer
33 33 from pygments.lexers import get_lexer_by_name
34 34
35 35 plain_text_lexer = get_lexer_by_name(
36 36 'text', stripall=False, stripnl=False, ensurenl=False)
37 37
38 38
39 39 log = logging.getLogger()
40 40
41 41
42 42 def filenode_as_lines_tokens(filenode, lexer=None):
43 43 lexer = lexer or get_lexer_for_filenode(filenode)
44 44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
45 45 tokens = tokenize_string(filenode.content, lexer)
46 46 lines = split_token_stream(tokens, split_string='\n')
47 47 rv = list(lines)
48 48 return rv
49 49
50 50
51 51 def tokenize_string(content, lexer):
52 52 """
53 53 Use pygments to tokenize some content based on a lexer
54 54 ensuring all original new lines and whitespace is preserved
55 55 """
56 56
57 57 lexer.stripall = False
58 58 lexer.stripnl = False
59 59 lexer.ensurenl = False
60 60 for token_type, token_text in lex(content, lexer):
61 61 yield pygment_token_class(token_type), token_text
62 62
63 63
64 64 def split_token_stream(tokens, split_string=u'\n'):
65 65 """
66 66 Take a list of (TokenType, text) tuples and split them by a string
67 67
68 68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
69 69 [(TEXT, 'some'), (TEXT, 'text'),
70 70 (TEXT, 'more'), (TEXT, 'text')]
71 71 """
72 72
73 73 buffer = []
74 74 for token_class, token_text in tokens:
75 75 parts = token_text.split(split_string)
76 76 for part in parts[:-1]:
77 77 buffer.append((token_class, part))
78 78 yield buffer
79 79 buffer = []
80 80
81 81 buffer.append((token_class, parts[-1]))
82 82
83 83 if buffer:
84 84 yield buffer
85 85
86 86
87 87 def filenode_as_annotated_lines_tokens(filenode):
88 88 """
89 89 Take a file node and return a list of annotations => lines, if no annotation
90 90 is found, it will be None.
91 91
92 92 eg:
93 93
94 94 [
95 95 (annotation1, [
96 96 (1, line1_tokens_list),
97 97 (2, line2_tokens_list),
98 98 ]),
99 99 (annotation2, [
100 100 (3, line1_tokens_list),
101 101 ]),
102 102 (None, [
103 103 (4, line1_tokens_list),
104 104 ]),
105 105 (annotation1, [
106 106 (5, line1_tokens_list),
107 107 (6, line2_tokens_list),
108 108 ])
109 109 ]
110 110 """
111 111
112 112 commit_cache = {} # cache commit_getter lookups
113 113
114 114 def _get_annotation(commit_id, commit_getter):
115 115 if commit_id not in commit_cache:
116 116 commit_cache[commit_id] = commit_getter()
117 117 return commit_cache[commit_id]
118 118
119 119 annotation_lookup = {
120 120 line_no: _get_annotation(commit_id, commit_getter)
121 121 for line_no, commit_id, commit_getter, line_content
122 122 in filenode.annotate
123 123 }
124 124
125 125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
126 126 for line_no, tokens
127 127 in enumerate(filenode_as_lines_tokens(filenode), 1))
128 128
129 129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
130 130
131 131 for annotation, group in grouped_annotations_lines:
132 132 yield (
133 133 annotation, [(line_no, tokens)
134 134 for (_, line_no, tokens) in group]
135 135 )
136 136
137 137
138 138 def render_tokenstream(tokenstream):
139 139 result = []
140 140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
141 141
142 142 if token_class:
143 143 result.append(u'<span class="%s">' % token_class)
144 144 else:
145 145 result.append(u'<span>')
146 146
147 147 for op_tag, token_text in token_ops_texts:
148 148
149 149 if op_tag:
150 150 result.append(u'<%s>' % op_tag)
151 151
152 152 escaped_text = html_escape(token_text)
153 153
154 154 # TODO: dan: investigate showing hidden characters like space/nl/tab
155 155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
156 156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
157 157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
158 158
159 159 result.append(escaped_text)
160 160
161 161 if op_tag:
162 162 result.append(u'</%s>' % op_tag)
163 163
164 164 result.append(u'</span>')
165 165
166 166 html = ''.join(result)
167 167 return html
168 168
169 169
170 170 def rollup_tokenstream(tokenstream):
171 171 """
172 172 Group a token stream of the format:
173 173
174 174 ('class', 'op', 'text')
175 175 or
176 176 ('class', 'text')
177 177
178 178 into
179 179
180 180 [('class1',
181 181 [('op1', 'text'),
182 182 ('op2', 'text')]),
183 183 ('class2',
184 184 [('op3', 'text')])]
185 185
186 186 This is used to get the minimal tags necessary when
187 187 rendering to html eg for a token stream ie.
188 188
189 189 <span class="A"><ins>he</ins>llo</span>
190 190 vs
191 191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
192 192
193 193 If a 2 tuple is passed in, the output op will be an empty string.
194 194
195 195 eg:
196 196
197 197 >>> rollup_tokenstream([('classA', '', 'h'),
198 198 ('classA', 'del', 'ell'),
199 199 ('classA', '', 'o'),
200 200 ('classB', '', ' '),
201 201 ('classA', '', 'the'),
202 202 ('classA', '', 're'),
203 203 ])
204 204
205 205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
206 206 ('classB', [('', ' ')],
207 207 ('classA', [('', 'there')]]
208 208
209 209 """
210 210 if tokenstream and len(tokenstream[0]) == 2:
211 211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
212 212
213 213 result = []
214 214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
215 215 ops = []
216 216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
217 217 text_buffer = []
218 218 for t_class, t_op, t_text in token_text_list:
219 219 text_buffer.append(t_text)
220 220 ops.append((token_op, ''.join(text_buffer)))
221 221 result.append((token_class, ops))
222 222 return result
223 223
224 224
225 225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
226 226 """
227 227 Converts a list of (token_class, token_text) tuples to a list of
228 228 (token_class, token_op, token_text) tuples where token_op is one of
229 229 ('ins', 'del', '')
230 230
231 231 :param old_tokens: list of (token_class, token_text) tuples of old line
232 232 :param new_tokens: list of (token_class, token_text) tuples of new line
233 233 :param use_diff_match_patch: boolean, will use google's diff match patch
234 234 library which has options to 'smooth' out the character by character
235 235 differences making nicer ins/del blocks
236 236 """
237 237
238 238 old_tokens_result = []
239 239 new_tokens_result = []
240 240
241 241 similarity = difflib.SequenceMatcher(None,
242 242 ''.join(token_text for token_class, token_text in old_tokens),
243 243 ''.join(token_text for token_class, token_text in new_tokens)
244 244 ).ratio()
245 245
246 246 if similarity < 0.6: # return, the blocks are too different
247 247 for token_class, token_text in old_tokens:
248 248 old_tokens_result.append((token_class, '', token_text))
249 249 for token_class, token_text in new_tokens:
250 250 new_tokens_result.append((token_class, '', token_text))
251 251 return old_tokens_result, new_tokens_result, similarity
252 252
253 253 token_sequence_matcher = difflib.SequenceMatcher(None,
254 254 [x[1] for x in old_tokens],
255 255 [x[1] for x in new_tokens])
256 256
257 257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
258 258 # check the differences by token block types first to give a more
259 259 # nicer "block" level replacement vs character diffs
260 260
261 261 if tag == 'equal':
262 262 for token_class, token_text in old_tokens[o1:o2]:
263 263 old_tokens_result.append((token_class, '', token_text))
264 264 for token_class, token_text in new_tokens[n1:n2]:
265 265 new_tokens_result.append((token_class, '', token_text))
266 266 elif tag == 'delete':
267 267 for token_class, token_text in old_tokens[o1:o2]:
268 268 old_tokens_result.append((token_class, 'del', token_text))
269 269 elif tag == 'insert':
270 270 for token_class, token_text in new_tokens[n1:n2]:
271 271 new_tokens_result.append((token_class, 'ins', token_text))
272 272 elif tag == 'replace':
273 273 # if same type token blocks must be replaced, do a diff on the
274 274 # characters in the token blocks to show individual changes
275 275
276 276 old_char_tokens = []
277 277 new_char_tokens = []
278 278 for token_class, token_text in old_tokens[o1:o2]:
279 279 for char in token_text:
280 280 old_char_tokens.append((token_class, char))
281 281
282 282 for token_class, token_text in new_tokens[n1:n2]:
283 283 for char in token_text:
284 284 new_char_tokens.append((token_class, char))
285 285
286 286 old_string = ''.join([token_text for
287 287 token_class, token_text in old_char_tokens])
288 288 new_string = ''.join([token_text for
289 289 token_class, token_text in new_char_tokens])
290 290
291 291 char_sequence = difflib.SequenceMatcher(
292 292 None, old_string, new_string)
293 293 copcodes = char_sequence.get_opcodes()
294 294 obuffer, nbuffer = [], []
295 295
296 296 if use_diff_match_patch:
297 297 dmp = diff_match_patch()
298 298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
299 299 reps = dmp.diff_main(old_string, new_string)
300 300 dmp.diff_cleanupEfficiency(reps)
301 301
302 302 a, b = 0, 0
303 303 for op, rep in reps:
304 304 l = len(rep)
305 305 if op == 0:
306 306 for i, c in enumerate(rep):
307 307 obuffer.append((old_char_tokens[a+i][0], '', c))
308 308 nbuffer.append((new_char_tokens[b+i][0], '', c))
309 309 a += l
310 310 b += l
311 311 elif op == -1:
312 312 for i, c in enumerate(rep):
313 313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
314 314 a += l
315 315 elif op == 1:
316 316 for i, c in enumerate(rep):
317 317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
318 318 b += l
319 319 else:
320 320 for ctag, co1, co2, cn1, cn2 in copcodes:
321 321 if ctag == 'equal':
322 322 for token_class, token_text in old_char_tokens[co1:co2]:
323 323 obuffer.append((token_class, '', token_text))
324 324 for token_class, token_text in new_char_tokens[cn1:cn2]:
325 325 nbuffer.append((token_class, '', token_text))
326 326 elif ctag == 'delete':
327 327 for token_class, token_text in old_char_tokens[co1:co2]:
328 328 obuffer.append((token_class, 'del', token_text))
329 329 elif ctag == 'insert':
330 330 for token_class, token_text in new_char_tokens[cn1:cn2]:
331 331 nbuffer.append((token_class, 'ins', token_text))
332 332 elif ctag == 'replace':
333 333 for token_class, token_text in old_char_tokens[co1:co2]:
334 334 obuffer.append((token_class, 'del', token_text))
335 335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 336 nbuffer.append((token_class, 'ins', token_text))
337 337
338 338 old_tokens_result.extend(obuffer)
339 339 new_tokens_result.extend(nbuffer)
340 340
341 341 return old_tokens_result, new_tokens_result, similarity
342 342
343 343
344 344 class DiffSet(object):
345 345 """
346 346 An object for parsing the diff result from diffs.DiffProcessor and
347 347 adding highlighting, side by side/unified renderings and line diffs
348 348 """
349 349
350 350 HL_REAL = 'REAL' # highlights using original file, slow
351 351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
352 352 # in the case of multiline code
353 353 HL_NONE = 'NONE' # no highlighting, fastest
354 354
355 355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
356 356 source_repo_name=None,
357 357 source_node_getter=lambda filename: None,
358 358 target_node_getter=lambda filename: None,
359 359 source_nodes=None, target_nodes=None,
360 360 max_file_size_limit=150 * 1024, # files over this size will
361 361 # use fast highlighting
362 362 comments=None,
363 363 ):
364 364
365 365 self.highlight_mode = highlight_mode
366 366 self.highlighted_filenodes = {}
367 367 self.source_node_getter = source_node_getter
368 368 self.target_node_getter = target_node_getter
369 369 self.source_nodes = source_nodes or {}
370 370 self.target_nodes = target_nodes or {}
371 371 self.repo_name = repo_name
372 372 self.source_repo_name = source_repo_name or repo_name
373 373 self.comments = comments or {}
374 self.comments_store = self.comments.copy()
374 375 self.max_file_size_limit = max_file_size_limit
375 376
376 377 def render_patchset(self, patchset, source_ref=None, target_ref=None):
377 378 diffset = AttributeDict(dict(
378 379 lines_added=0,
379 380 lines_deleted=0,
380 381 changed_files=0,
381 382 files=[],
382 383 limited_diff=isinstance(patchset, LimitedDiffContainer),
383 384 repo_name=self.repo_name,
384 385 source_repo_name=self.source_repo_name,
385 386 source_ref=source_ref,
386 387 target_ref=target_ref,
387 388 ))
388 389 for patch in patchset:
389 390 filediff = self.render_patch(patch)
390 391 filediff.diffset = diffset
391 392 diffset.files.append(filediff)
392 393 diffset.changed_files += 1
393 394 if not patch['stats']['binary']:
394 395 diffset.lines_added += patch['stats']['added']
395 396 diffset.lines_deleted += patch['stats']['deleted']
396 397
397 398 return diffset
398 399
399 400 _lexer_cache = {}
400 401 def _get_lexer_for_filename(self, filename):
401 402 # cached because we might need to call it twice for source/target
402 403 if filename not in self._lexer_cache:
403 404 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
404 405 return self._lexer_cache[filename]
405 406
406 407 def render_patch(self, patch):
407 408 log.debug('rendering diff for %r' % patch['filename'])
408 409
409 410 source_filename = patch['original_filename']
410 411 target_filename = patch['filename']
411 412
412 413 source_lexer = plain_text_lexer
413 414 target_lexer = plain_text_lexer
414 415
415 416 if not patch['stats']['binary']:
416 417 if self.highlight_mode == self.HL_REAL:
417 418 if (source_filename and patch['operation'] in ('D', 'M')
418 419 and source_filename not in self.source_nodes):
419 420 self.source_nodes[source_filename] = (
420 421 self.source_node_getter(source_filename))
421 422
422 423 if (target_filename and patch['operation'] in ('A', 'M')
423 424 and target_filename not in self.target_nodes):
424 425 self.target_nodes[target_filename] = (
425 426 self.target_node_getter(target_filename))
426 427
427 428 elif self.highlight_mode == self.HL_FAST:
428 429 source_lexer = self._get_lexer_for_filename(source_filename)
429 430 target_lexer = self._get_lexer_for_filename(target_filename)
430 431
431 432 source_file = self.source_nodes.get(source_filename, source_filename)
432 433 target_file = self.target_nodes.get(target_filename, target_filename)
433 434
434 435 source_filenode, target_filenode = None, None
435 436
436 437 # TODO: dan: FileNode.lexer works on the content of the file - which
437 438 # can be slow - issue #4289 explains a lexer clean up - which once
438 439 # done can allow caching a lexer for a filenode to avoid the file lookup
439 440 if isinstance(source_file, FileNode):
440 441 source_filenode = source_file
441 442 source_lexer = source_file.lexer
442 443 if isinstance(target_file, FileNode):
443 444 target_filenode = target_file
444 445 target_lexer = target_file.lexer
445 446
446 447 source_file_path, target_file_path = None, None
447 448
448 449 if source_filename != '/dev/null':
449 450 source_file_path = source_filename
450 451 if target_filename != '/dev/null':
451 452 target_file_path = target_filename
452 453
453 454 source_file_type = source_lexer.name
454 455 target_file_type = target_lexer.name
455 456
456 457 op_hunks = patch['chunks'][0]
457 458 hunks = patch['chunks'][1:]
458 459
459 460 filediff = AttributeDict({
460 461 'source_file_path': source_file_path,
461 462 'target_file_path': target_file_path,
462 463 'source_filenode': source_filenode,
463 464 'target_filenode': target_filenode,
464 465 'hunks': [],
465 466 'source_file_type': target_file_type,
466 467 'target_file_type': source_file_type,
467 468 'patch': patch,
468 469 'source_mode': patch['stats']['old_mode'],
469 470 'target_mode': patch['stats']['new_mode'],
470 471 'limited_diff': isinstance(patch, LimitedDiffContainer),
471 472 'diffset': self,
472 473 })
473 474
474 475 for hunk in hunks:
475 476 hunkbit = self.parse_hunk(hunk, source_file, target_file)
476 477 hunkbit.filediff = filediff
477 478 filediff.hunks.append(hunkbit)
479
480 left_comments = {}
481
482 if source_file_path in self.comments_store:
483 for lineno, comments in self.comments_store[source_file_path].items():
484 left_comments[lineno] = comments
485
486 if target_file_path in self.comments_store:
487 for lineno, comments in self.comments_store[target_file_path].items():
488 left_comments[lineno] = comments
489
490 filediff.left_comments = left_comments
478 491 return filediff
479 492
480 493 def parse_hunk(self, hunk, source_file, target_file):
481 494 result = AttributeDict(dict(
482 495 source_start=hunk['source_start'],
483 496 source_length=hunk['source_length'],
484 497 target_start=hunk['target_start'],
485 498 target_length=hunk['target_length'],
486 499 section_header=hunk['section_header'],
487 500 lines=[],
488 501 ))
489 502 before, after = [], []
490 503
491 504 for line in hunk['lines']:
492 505 if line['action'] == 'unmod':
493 506 result.lines.extend(
494 507 self.parse_lines(before, after, source_file, target_file))
495 508 after.append(line)
496 509 before.append(line)
497 510 elif line['action'] == 'add':
498 511 after.append(line)
499 512 elif line['action'] == 'del':
500 513 before.append(line)
501 514 elif line['action'] == 'old-no-nl':
502 515 before.append(line)
503 516 elif line['action'] == 'new-no-nl':
504 517 after.append(line)
505 518
506 519 result.lines.extend(
507 520 self.parse_lines(before, after, source_file, target_file))
508 521 result.unified = self.as_unified(result.lines)
509 522 result.sideside = result.lines
523
510 524 return result
511 525
512 526 def parse_lines(self, before_lines, after_lines, source_file, target_file):
513 527 # TODO: dan: investigate doing the diff comparison and fast highlighting
514 528 # on the entire before and after buffered block lines rather than by
515 529 # line, this means we can get better 'fast' highlighting if the context
516 530 # allows it - eg.
517 531 # line 4: """
518 532 # line 5: this gets highlighted as a string
519 533 # line 6: """
520 534
521 535 lines = []
522 536 while before_lines or after_lines:
523 537 before, after = None, None
524 538 before_tokens, after_tokens = None, None
525 539
526 540 if before_lines:
527 541 before = before_lines.pop(0)
528 542 if after_lines:
529 543 after = after_lines.pop(0)
530 544
531 545 original = AttributeDict()
532 546 modified = AttributeDict()
533 547
534 548 if before:
535 549 if before['action'] == 'old-no-nl':
536 550 before_tokens = [('nonl', before['line'])]
537 551 else:
538 552 before_tokens = self.get_line_tokens(
539 553 line_text=before['line'], line_number=before['old_lineno'],
540 554 file=source_file)
541 555 original.lineno = before['old_lineno']
542 556 original.content = before['line']
543 557 original.action = self.action_to_op(before['action'])
544 558 original.comments = self.get_comments_for('old',
545 559 source_file, before['old_lineno'])
546 560
547 561 if after:
548 562 if after['action'] == 'new-no-nl':
549 563 after_tokens = [('nonl', after['line'])]
550 564 else:
551 565 after_tokens = self.get_line_tokens(
552 566 line_text=after['line'], line_number=after['new_lineno'],
553 567 file=target_file)
554 568 modified.lineno = after['new_lineno']
555 569 modified.content = after['line']
556 570 modified.action = self.action_to_op(after['action'])
557 571 modified.comments = self.get_comments_for('new',
558 572 target_file, after['new_lineno'])
559 573
560 574 # diff the lines
561 575 if before_tokens and after_tokens:
562 576 o_tokens, m_tokens, similarity = tokens_diff(
563 577 before_tokens, after_tokens)
564 578 original.content = render_tokenstream(o_tokens)
565 579 modified.content = render_tokenstream(m_tokens)
566 580 elif before_tokens:
567 581 original.content = render_tokenstream(
568 582 [(x[0], '', x[1]) for x in before_tokens])
569 583 elif after_tokens:
570 584 modified.content = render_tokenstream(
571 585 [(x[0], '', x[1]) for x in after_tokens])
572 586
573 587 lines.append(AttributeDict({
574 588 'original': original,
575 589 'modified': modified,
576 590 }))
577 591
578 592 return lines
579 593
580 594 def get_comments_for(self, version, file, line_number):
581 595 if hasattr(file, 'unicode_path'):
582 596 file = file.unicode_path
583 597
584 598 if not isinstance(file, basestring):
585 599 return None
586 600
587 601 line_key = {
588 602 'old': 'o',
589 603 'new': 'n',
590 604 }[version] + str(line_number)
591 605
592 return self.comments.get(file, {}).get(line_key)
606 if file in self.comments_store:
607 file_comments = self.comments_store[file]
608 if line_key in file_comments:
609 return file_comments.pop(line_key)
593 610
594 611 def get_line_tokens(self, line_text, line_number, file=None):
595 612 filenode = None
596 613 filename = None
597 614
598 615 if isinstance(file, basestring):
599 616 filename = file
600 617 elif isinstance(file, FileNode):
601 618 filenode = file
602 619 filename = file.unicode_path
603 620
604 621 if self.highlight_mode == self.HL_REAL and filenode:
605 622 if line_number and file.size < self.max_file_size_limit:
606 623 return self.get_tokenized_filenode_line(file, line_number)
607 624
608 625 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
609 626 lexer = self._get_lexer_for_filename(filename)
610 627 return list(tokenize_string(line_text, lexer))
611 628
612 629 return list(tokenize_string(line_text, plain_text_lexer))
613 630
614 631 def get_tokenized_filenode_line(self, filenode, line_number):
615 632
616 633 if filenode not in self.highlighted_filenodes:
617 634 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
618 635 self.highlighted_filenodes[filenode] = tokenized_lines
619 636 return self.highlighted_filenodes[filenode][line_number - 1]
620 637
621 638 def action_to_op(self, action):
622 639 return {
623 640 'add': '+',
624 641 'del': '-',
625 642 'unmod': ' ',
626 643 'old-no-nl': ' ',
627 644 'new-no-nl': ' ',
628 645 }.get(action, action)
629 646
630 647 def as_unified(self, lines):
631 """ Return a generator that yields the lines of a diff in unified order """
648 """
649 Return a generator that yields the lines of a diff in unified order
650 """
632 651 def generator():
633 652 buf = []
634 653 for line in lines:
635 654
636 655 if buf and not line.original or line.original.action == ' ':
637 656 for b in buf:
638 657 yield b
639 658 buf = []
640 659
641 660 if line.original:
642 661 if line.original.action == ' ':
643 662 yield (line.original.lineno, line.modified.lineno,
644 663 line.original.action, line.original.content,
645 664 line.original.comments)
646 665 continue
647 666
648 667 if line.original.action == '-':
649 668 yield (line.original.lineno, None,
650 669 line.original.action, line.original.content,
651 670 line.original.comments)
652 671
653 672 if line.modified.action == '+':
654 673 buf.append((
655 674 None, line.modified.lineno,
656 675 line.modified.action, line.modified.content,
657 676 line.modified.comments))
658 677 continue
659 678
660 679 if line.modified:
661 680 yield (None, line.modified.lineno,
662 681 line.modified.action, line.modified.content,
663 682 line.modified.comments)
664 683
665 684 for b in buf:
666 685 yield b
667 686
668 687 return generator()
General Comments 0
You need to be logged in to leave comments. Login now