Skip to content

Commit 050734a

Browse files
committed
Intraline highlighting, formatting with Black
1 parent 338a5b1 commit 050734a

File tree

2 files changed

+130
-41
lines changed

2 files changed

+130
-41
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
2-
dunk/__pycache__
2+
dunk/__pycache__
3+
dist

dunk/dunk.py

+128-40
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import functools
22
import sys
3+
from difflib import SequenceMatcher
34
from pathlib import Path
4-
from typing import Dict, List, cast, Iterable, Tuple, TypeVar
5+
from typing import Dict, List, cast, Iterable, Tuple, TypeVar, Optional, Set
56

67
from rich.color import blend_rgb, Color
78
from rich.color_triplet import ColorTriplet
@@ -10,15 +11,18 @@
1011
from rich.style import Style
1112
from rich.syntax import Syntax
1213
from rich.table import Table
14+
from rich.text import Text
1315
from unidiff import PatchSet
1416
from unidiff.patch import PatchedFile, Hunk, Line
1517

1618
console = Console(force_terminal=True)
17-
T = TypeVar("T")
19+
1820
MONOKAI_LIGHT_ACCENT = Color.from_rgb(62, 64, 54)
1921
MONOKAI_BACKGROUND = Color.from_rgb(red=39, green=40, blue=34)
2022
DUNK_BACKGROUND = Color.from_rgb(red=26, green=30, blue=22)
2123

24+
T = TypeVar("T")
25+
2226

2327
# TODO: Use rich pager here?
2428

@@ -79,7 +83,8 @@ def main():
7983

8084
console.print()
8185
console.rule(
82-
f" [b]{patch.path}[/] ([green]{patch.added} additions[/], [red]{patch.removed} removals[/]) {additional_context}",
86+
f" [b]{patch.path}[/] ([green]{patch.added} additions[/], "
87+
f"[red]{patch.removed} removals[/]) {additional_context}",
8388
style="#45483d",
8489
characters="▁",
8590
)
@@ -88,8 +93,8 @@ def main():
8893
lexer = Syntax.guess_lexer(patch.path)
8994

9095
for is_first_hunk, hunk in loop_first(patch):
91-
# Use difflib to examine differences between each link of the hunk
92-
# Target essentially means the additions/green text in the diff
96+
# Use difflib to examine differences between each line of the hunk
97+
# Target essentially means the additions/green text in diff
9398
target_line_range = (
9499
hunk.target_start,
95100
hunk.target_length + hunk.target_start - 1,
@@ -113,8 +118,6 @@ def main():
113118
line_numbers=True,
114119
indent_guides=True,
115120
)
116-
117-
# Gather information on source which lines were added/removed, so we can highlight them
118121
source_removed_linenos = set()
119122
target_added_linenos = set()
120123
context_linenos = []
@@ -127,11 +130,13 @@ def main():
127130
elif line.is_context:
128131
context_linenos.append((line.source_line_no, line.target_line_no))
129132

130-
# To ensure that lines are aligned on the left and right in the split diff, we need to add some padding above the lines
131-
# the amount of padding can be calculated by *changes* in the difference in offset between the source and target context
132-
# line numbers. When a change occurs, we note how much the change was, and that's how much padding we need to add. If the
133-
# change in source - target context line numbers is positive, we pad above the target. If it's negative, we pad above the
134-
# source line.
133+
# To ensure that lines are aligned on the left and right in the split
134+
# diff, we need to add some padding above the lines the amount of padding
135+
# can be calculated by *changes* in the difference in offset between the
136+
# source and target context line numbers. When a change occurs, we note
137+
# how much the change was, and that's how much padding we need to add. If
138+
# the change in source - target context line numbers is positive,
139+
# we pad above the target. If it's negative, we pad above the source line.
135140
source_lineno_to_padding = {}
136141
target_lineno_to_padding = {}
137142

@@ -149,29 +154,84 @@ def main():
149154
target_lineno_to_padding[target_lineno] = pad_amount
150155
current_delta = delta
151156

