Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 65d1557

Browse files
committed
block matching added this ensures crazy css edits apply
1 parent fd32e52 commit 65d1557

File tree

2 files changed

+168
-4
lines changed

2 files changed

+168
-4
lines changed

lib/utils/diff_utils/simple_diff.rb

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,38 @@ def apply(content, search, replace)
2323
lines = content.split("\n")
2424
search_lines = search.split("\n")
2525

26-
# First try exact matching
26+
# 1. Try exact matching
2727
match_positions =
2828
find_matches(lines, search_lines) { |line, search_line| line == search_line }
2929

30-
# sripped match
30+
# 2. Try stripped matching
3131
if match_positions.empty?
3232
match_positions =
3333
find_matches(lines, search_lines) do |line, search_line|
3434
line.strip == search_line.strip
3535
end
3636
end
3737

38-
# Fallback to fuzzy matching if no exact matches found
38+
# 3. Try fuzzy matching
3939
if match_positions.empty?
4040
match_positions =
4141
find_matches(lines, search_lines) do |line, search_line|
4242
fuzzy_match?(line, search_line)
4343
end
4444
end
4545

46+
# 4. Try block matching as last resort
47+
if match_positions.empty?
48+
if block_matches = find_block_matches(content, search)
49+
return replace_blocks(content, block_matches, replace)
50+
end
51+
end
52+
4653
if match_positions.empty?
4754
raise NoMatchError, "Could not find a match for the search content"
4855
end
4956

50-
# Replace every occurrence (process in descending order to avoid shifting indices)
57+
# Replace matches in reverse order
5158
match_positions.sort.reverse.each do |pos|
5259
lines.slice!(pos, search_lines.length)
5360
lines.insert(pos, *replace.split("\n"))
@@ -61,11 +68,13 @@ def apply(content, search, replace)
6168
def find_matches(lines, search_lines)
6269
matches = []
6370
max_index = lines.length - search_lines.length
71+
6472
(0..max_index).each do |i|
6573
if (0...search_lines.length).all? { |j| yield(lines[i + j], search_lines[j]) }
6674
matches << i
6775
end
6876
end
77+
6978
matches
7079
end
7180

@@ -80,16 +89,89 @@ def levenshtein_distance(s1, s2)
8089
m = s1.length
8190
n = s2.length
8291
d = Array.new(m + 1) { Array.new(n + 1, 0) }
92+
8393
(0..m).each { |i| d[i][0] = i }
8494
(0..n).each { |j| d[0][j] = j }
95+
8596
(1..m).each do |i|
8697
(1..n).each do |j|
8798
cost = s1[i - 1] == s2[j - 1] ? 0 : 1
8899
d[i][j] = [d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost].min
89100
end
90101
end
102+
91103
d[m][n]
92104
end
105+
106+
def find_block_matches(content, search)
107+
content_blocks = extract_blocks(content)
108+
search_blocks = extract_blocks(search)
109+
110+
return nil if content_blocks.empty? || search_blocks.empty?
111+
112+
matches = []
113+
search_blocks.each do |search_block|
114+
content_blocks.each do |content_block|
115+
matches << content_block if content_block[:text] == search_block[:text]
116+
end
117+
end
118+
119+
matches.empty? ? nil : matches
120+
end
121+
122+
def extract_blocks(text)
123+
lines = text.split("\n")
124+
blocks = []
125+
current_block = []
126+
block_start = nil
127+
128+
lines.each_with_index do |line, index|
129+
if line =~ /^[^\s]/
130+
# Save previous block if exists
131+
if !current_block.empty?
132+
current_block << line
133+
blocks << {
134+
start: block_start,
135+
length: current_block.length,
136+
text: current_block.join("\n").strip,
137+
}
138+
current_block = []
139+
else
140+
current_block = [line]
141+
block_start = index
142+
end
143+
else
144+
# Continue current block
145+
current_block << line if current_block.any?
146+
end
147+
end
148+
149+
# Add final block
150+
if !current_block.empty?
151+
blocks << {
152+
start: block_start,
153+
length: current_block.length,
154+
text: current_block.join("\n").strip,
155+
}
156+
end
157+
158+
blocks
159+
end
160+
161+
def replace_blocks(content, blocks, replace)
162+
lines = content.split("\n")
163+
164+
# Sort blocks in reverse order to maintain correct positions
165+
blocks
166+
.sort_by { |b| -b[:start] }
167+
.each_with_index do |block, index|
168+
replacement = index.zero? ? replace : ""
169+
lines.slice!(block[:start], block[:length])
170+
lines.insert(block[:start], *replacement.split("\n"))
171+
end
172+
173+
lines.join("\n")
174+
end
93175
end
94176
end
95177
end

spec/lib/utils/diff_utils/simple_diff_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,87 @@
8282
replace = "new_content"
8383
expect(subject.apply(content, search, replace)).to eq("def method\nnew_content\nend")
8484
end
85+
86+
it "handles CSS blocks in different orders" do
87+
content = <<~CSS
88+
.first {
89+
color: red;
90+
padding: 10px;
91+
}
92+
.second {
93+
color: blue;
94+
margin: 20px;
95+
}
96+
CSS
97+
98+
search = <<~CSS
99+
.second {
100+
color: blue;
101+
margin: 20px;
102+
}
103+
.first {
104+
color: red;
105+
padding: 10px;
106+
}
107+
CSS
108+
109+
replace = <<~CSS
110+
.new-block {
111+
color: green;
112+
}
113+
CSS
114+
115+
expected = <<~CSS
116+
.new-block {
117+
color: green;
118+
}
119+
CSS
120+
121+
expect(subject.apply(content, search, replace)).to eq(expected.strip)
122+
end
123+
124+
it "handles JavaScript blocks in different orders" do
125+
content = <<~JS
126+
function first() {
127+
const x = 1;
128+
return x + 2;
129+
}
130+
131+
function second() {
132+
if (true) {
133+
return 42;
134+
}
135+
return 0;
136+
}
137+
JS
138+
139+
search = <<~JS
140+
function second() {
141+
if (true) {
142+
return 42;
143+
}
144+
return 0;
145+
}
146+
147+
function first() {
148+
const x = 1;
149+
return x + 2;
150+
}
151+
JS
152+
153+
replace = <<~JS
154+
function replacement() {
155+
return 'new';
156+
}
157+
JS
158+
159+
expected = <<~JS
160+
function replacement() {
161+
return 'new';
162+
}
163+
JS
164+
165+
expect(subject.apply(content, search, replace).strip).to eq(expected.strip)
166+
end
85167
end
86168
end

0 commit comments

Comments
 (0)