diff --git a/README.md b/README.md
index df9ef91..3897e4b 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ Take a look at [demos on Ace-diff page](https://ace-diff.github.io/ace-diff/). T
- Readonly option for left/right editors
- Control how aggressively diffs are combined
- Allow users to copy diffs from one side to the other
+- Character-level diff highlighting (shows exact changes within lines)
+- Gutter decorations marking changed lines
## How to install
@@ -113,6 +115,7 @@ Here are all the defaults. I'll explain each one in details below. Note: you onl
diffGranularity: 'broad',
showDiffs: true,
showConnectors: true,
+ charDiffs: true,
maxDiffs: 5000,
left: {
id: null,
@@ -133,6 +136,8 @@ Here are all the defaults. I'll explain each one in details below. Note: you onl
classes: {
gutterID: 'acediff__gutter',
diff: 'acediff__diffLine',
+ diffChar: 'acediff__diffChar',
+ diffGutter: 'acediff__diffGutter',
connector: 'acediff__connector',
newCodeConnectorLink: 'acediff__newCodeConnector',
newCodeConnectorLinkContent: '→',
@@ -154,6 +159,7 @@ Here are all the defaults. I'll explain each one in details below. Note: you onl
- `diffGranularity` (string, optional, default: `broad`). this has two options (`specific`, and `broad`). Basically this determines how aggressively AceDiff combines diffs to simplify the interface. I found that often it's a judgement call as to whether multiple diffs on one side should be grouped. This setting provides a little control over it.
- `showDiffs` (boolean, optional, default: `true`). Whether or not the diffs are enabled. This basically turns everything off.
- `showConnectors` (boolean, optional, default: `true`). Whether or not the gutter in the middle show show connectors visualizing where the left and right changes map to one another.
+- `charDiffs` (boolean, optional, default: `true`). When enabled, highlights the specific characters that changed within a line, not just the whole line. Provides more granular diff visualization.
- `maxDiffs` (integer, optional, default: `5000`). This was added a safety precaution. For really massive files with vast numbers of diffs, it's possible the Ace instances or AceDiff will become too laggy. This simply disables the diffing altogether once you hit a certain number of diffs.
- `left/right`. this object contains settings specific to the leftmost editor.
- `left.content / right.content` (string, optional, default: `null`). If you like, when you instantiate AceDiff you can include the content that should appear in the leftmost editor via this property.
@@ -167,6 +173,8 @@ Here are all the defaults. I'll explain each one in details below. Note: you onl
- `gutterID`: the ID for the gutter element between editors
- `diff`: the class for a diff line on either editor
+- `diffChar`: the class for character-level diff highlighting (used when `charDiffs` is enabled)
+- `diffGutter`: the class for gutter decorations on diff lines
- `connector`: the SVG connector class
- `newCodeConnectorLink`: the class for the copy-to-right link element
- `newCodeConnectorLinkContent`: the content of the copy to right link. Defaults to a unicode right arrow ('→')
@@ -183,6 +191,7 @@ There are a few API methods available on your AceDiff instance.
- `aceInstance.setOptions()`: this lets you set many of the above options on the fly. Note: certain things used during the construction of the editor, like the classes can't be overridden.
- `aceInstance.getNumDiffs()`: returns the number of diffs currently being displayed.
- `aceInstance.diff()`: updates the diff. This shouldn't ever be required because AceDiff automatically recognizes the key events like changes to the editor and window resizing. But I've included it because there may always be that fringe case...
+- `aceInstance.clear()`: clears all diff markers, gutter decorations, and connectors without destroying the editors. Useful when you want to temporarily hide diffs.
- `aceInstance.destroy()`: destroys the AceDiff instance. Basically this just destroys both editors and cleans out the gutter.
## Browser Support
diff --git a/package.json b/package.json
index 895a08c..af4db7b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"main": "dist/ace-diff.min.js",
"module": "dist/module.js",
"scripts": {
- "build": "parcel build",
+ "build": "rm -rf .parcel-cache && parcel build",
"watch": "parcel watch",
"dev": "parcel serve test/fixtures/index.html --open",
"serve": "parcel test/fixtures/*.html -p 8081",
diff --git a/src/index.js b/src/index.js
index c6a1e17..4950965 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,7 +17,6 @@ import query from './dom/query.js'
import C from './constants.js'
import './styles/ace-diff.scss'
-import './styles/ace-diff-dark.scss'
// Range module placeholder
let Range
@@ -56,6 +55,7 @@ export default function AceDiff(options = {}) {
lockScrolling: false, // not implemented yet
showDiffs: true,
showConnectors: true,
+ charDiffs: true,
maxDiffs: 5000,
left: {
id: null,
@@ -76,6 +76,8 @@ export default function AceDiff(options = {}) {
classes: {
gutterID: 'acediff__gutter',
diff: 'acediff__diffLine',
+ diffChar: 'acediff__diffChar',
+ diffGutter: 'acediff__diffGutter',
connector: 'acediff__connector',
newCodeConnectorLink: 'acediff__newCodeConnector',
newCodeConnectorLinkContent: '→',
@@ -132,7 +134,7 @@ export default function AceDiff(options = {}) {
)
acediff.options.right.id = ensureElement(acediff.el, 'acediff__right')
- acediff.el.innerHTML = `
${acediff.el.innerHTML}
`
+ acediff.el.innerHTML = `${acediff.el.innerHTML}
`
// instantiate the editors in an internal data structure
// that will store a little info about the diffs and
@@ -142,11 +144,13 @@ export default function AceDiff(options = {}) {
ace: ace.edit(acediff.options.left.id),
markers: [],
lineLengths: [],
+ diffGutters: [],
},
right: {
ace: ace.edit(acediff.options.right.id),
markers: [],
lineLengths: [],
+ diffGutters: [],
},
editorHeight: null,
}
@@ -260,6 +264,12 @@ AceDiff.prototype = {
decorate(this)
},
+ clear() {
+ clearDiffs(this)
+ clearGutter(this)
+ clearArrows(this)
+ },
+
destroy() {
// destroy the two editors
const leftValue = this.editors.left.ace.getValue()
@@ -398,7 +408,7 @@ function getLineLengths(editor) {
}
// shows a diff in one of the two editors.
-function showDiff(acediff, editor, startLine, endLine, className) {
+function showDiff(acediff, editor, startLine, endLine, chars, className) {
const editorInstance = acediff.editors[editor]
if (endLine < startLine) {
@@ -408,20 +418,42 @@ function showDiff(acediff, editor, startLine, endLine, className) {
const classNames = `${className} ${
endLine > startLine ? 'lines' : 'targetOnly'
- }`
+ } ${editor}`
- if (endLine > startLine) {
- endLine -= 1 /* because endLine is usually + 1 */
+ let markerEndLine = endLine
+ if (markerEndLine > startLine) {
+ markerEndLine -= 1 /* because endLine is usually + 1 */
}
// to get Ace to highlight the full row we just set the start and end chars to 0 and 1
editorInstance.markers.push(
editorInstance.ace.session.addMarker(
- new Range(startLine, 0, endLine, 1),
+ new Range(startLine, 0, markerEndLine, 1),
classNames,
'fullLine',
),
)
+
+ // Add character-level highlighting if charDiffs is enabled
+ if (acediff.options.charDiffs && chars && chars.length > 0) {
+ const charClassName = `${acediff.options.classes.diffChar} ${editor}`
+ chars.forEach((char) => {
+ editorInstance.markers.push(
+ editorInstance.ace.session.addMarker(
+ new Range(char.lineStart, char.start, char.lineEnd - 1, char.end),
+ charClassName,
+ 'text',
+ ),
+ )
+ })
+ }
+
+ // Add gutter decorations for diff lines
+ const gutterClassName = `${acediff.options.classes.diffGutter} ${editor}`
+ for (let line = startLine; line < endLine; line += 1) {
+ editorInstance.ace.session.addGutterDecoration(line, gutterClassName)
+ editorInstance.diffGutters.push({ line, className: gutterClassName })
+ }
}
// called onscroll. Updates the gap to ensure the connectors are all lining up
@@ -434,12 +466,31 @@ function updateGap(acediff) {
}
function clearDiffs(acediff) {
+ // Clear markers
acediff.editors.left.markers.forEach((marker) => {
acediff.editors.left.ace.getSession().removeMarker(marker)
}, acediff)
acediff.editors.right.markers.forEach((marker) => {
acediff.editors.right.ace.getSession().removeMarker(marker)
}, acediff)
+ acediff.editors.left.markers = []
+ acediff.editors.right.markers = []
+
+ // Clear gutter decorations
+ acediff.editors.left.diffGutters.forEach((gutter) => {
+ acediff.editors.left.ace.session.removeGutterDecoration(
+ gutter.line,
+ gutter.className,
+ )
+ }, acediff)
+ acediff.editors.right.diffGutters.forEach((gutter) => {
+ acediff.editors.right.ace.session.removeGutterDecoration(
+ gutter.line,
+ gutter.className,
+ )
+ }, acediff)
+ acediff.editors.left.diffGutters = []
+ acediff.editors.right.diffGutters = []
}
function addConnector(
@@ -599,6 +650,9 @@ function computeDiff(acediff, diffType, offsetLeft, offsetRight, diffText) {
leftEndOffset: offsetLeft + diffText.length,
rightStartOffset: offsetRight,
rightEndOffset: offsetRight,
+ // Store character positions for charDiffs highlighting
+ leftStartChar: info.startChar,
+ leftEndChar: info.endChar,
}
} else {
let info = getSingleDiffInfo(acediff.editors.right, offsetRight, diffText)
@@ -644,6 +698,9 @@ function computeDiff(acediff, diffType, offsetLeft, offsetRight, diffText) {
leftEndOffset: offsetLeft,
rightStartOffset: offsetRight,
rightEndOffset: offsetRight + diffText.length,
+ // Store character positions for charDiffs highlighting
+ rightStartChar: info.startChar,
+ rightEndChar: info.endChar,
}
}
@@ -783,14 +840,20 @@ function clearGutter(acediff) {
// gutter.innerHTML = '';
const gutterEl = document.getElementById(acediff.options.classes.gutterID)
- gutterEl.removeChild(acediff.gutterSVG)
+ if (acediff.gutterSVG) {
+ gutterEl.removeChild(acediff.gutterSVG)
+ }
createGutter(acediff)
}
function clearArrows(acediff) {
- acediff.copyLeftContainer.innerHTML = ''
- acediff.copyRightContainer.innerHTML = ''
+ if (acediff.copyLeftContainer) {
+ acediff.copyLeftContainer.innerHTML = ''
+ }
+ if (acediff.copyRightContainer) {
+ acediff.copyRightContainer.innerHTML = ''
+ }
}
/*
@@ -806,9 +869,35 @@ function simplifyDiffs(acediff, diffs) {
: val <= 1
}
+ // Helper to create a diff object with character arrays for charDiffs
+ function createDiffWithChars(diff) {
+ const newDiff = Object.assign({}, diff, {
+ leftChars: [],
+ rightChars: [],
+ })
+ // Add character range info if present
+ if (diff.leftEndChar !== undefined) {
+ newDiff.leftChars.push({
+ start: diff.leftStartChar,
+ end: diff.leftEndChar,
+ lineStart: diff.leftStartLine,
+ lineEnd: diff.leftEndLine,
+ })
+ }
+ if (diff.rightEndChar !== undefined) {
+ newDiff.rightChars.push({
+ start: diff.rightStartChar,
+ end: diff.rightEndChar,
+ lineStart: diff.rightStartLine,
+ lineEnd: diff.rightEndLine,
+ })
+ }
+ return newDiff
+ }
+
diffs.forEach((diff, index) => {
if (index === 0) {
- groupedDiffs.push(diff)
+ groupedDiffs.push(createDiffWithChars(diff))
return
}
@@ -854,13 +943,30 @@ function simplifyDiffs(acediff, diffs) {
diff.rightEndOffset,
groupedDiffs[i].rightEndOffset,
)
+ // Add character ranges to the grouped diff
+ if (diff.leftEndChar !== undefined) {
+ groupedDiffs[i].leftChars.push({
+ start: diff.leftStartChar,
+ end: diff.leftEndChar,
+ lineStart: diff.leftStartLine,
+ lineEnd: diff.leftEndLine,
+ })
+ }
+ if (diff.rightEndChar !== undefined) {
+ groupedDiffs[i].rightChars.push({
+ start: diff.rightStartChar,
+ end: diff.rightEndChar,
+ lineStart: diff.rightStartLine,
+ lineEnd: diff.rightEndLine,
+ })
+ }
isGrouped = true
break
}
}
if (!isGrouped) {
- groupedDiffs.push(diff)
+ groupedDiffs.push(createDiffWithChars(diff))
}
})
@@ -890,6 +996,7 @@ function decorate(acediff) {
C.EDITOR_LEFT,
info.leftStartLine,
info.leftEndLine,
+ info.leftChars,
acediff.options.classes.diff,
)
showDiff(
@@ -897,6 +1004,7 @@ function decorate(acediff) {
C.EDITOR_RIGHT,
info.rightStartLine,
info.rightEndLine,
+ info.rightChars,
acediff.options.classes.diff,
)
diff --git a/src/styles/_ace-diff-base.scss b/src/styles/_ace-diff-base.scss
index 6c0d433..4d30059 100644
--- a/src/styles/_ace-diff-base.scss
+++ b/src/styles/_ace-diff-base.scss
@@ -6,6 +6,7 @@ $gutterBackground: #efefef !default;
$copyArrowsColor: #000 !default;
$mergeRightColor: #c98100 !default;
$mergeLeftColor: #004ea0 !default;
+$charDiffBackground: color.adjust($connectorBackground, $lightness: -15%) !default;
.acediff {
// .acediff class itself got no styles
@@ -58,6 +59,18 @@ $mergeLeftColor: #004ea0 !default;
}
}
+ // Character-level diff highlighting (darker shade within diff lines)
+ &__diffChar {
+ background-color: $charDiffBackground;
+ position: absolute;
+ z-index: 5;
+ }
+
+ // Gutter decoration for diff lines
+ &__diffGutter {
+ background-color: $connectorBackground !important;
+ }
+
// SVG connector
&__connector {
fill: $connectorBackground;