in src/__Private/LSPLib/create_textedits.hack [16:148]
function create_textedits(string $from, string $to): vec<LSP\TextEdit> {
$diff = DiffLib\StringDiff::lines($from, $to)
->getDiff()
|> DiffLib\cluster($$);
$edits = vec[];
while (!C\is_empty($diff)) {
$first = C\firstx($diff);
$diff = Vec\drop($diff, 1);
if ($first is DiffLib\DiffKeepOp<_>) {
continue;
}
// If we have a replacement, the deletion always comes first - so, if we
// have an InsertOp here, it's a pure insertion
if ($first is DiffLib\DiffInsertOp<_>) {
$pos = shape(
'line' => $first->getNewPos(),
'character' => 0,
);
$edits[] = shape(
'range' => shape('start' => $pos, 'end' => $pos),
'newText' => Str\join($first->getContent(), "\n")."\n",
);
continue;
}
invariant(
$first is DiffLib\DiffDeleteOp<_>,
'Expected a DeleteOp, InsertOp, or KeepOp, got %s',
\get_class($first),
);
$pos = $first->getOldPos();
$len = C\count($first->getContent());
// if ($first, $next) is (Delete, Insert) we have a replacement
$next = C\first($diff);
if (!$next is DiffLib\DiffInsertOp<_>) {
// (Delete, null|Keep) - just a deletion
$edits[] = shape(
'range' => shape(
'start' => shape('line' => $pos, 'character' => 0),
'end' => shape('line' => $pos + $len, 'character' => 0),
),
'newText' => '',
);
continue;
}
// turn ['remove foo', 'add bar'] into 'replace foo with bar'
$diff = Vec\drop($diff, 1); // consume $next
$a = Str\join($first->getContent(), "\n");
$b = Str\join($next->getContent(), "\n");
// If they're 'too different' (arbitrary heuristic), replace the whole
// line
if (\levenshtein($a, $b) > (0.5 * Str\length($b))) {
$edits[] = shape(
'range' => shape(
'start' => shape('line' => $pos, 'character' => 0),
'end' => shape('line' => $pos + $len, 'character' => 0),
),
'newText' => Str\join($next->getContent(), "\n")."\n",
);
continue;
}
// Just replace characters within the line
$ildiff = DiffLib\StringDiff::characters($a, $b)
|> DiffLib\cluster($$->getDiff());
$offset_to_pos = (int $offset) ==> {
$haystack = Str\slice($a, 0, $offset);
$lines = Str\split($haystack, "\n");
return shape(
'line' => $first->getOldPos() + (C\count($lines) - 1),
'character' => Str\length(C\lastx($lines)),
);
};
// ok, now we basically do the same again :)
while (!C\is_empty($ildiff)) {
$ilfirst = C\firstx($ildiff);
$ildiff = Vec\drop($ildiff, 1);
if ($ilfirst is DiffLib\DiffKeepOp<_>) {
continue;
}
if ($ilfirst is DiffLib\DiffInsertOp<_>) {
$ilpos = $offset_to_pos($ilfirst->getNewPos());
$edits[] = shape(
'range' => shape(
'start' => $ilpos,
'end' => $ilpos,
),
'newText' => Str\join($ilfirst->getContent(), ''),
);
continue;
}
invariant($ilfirst is DiffLib\DiffDeleteOp<_>, 'unhandled op kind');
$ilnext = C\first($ildiff);
if (!$ilnext is DiffLib\DiffInsertOp<_>) {
$edits[] = shape(
'range' => shape(
'start' => $offset_to_pos($ilfirst->getOldPos()),
'end' => $offset_to_pos(
$ilfirst->getOldPos() + C\count($ilfirst->getContent()),
),
),
'newText' => '',
);
continue;
}
// Okay, replacement again :)
$ildiff = Vec\drop($ildiff, 1);
$edits[] = shape(
'range' => shape(
'start' => $offset_to_pos($ilfirst->getOldPos()),
'end' => $offset_to_pos(
$ilfirst->getOldPos() + C\count($ilfirst->getContent()),
),
),
'newText' => Str\join($ilnext->getContent(), ''),
);
}
}
return $edits;
}