-
Notifications
You must be signed in to change notification settings - Fork 656
Better diff highlighting for code blocks #1433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sfc-gh-dmatthews
wants to merge
11
commits into
ccv2-concepts
Choose a base branch
from
prettier-python-diff
base: ccv2-concepts
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+308
β82
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
248872c
Diff highlighting
sfc-gh-dmatthews 9646929
Smart copy for diffs
sfc-gh-dmatthews 0320608
Hide diff marks
sfc-gh-dmatthews a82751e
Fade edges
sfc-gh-dmatthews 0d40819
Diff markers
sfc-gh-dmatthews 50c8023
Fix highlight
sfc-gh-dmatthews d7da23e
Cleanup
sfc-gh-dmatthews 8345429
Add character to every diff line
sfc-gh-dmatthews 6c0c8a3
Update old diff blocks
sfc-gh-dmatthews f287e1f
Refactor diff styling
sfc-gh-dmatthews a3f1c98
Move comment
sfc-gh-dmatthews File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import React, { useCallback, useEffect, useRef, useState } from "react"; | ||
| import React, { useCallback, useEffect, useRef } from "react"; | ||
| import classNames from "classnames"; | ||
| import Prism from "prismjs"; | ||
| import "prismjs/plugins/line-numbers/prism-line-numbers"; | ||
|
|
@@ -7,8 +7,13 @@ import "prismjs/plugins/line-highlight/prism-line-highlight.css"; | |
| import "prismjs/plugins/toolbar/prism-toolbar"; | ||
| import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard"; | ||
| import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace"; | ||
| import "prismjs/plugins/diff-highlight/prism-diff-highlight"; | ||
| import "prismjs/plugins/diff-highlight/prism-diff-highlight.css"; | ||
|
|
||
| import Image from "./image"; | ||
| import languageDisplayNames, { | ||
| getPrismLanguage, | ||
| } from "../../lib/languageDisplayNames"; | ||
|
|
||
| import styles from "./code.module.css"; | ||
|
|
||
|
|
@@ -68,10 +73,6 @@ const TryMeButton = ({ code }) => { | |
| ); | ||
| }; | ||
|
|
||
| import languageDisplayNames, { | ||
| getPrismLanguage, | ||
| } from "../../lib/languageDisplayNames"; | ||
|
|
||
| // Initialize the cache for imported languages. | ||
| const languageImports = new Map(); | ||
|
|
||
|
|
@@ -123,7 +124,10 @@ const Code = ({ | |
|
|
||
| // Extract language identifier for display | ||
| const langId = languageClass?.substring(9) || language || "python"; | ||
| const displayLanguage = languageDisplayNames[langId] || langId; | ||
| const diffMatch = langId.match(/^diff-([\w-]+)$/); | ||
| const displayLanguage = diffMatch | ||
| ? languageDisplayNames[diffMatch[1]] || diffMatch[1] | ||
| : languageDisplayNames[langId] || langId; | ||
| const showLanguage = | ||
| langId.toLowerCase() !== "none" && (showAll || !filename); | ||
|
|
||
|
|
@@ -168,15 +172,131 @@ const Code = ({ | |
| ); | ||
| }; | ||
|
|
||
| // Strip deleted lines and diff prefixes (+/=) to produce copy-friendly text. | ||
| function getCleanDiffText(textContent) { | ||
| return textContent | ||
| .split(/\r?\n/) | ||
| .filter((line) => !line.startsWith("-")) | ||
| .map((line) => line.substring(1)) | ||
| .join("\n"); | ||
| } | ||
|
|
||
| // Add +/- markers into the left margin of the code block. | ||
| function addDiffMarkers(container, codeElement) { | ||
| const pre = codeElement.closest("pre"); | ||
| if (!pre) return; | ||
|
|
||
| const lines = codeElement.textContent.split(/\r?\n/); | ||
| // Drop trailing empty line from a final newline | ||
| if (lines.length > 0 && lines[lines.length - 1] === "") { | ||
| lines.pop(); | ||
| } | ||
|
|
||
| const markerEl = document.createElement("div"); | ||
| markerEl.className = `diff-markers ${styles.DiffMarkers}`; | ||
| markerEl.setAttribute("aria-hidden", "true"); | ||
|
|
||
| for (const line of lines) { | ||
| const span = document.createElement("span"); | ||
| if (line.startsWith("+")) { | ||
| span.textContent = "+"; | ||
| span.className = "diff-marker-insert"; | ||
| } else if (line.startsWith("-")) { | ||
| span.textContent = "\u2212"; // minus sign instead of hyphen; visually balanced with + | ||
| span.className = "diff-marker-delete"; | ||
| } else { | ||
| // Non-breaking space keeps this span from collapsing so | ||
| // subsequent +/- markers stay vertically aligned with their code lines. | ||
| span.textContent = "\u00A0"; | ||
| } | ||
| markerEl.appendChild(span); | ||
| } | ||
|
|
||
| const topOffset = | ||
| pre.offsetTop + parseFloat(getComputedStyle(pre).paddingTop); | ||
| markerEl.style.top = `${topOffset}px`; | ||
|
|
||
| container.appendChild(markerEl); | ||
| } | ||
|
|
||
| function overrideDiffCopyButton(container, codeElement) { | ||
| const copyButton = container.querySelector(".copy-to-clipboard-button"); | ||
| if (!copyButton) return; | ||
|
|
||
| const timeout = | ||
| parseInt(codeElement.getAttribute("data-prismjs-copy-timeout")) || 5000; | ||
|
|
||
| const newButton = copyButton.cloneNode(true); | ||
| copyButton.parentNode.replaceChild(newButton, copyButton); | ||
|
|
||
| newButton.addEventListener("click", async () => { | ||
| const cleanText = getCleanDiffText(codeElement.textContent); | ||
| const span = newButton.querySelector("span"); | ||
|
|
||
| try { | ||
| await navigator.clipboard.writeText(cleanText); | ||
| if (span) { | ||
| span.textContent = "Copied!"; | ||
| newButton.setAttribute("data-copy-state", "copy-success"); | ||
| } | ||
| } catch { | ||
| if (span) { | ||
| span.textContent = "Press Ctrl+C to copy"; | ||
| newButton.setAttribute("data-copy-state", "copy-error"); | ||
| } | ||
| } | ||
|
|
||
| if (span) { | ||
| setTimeout(() => { | ||
| span.textContent = "Copy"; | ||
| newButton.setAttribute("data-copy-state", "copy"); | ||
| }, timeout); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| async function highlightElement( | ||
| importLanguage, | ||
| languageImports, | ||
| codeElement, | ||
| hideCopyButton, | ||
| ) { | ||
| if (typeof window !== "undefined") { | ||
| // Only import the language if it hasn't been imported before. | ||
| if (!languageImports.has(importLanguage)) { | ||
| const isDiff = importLanguage.startsWith("diff-"); | ||
| if (isDiff) { | ||
| const baseLang = importLanguage.substring(5); | ||
| for (const lang of ["diff", baseLang]) { | ||
| if (!languageImports.has(lang)) { | ||
| try { | ||
| await import(`prismjs/components/prism-${lang}`); | ||
| languageImports.set(lang, true); | ||
| } catch (error) { | ||
| console.error(`Prism doesn't support this language: ${lang}`); | ||
| } | ||
| } | ||
| } | ||
| // Prism's diff grammar only recognizes +/- prefixes. We use "=" for | ||
| // unchanged lines to keep code aligned (no visual shift) and to preserve | ||
| // leading whitespace that markdown processing would otherwise strip. | ||
| if (!Prism.languages.diff["unchanged-equal"]) { | ||
| Prism.languages.diff["unchanged-equal"] = { | ||
| pattern: /^(?:=.*(?:\r\n?|\n|(?![\s\S])))+/m, | ||
| alias: ["unchanged"], | ||
| inside: { | ||
| line: { | ||
| pattern: /(.)(?=[\s\S]).*(?:\r\n?|\n)?/, | ||
| lookbehind: true, | ||
| }, | ||
| prefix: { | ||
| pattern: /[\s\S]/, | ||
| alias: "unchanged", | ||
| }, | ||
| }, | ||
| }; | ||
| Prism.languages.diff.PREFIXES["unchanged-equal"] = "="; | ||
| } | ||
| languageImports.set(importLanguage, true); | ||
| } else if (!languageImports.has(importLanguage)) { | ||
| try { | ||
| await import(`prismjs/components/prism-${importLanguage}`); | ||
| languageImports.set(importLanguage, true); | ||
|
|
@@ -185,16 +305,18 @@ async function highlightElement( | |
| } | ||
| } | ||
|
|
||
| // Highlight the code block and conditionally enable toolbar plugins (including copy button) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Are these comments intentionally removed? |
||
| if (codeElement) { | ||
| // First highlight the element | ||
| Prism.highlightElement(codeElement); | ||
|
|
||
| // Then activate toolbar plugins on the parent container if copy button is not hidden | ||
| if (!hideCopyButton) { | ||
| const container = codeElement.closest(`.${styles.Container}`); | ||
| if (container) { | ||
| Prism.highlightAllUnder(container); | ||
|
|
||
| if (isDiff) { | ||
| addDiffMarkers(container, codeElement); | ||
| overrideDiffCopyButton(container, codeElement); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π