152-
# For inline diffing
153-
# if you have a contiguous streak of removal lines, followed by a contiguous streak of addition lines,
154-
# you can collect the removals into a string, collect the additions into a string, and diff two strings,
155-
# to find the locations in the line where things differ ABC
157+
# Track which source and target lines are aligned and should be intraline
158+
# diffed Work out row number of lines in each side of the diff. Row
159+
# number is how far from the top of the syntax snippet we are. A line in
160+
# the source and target with the same row numbers will be aligned in the
161+
# diff (their line numbers in the source code may be different, though).
162+
# There can be gaps in row numbers too, since sometimes we add padding
163+
# above rows to ensure the source and target diffs are aligned with each
164+
# other.
165+
166+
# Map row numbers to lines
167+
source_lines_by_row_number = {}
168+
target_lines_by_row_number = {}
169+
170+
accumulated_source_padding = 0
171+
for i, line in enumerate(hunk.source_lines()):
172+
lineno = hunk.source_start + i
173+
this_line_padding = source_lineno_to_padding.get(lineno, 0)
174+
accumulated_source_padding += this_line_padding
175+
row_number = i + accumulated_source_padding
176+
source_lines_by_row_number[row_number] = line
177+
178+
accumulated_target_padding = 0
179+
for i, line in enumerate(hunk.target_lines()):
180+
lineno = hunk.target_start + i
181+
this_line_padding = target_lineno_to_padding.get(lineno, 0)
182+
accumulated_target_padding += this_line_padding
183+
row_number = i + accumulated_target_padding
184+
target_lines_by_row_number[row_number] = line
185+
186+
row_number_to_deletion_ranges = {}
187+
row_number_to_insertion_ranges = {}
188+
# Collect intraline diff info for highlighting
189+
for row_number, source_line in source_lines_by_row_number.items():
190+
target_line = target_lines_by_row_number.get(row_number)
191+
are_diffable = (
192+
source_line
193+
and target_line
194+
and source_line.is_removed
195+
and target_line.is_added
196+
)
197+
if target_line and are_diffable:
198+
matcher = SequenceMatcher(
199+
None, source_line.value, target_line.value
200+
)
201+
opcodes = matcher.get_opcodes()
202+
ratio = matcher.ratio()
203+
if ratio > 0.66: # 0.66 seems to work well
204+
for tag, i1, i2, j1, j2 in opcodes:
205+
if tag == "delete":
206+
row_number_to_deletion_ranges[row_number] = (i1, i2)
207+
elif tag == "insert":
208+
row_number_to_insertion_ranges[row_number] = (j1, j2)
209+
elif tag == "replace":
210+
row_number_to_deletion_ranges[row_number] = (i1, i2)
211+
row_number_to_insertion_ranges[row_number] = (j1, j2)
156212

157213
source_syntax_lines: List[List[Segment]] = console.render_lines(
158214
source_syntax
159215
)
160216
target_syntax_lines = console.render_lines(target_syntax)
161217

