<style>
.text-detail {
width: 100%;
font-size: 1rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
display: grid;
column-gap: 0;
row-gap: 0;
{% if detail.is_multicolumn() %}
grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr);
{% else %}
grid-template-columns: max-content minmax(0, 1fr);
{% endif %}
}
.text-detail-row {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
}
.text-detail-cell {
border-left: 1px solid var(--status-neutral-border);
border-right: 1px solid var(--status-neutral-border);
padding: 0 0.5rem;
vertical-align: top;
line-height: 1.6;
user-select: none;
box-sizing: border-box;
grid-row: 1;
}
.text-detail-header .text-detail-cell {
background: var(--status-neutral-bg);
text-align: left;
font-weight: 600;
border-bottom: 1px solid var(--status-neutral-border);
}
.text-detail-cell.line-no {
text-align: right;
color: var(--status-neutral-text-subtle);
background: var(--status-neutral-bg);
}
.text-detail-cell.same {
background: var(--status-neutral-bg);
}
.text-detail-cell.added {
background: var(--status-added-bg);
}
.text-detail-cell.deleted {
background: var(--status-deleted-bg);
}
.text-detail-cell.empty {
background: var(--status-neutral-surface);
}
.text-detail-cell .cell-text {
display: block;
white-space: pre-wrap;
word-break: break-word;
user-select: text;
}
.text-detail.select-left .cell-right .cell-text,
.text-detail.select-right .cell-left .cell-text {
user-select: none;
}
{% if detail.is_multicolumn() %}
@media (max-width: 1024px) {
.text-detail {
grid-template-columns: max-content max-content minmax(0, 1fr);
}
.text-detail .text-detail-cell.line-no.expected {
grid-column: 1;
}
.text-detail .text-detail-cell.line-no.actual {
grid-column: 2;
}
.text-detail .text-detail-header .text-detail-cell.header-label {
grid-column: 3;
color: transparent;
}
.text-detail .cell-left,
.text-detail .cell-right {
grid-column: 3;
min-width: 0;
}
.text-detail .cell-right.same {
display: none;
}
.text-detail .empty {
display: none;
}
}
{% endif %}
</style>
<div class="text-detail">
<div class="text-detail-row text-detail-header">
{% match detail %}
{% when TextDetailBody::Diff with { .. } %}
<div class="text-detail-cell line-no">Line</div>
<div class="text-detail-cell header-label">expected</div>
<div class="text-detail-cell line-no">Line</div>
<div class="text-detail-cell header-label">actual</div>
{% when TextDetailBody::Single with { label, .. } %}
<div class="text-detail-cell line-no">Line</div>
<div class="text-detail-cell">{{ label }}</div>
{% endmatch %}
</div>
{% match detail %}
{% when TextDetailBody::Diff with { lines } %}
{% let mut expected_index = 1usize.. %}
{% let mut actual_index = 1usize.. %}
{% for change in lines.iter_all_changes() %}
<div class="text-detail-row">
{% match change.tag() %}
{% when similar::ChangeTag::Equal %}
<div class="text-detail-cell line-no expected">{{ expected_index.next().unwrap() }}</div>
<div class="text-detail-cell cell-left same"><span class="cell-text">{{ change.to_string_lossy() }}</span>
</div>
<div class="text-detail-cell line-no actual">{{ actual_index.next().unwrap() }}</div>
<div class="text-detail-cell cell-right same"><span class="cell-text">{{ change.to_string_lossy() }}</span>
</div>
{% when similar::ChangeTag::Delete %}
<div class="text-detail-cell line-no expected">{{ expected_index.next().unwrap() }}</div>
<div class="text-detail-cell cell-left deleted"><span
class="cell-text">{{ change.to_string_lossy() }}</span></div>
<div class="text-detail-cell line-no actual"></div>
<div class="text-detail-cell cell-right empty"><span class="cell-text"></span></div>
{% when similar::ChangeTag::Insert %}
<div class="text-detail-cell line-no expected"></div>
<div class="text-detail-cell cell-left empty"><span class="cell-text"></span></div>
<div class="text-detail-cell line-no actual">{{ actual_index.next().unwrap() }}</div>
<div class="text-detail-cell cell-right added"><span class="cell-text">{{ change.to_string_lossy() }}</span>
</div>
{% endmatch %}
</div>
{% endfor %}
{% when TextDetailBody::Single with { label, body } %}
{% for (i, line) in body.lines().enumerate() %}
<div class="text-detail-row">
<div class="text-detail-cell line-no">{{ i + 1 }}</div>
<div class="text-detail-cell cell-left {{ label }}"><span class="cell-text">{{ line }}</span></div>
</div>
{% endfor %}
{% endmatch %}
</div>
<script>
(() => {
const detail = document.querySelector(".text-detail");
if (!detail) {
return;
}
const clearSelectionMode = () => {
detail.classList.remove("select-left", "select-right");
};
const clearSelectionRanges = () => {
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
};
let pointerDown = false;
let dragged = false;
let downSide = null;
document.addEventListener(
"mousedown",
(event) => {
if (!event.target.closest(".text-detail")) {
clearSelectionMode();
}
},
true
);
detail.addEventListener("mousedown", (event) => {
const cell = event.target.closest(".text-detail-cell");
if (!cell) {
clearSelectionMode();
clearSelectionRanges();
return;
}
pointerDown = true;
dragged = false;
if (cell.classList.contains("cell-left")) {
downSide = "left";
} else if (cell.classList.contains("cell-right")) {
downSide = "right";
} else {
downSide = null;
}
clearSelectionMode();
clearSelectionRanges();
if (downSide === "left") {
detail.classList.add("select-left");
} else if (downSide === "right") {
detail.classList.add("select-right");
}
});
detail.addEventListener("mousemove", (event) => {
if (!pointerDown || dragged) {
return;
}
if (event.buttons !== 0) {
dragged = true;
}
});
document.addEventListener(
"mouseup",
() => {
if (!pointerDown) {
return;
}
pointerDown = false;
if (!dragged && downSide) {
clearSelectionRanges();
}
dragged = false;
downSide = null;
},
true
);
})();
</script>