Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / differential / parser / DifferentialLineAdjustmentMap.php
blob2b4033c798bab09cf8bab03c247e167181b68f6c
1 <?php
3 /**
4 * Datastructure which follows lines of code across source changes.
6 * This map is used to update the positions of inline comments after diff
7 * updates. For example, if a inline comment appeared on line 30 of a diff
8 * but the next update adds 15 more lines above it, the comment should move
9 * down to line 45.
12 final class DifferentialLineAdjustmentMap extends Phobject {
14 private $map;
15 private $nearestMap;
16 private $isInverse;
17 private $finalOffset;
18 private $nextMapInChain;
20 /**
21 * Get the raw adjustment map.
23 public function getMap() {
24 return $this->map;
27 public function getNearestMap() {
28 if ($this->nearestMap === null) {
29 $this->buildNearestMap();
32 return $this->nearestMap;
35 public function getFinalOffset() {
36 // Make sure we've built this map already.
37 $this->getNearestMap();
38 return $this->finalOffset;
42 /**
43 * Add a map to the end of the chain.
45 * When a line is mapped with @{method:mapLine}, it is mapped through all
46 * maps in the chain.
48 public function addMapToChain(DifferentialLineAdjustmentMap $map) {
49 if ($this->nextMapInChain) {
50 $this->nextMapInChain->addMapToChain($map);
51 } else {
52 $this->nextMapInChain = $map;
54 return $this;
58 /**
59 * Map a line across a change, or a series of changes.
61 * @param int Line to map
62 * @param bool True to map it as the end of a range.
63 * @return wild Spooky magic.
65 public function mapLine($line, $is_end) {
66 $nmap = $this->getNearestMap();
68 $deleted = false;
69 $offset = false;
70 if (isset($nmap[$line])) {
71 $line_range = $nmap[$line];
72 if ($is_end) {
73 $to_line = end($line_range);
74 } else {
75 $to_line = reset($line_range);
77 if ($to_line <= 0) {
78 // If we're tracing the first line and this block is collapsing,
79 // compute the offset from the top of the block.
80 if (!$is_end && $this->isInverse) {
81 $offset = 1;
82 $cursor = $line - 1;
83 while (isset($nmap[$cursor])) {
84 $prev = $nmap[$cursor];
85 $prev = reset($prev);
86 if ($prev == $to_line) {
87 $offset++;
88 } else {
89 break;
91 $cursor--;
95 $to_line = -$to_line;
96 if (!$this->isInverse) {
97 $deleted = true;
100 $line = $to_line;
101 } else {
102 $line = $line + $this->finalOffset;
105 if ($this->nextMapInChain) {
106 $chain = $this->nextMapInChain->mapLine($line, $is_end);
107 list($chain_deleted, $chain_offset, $line) = $chain;
108 $deleted = ($deleted || $chain_deleted);
109 if ($chain_offset !== false) {
110 if ($offset === false) {
111 $offset = 0;
113 $offset += $chain_offset;
117 return array($deleted, $offset, $line);
122 * Build a derived map which maps deleted lines to the nearest valid line.
124 * This computes a "nearest line" map and a final-line offset. These
125 * derived maps allow us to map deleted code to the previous (or next) line
126 * which actually exists.
128 private function buildNearestMap() {
129 $map = $this->map;
130 $nmap = array();
132 $nearest = 0;
133 foreach ($map as $key => $value) {
134 if ($value) {
135 $nmap[$key] = $value;
136 $nearest = end($value);
137 } else {
138 $nmap[$key][0] = -$nearest;
142 if (isset($key)) {
143 $this->finalOffset = ($nearest - $key);
144 } else {
145 $this->finalOffset = 0;
148 foreach (array_reverse($map, true) as $key => $value) {
149 if ($value) {
150 $nearest = reset($value);
151 } else {
152 $nmap[$key][1] = -$nearest;
156 $this->nearestMap = $nmap;
158 return $this;
161 public static function newFromHunks(array $hunks) {
162 assert_instances_of($hunks, 'DifferentialHunk');
164 $map = array();
165 $o = 0;
166 $n = 0;
168 $hunks = msort($hunks, 'getOldOffset');
169 foreach ($hunks as $hunk) {
171 // If the hunks are disjoint, add the implied missing lines where
172 // nothing changed.
173 $min = ($hunk->getOldOffset() - 1);
174 while ($o < $min) {
175 $o++;
176 $n++;
177 $map[$o][] = $n;
180 $lines = $hunk->getStructuredLines();
181 foreach ($lines as $line) {
182 switch ($line['type']) {
183 case '-':
184 $o++;
185 $map[$o] = array();
186 break;
187 case '+':
188 $n++;
189 $map[$o][] = $n;
190 break;
191 case ' ':
192 $o++;
193 $n++;
194 $map[$o][] = $n;
195 break;
196 default:
197 break;
202 $map = self::reduceMapRanges($map);
204 return self::newFromMap($map);
207 public static function newFromMap(array $map) {
208 $obj = new DifferentialLineAdjustmentMap();
209 $obj->map = $map;
210 return $obj;
213 public static function newInverseMap(DifferentialLineAdjustmentMap $map) {
214 $old = $map->getMap();
215 $inv = array();
216 $last = 0;
217 foreach ($old as $k => $v) {
218 if (count($v) > 1) {
219 $v = range(reset($v), end($v));
221 if ($k == 0) {
222 foreach ($v as $line) {
223 $inv[$line] = array();
224 $last = $line;
226 } else if ($v) {
227 $first = true;
228 foreach ($v as $line) {
229 if ($first) {
230 $first = false;
231 $inv[$line][] = $k;
232 $last = $line;
233 } else {
234 $inv[$line] = array();
237 } else {
238 $inv[$last][] = $k;
242 $inv = self::reduceMapRanges($inv);
244 $obj = new DifferentialLineAdjustmentMap();
245 $obj->map = $inv;
246 $obj->isInverse = !$map->isInverse;
247 return $obj;
250 private static function reduceMapRanges(array $map) {
251 foreach ($map as $key => $values) {
252 if (count($values) > 2) {
253 $map[$key] = array(reset($values), end($values));
256 return $map;
260 public static function loadMaps(array $maps) {
261 $keys = array();
262 foreach ($maps as $map) {
263 list($u, $v) = $map;
264 $keys[self::getCacheKey($u, $v)] = $map;
267 $cache = new PhabricatorKeyValueDatabaseCache();
268 $cache = new PhutilKeyValueCacheProfiler($cache);
269 $cache->setProfiler(PhutilServiceProfiler::getInstance());
271 $results = array();
273 if ($keys) {
274 $caches = $cache->getKeys(array_keys($keys));
275 foreach ($caches as $key => $value) {
276 list($u, $v) = $keys[$key];
277 try {
278 $results[$u][$v] = self::newFromMap(
279 phutil_json_decode($value));
280 } catch (Exception $ex) {
281 // Ignore, rebuild below.
283 unset($keys[$key]);
287 if ($keys) {
288 $built = self::buildMaps($maps);
290 $write = array();
291 foreach ($built as $u => $list) {
292 foreach ($list as $v => $map) {
293 $write[self::getCacheKey($u, $v)] = json_encode($map->getMap());
294 $results[$u][$v] = $map;
298 $cache->setKeys($write);
301 return $results;
304 private static function buildMaps(array $maps) {
305 $need = array();
306 foreach ($maps as $map) {
307 list($u, $v) = $map;
308 $need[$u] = $u;
309 $need[$v] = $v;
312 if ($need) {
313 $changesets = id(new DifferentialChangesetQuery())
314 ->setViewer(PhabricatorUser::getOmnipotentUser())
315 ->withIDs($need)
316 ->needHunks(true)
317 ->execute();
318 $changesets = mpull($changesets, null, 'getID');
321 $results = array();
322 foreach ($maps as $map) {
323 list($u, $v) = $map;
324 $u_set = idx($changesets, $u);
325 $v_set = idx($changesets, $v);
327 if (!$u_set || !$v_set) {
328 continue;
331 // This is the simple case.
332 if ($u == $v) {
333 $results[$u][$v] = self::newFromHunks(
334 $u_set->getHunks());
335 continue;
338 $u_old = $u_set->makeOldFile();
339 $v_old = $v_set->makeOldFile();
341 // No difference between the two left sides.
342 if ($u_old == $v_old) {
343 $results[$u][$v] = self::newFromMap(
344 array());
345 continue;
348 // If we're missing context, this won't currently work. We can
349 // make this case work, but it's fairly rare.
350 $u_hunks = $u_set->getHunks();
351 $v_hunks = $v_set->getHunks();
352 if (count($u_hunks) != 1 ||
353 count($v_hunks) != 1 ||
354 head($u_hunks)->getOldOffset() != 1 ||
355 head($u_hunks)->getNewOffset() != 1 ||
356 head($v_hunks)->getOldOffset() != 1 ||
357 head($v_hunks)->getNewOffset() != 1) {
358 continue;
361 $changeset = id(new PhabricatorDifferenceEngine())
362 ->generateChangesetFromFileContent($u_old, $v_old);
364 $results[$u][$v] = self::newFromHunks(
365 $changeset->getHunks());
368 return $results;
371 private static function getCacheKey($u, $v) {
372 return 'diffadjust.v1('.$u.','.$v.')';