162-
highlighted_source_lines = highlight_lines_in_hunk(
218+
highlighted_source_lines = highlight_and_align_lines_in_hunk(
163219
hunk.source_start,
164220
source_removed_linenos,
165221
source_syntax_lines,
166222
ColorTriplet(255, 0, 0),
167223
source_lineno_to_padding,
224+
row_number_to_deletion_ranges,
225+
gutter_size=len(str(source_lineno_max)) + 2,
168226
)
169-
highlighted_target_lines = highlight_lines_in_hunk(
227+
highlighted_target_lines = highlight_and_align_lines_in_hunk(
170228
hunk.target_start,
171229
target_added_linenos,
172230
target_syntax_lines,
173231
ColorTriplet(0, 255, 0),
174232
target_lineno_to_padding,
233+
row_number_to_insertion_ranges,
234+
gutter_size=len(str(len(target_lines) + 1)) + 2,
175235
)
176236

177237
table = Table.grid()
@@ -190,30 +250,35 @@ def main():
190250
)
191251
console.rule(hunk_header, characters="╲", style=hunk_header_style)
192252
console.print(table)
253+
254+
# TODO: File name indicator at bottom of file, if file diff is larger than terminal height.
193255
console.rule(style="#45483d", characters="▔")
194256

195257

196-
def highlight_lines_in_hunk(
197-
start_lineno,
198-
highlight_linenos,
199-
syntax_lines,
200-
blend_colour,
258+
def highlight_and_align_lines_in_hunk(
259+
start_lineno: int,
260+
highlight_linenos: Set[Optional[int]],
261+
syntax_hunk_lines,
262+
blend_colour: ColorTriplet,
201263
lines_to_pad_above: Dict[int, int],
264+
highlight_ranges: Dict[int, Tuple[int, int]],
265+
gutter_size: int,
202266
):
203267
highlighted_lines = []
204-
for line in syntax_lines:
205-
if len(line) > 0:
206-
text, style, control = line[0]
207-
line[0] = Segment(
208-
"▏",
209-
Style.from_color(
210-
color=MONOKAI_LIGHT_ACCENT, bgcolor=MONOKAI_BACKGROUND
211-
),
212-
control,
213-
)
214-
215-
# Apply additional diff-related highlighting to lines
216-
for index, line in enumerate(syntax_lines):
268+
# TODO: Don't think this really asd
269+
# for line in syntax_hunk_lines:
270+
# if len(line) > 0:
271+
# text, style, control = line[0]
272+
# line[0] = Segment(
273+
# "▏",
274+
# Style.from_color(
275+
# color=MONOKAI_LIGHT_ACCENT, bgcolor=MONOKAI_BACKGROUND
276+
# ),
277+
# control,
278+
# )
279+
280+
# Apply diff-related highlighting to lines
281+
for index, line in enumerate(syntax_hunk_lines):
217282
lineno = index + start_lineno
218283

219284
if lineno in highlight_linenos:
@@ -222,6 +287,7 @@ def highlight_lines_in_hunk(
222287
for segment in line:
223288
style: Style
224289
text, style, control = segment
290+
225291
if style:
226292
if style.bgcolor:
227293
bgcolor_triplet = style.bgcolor.triplet
@@ -249,9 +315,6 @@ def highlight_lines_in_hunk(
249315
color=new_color, bgcolor=new_bgcolor
250316
)
251317
updated_style = style + overlay_style
252-
if segment_number == 1:
253-
updated_style += Style(italic=True)
254-
255318
new_line.append(Segment(text, updated_style, control))
256319
else:
257320
new_line.append(segment)
@@ -261,7 +324,6 @@ def highlight_lines_in_hunk(
261324

262325
# Pad above the line if required
263326
pad = lines_to_pad_above.get(lineno, 0)
264-
# pad = 0
265327
for i in range(pad):
266328
highlighted_lines.append(
267329
[
@@ -271,6 +333,32 @@ def highlight_lines_in_hunk(
271333
]
272334
)
273335

336+
# Finally, apply the intraline diff highlighting for this line if required
337+
highlight_range = highlight_ranges.get(index)
338+
if highlight_range:
339+
start, end = highlight_range
340+
line_as_text = Text.assemble(
341+
*((text, style) for text, style, control in new_line), end=""
342+
)
343+
344+
intraline_bgcolor = Color.from_triplet(
345+
blend_rgb_cached(
346+
blend_colour, MONOKAI_BACKGROUND.triplet, cross_fade=0.6
347+
)
348+
)
349+
intraline_color = Color.from_triplet(
350+
blend_rgb_cached(
351+
intraline_bgcolor.triplet,
352+
Color.from_rgb(255, 255, 255).triplet,
353+
cross_fade=0.8,
354+
)
355+
)
356+
line_as_text.stylize(
357+
Style.from_color(color=intraline_color, bgcolor=intraline_bgcolor),
358+
start=start + gutter_size + 1,
359+
end=end + gutter_size + 1,
360+
)
361+
new_line = list(console.render(line_as_text))
274362
highlighted_lines.append(new_line)
275363
return highlighted_lines
276364

0 commit comments

Comments
 (0)