Difftastic

repository·master·Indexed 12 days ago

https://github.com/wilfred/difftastic

A structural diff tool (version 0.71.0) that uses tree-sitter parsers to compare files based on their syntax rather than lines of text. It provides context-aware diffs that ignore insignificant changes like reformatting or whitespace, and supports merge conflict markers, language overrides, and various display modes including side-by-side and inline.

Tokens
20K
Snippets
87
Records
129
Agent score
98%

What's inside Difftastic

  1. What is Difftastic?

    master

    Difftastic is a structural diff tool that compares files based on their syntax rather than just lines of text. It understands the underlying AST (Abstract Syntax Tree) of a file, allowing it to:

    • Highlight exactly which syntactic pieces have changed.
    • Distinguish between significant whitespace and mere indentation changes.
    • Handle code reformatting (e.g., splitting a single line into multiple lines) without showing them as massive changes.
    • Fall back to a line-oriented diff with word highlighting if a file extension is unrecognized or if parsing fails.
  2. What is syntactic diffing in Difftastic

    master

    Difftastic performs syntactic diffing by detecting the language, parsing the code into syntax trees, and comparing those trees directly. This allows the tool to recognize that code logic remains unchanged even if whitespace or line breaks are modified (e.g., changing a single-line method chain into a multi-line chain).

    Unlike standard line-oriented diffs that see a change in line structure as a complete replacement of the line, Difftastic identifies matched delimiters and unchanged components within the syntax tree.

    // old.rs
    let ts_lang = guess(path, guess_src).map(tsp::from_language);
    
    // new.rs
    let ts_lang = language_override
        .or_else(|| guess(path, guess_src))
        .map(tsp::from_language);
  3. Understanding 'Sliders' and depth-based matching in Difftastic

    master

    Difftastic uses specific heuristics to avoid common diffing errors:

    • Sliders (Flat and Nested): In text diffs, 'sliders' occur when lines are matched incorrectly (e.g., matching a delimiter line instead of a content line). In tree diffs, this happens when an insertion causes ambiguity in which delimiters match. Difftastic's behavior depends on the language; most languages prefer matching the inner delimiter, while Lisps and JSON prefer the outer delimiter.
    • Minimizing Depth Changes: When multiple nodes could potentially match (e.g., matching foo(123) vs foo(456) against a new foo(789)), Difftastic prefers the match that maintains the same nesting depth.
  4. How Difftastic handles reordering and insertions

    master

    Difftastic's tree-based approach provides specific behaviors for structural changes:

    • Reordering Within a List: For changes like (x y) to (y x), Difftastic aims to highlight the list contents as changed rather than the delimiters.
    • Middle Insertions: When a new node is inserted into the middle of a nested structure (e.g., foo(bar(123)) to foo(extra(bar(123)))), Difftastic attempts to treat the surrounding nodes (foo and bar) as unchanged.
    • Unordered Data Types: Unlike some tools that might ignore order in sets or maps, Difftastic considers ordering to be meaningful everywhere. It will always report changes if the order of elements in a list, set, or object changes.
    // Before
    (x y)
    
    // After
    (y x)
  5. How Difftastic handles unsupported or large files

    master

    Difftastic uses a fallback mechanism when syntactic parsing is not possible. It will use a conventional line-oriented diff with word highlighting in the following scenarios:

    1. Unsupported Formats: If the input files are not in a format that Difftastic recognizes or can parse.
    2. Large Inputs: When the input files are extremely large, Difftastic defaults to line-oriented diffing to maintain performance.
  6. How Difftastic handles comments and strings

    master

    Difftastic applies specific logic to non-code elements to improve readability:

    • Comments:
      • Matching Substrings: For code, Difftastic avoids matching common prefixes (like foobar vs foobaz). However, for comments, it allows matching common prefixes or suffixes.
      • Multiline/Reflowing Comments: Difftastic treats the inner content of block comments as identical even if decorative prefixes (like * in a doc comment) move or change due to reflowing.
    • Strings: While it is difficult to handle small changes in very large strings, Difftastic aims to show changes within string literals rather than simply marking the entire string as replaced.
  7. Understand Difftastic syntax tree terminology

    master

    Difftastic operates on a syntax tree rather than a line-oriented approach. Understanding these core terms helps interpret how diffs are structured:

    • Syntax node: The fundamental building block of the tree, which is either an Atom or a List.
    • Atom: A syntax node with no children (e.g., literals, variable names, or comments).
    • List: A syntax node containing children, bounded by an open and close Delimiter (e.g., expressions or function definitions).
    • Delimiter: A paired piece of syntax (like [ and ]) that marks the start and end of a list. Delimiters can be punctuation or non-punctuation strings (like begin and end).
    • Token: A small piece of syntax (like $x or function) used for highlighting and alignment. A token is either an atom or a non-empty delimiter.
    • Root: A syntax tree node that has no parent, representing top-level definitions in a file.
  8. Understand diff output terminology

    master

    When reading Difftastic's output, the following terms describe the comparison results:

    • LHS (Left-Hand Side): The first item being compared.
    • RHS (Right-Hand Side): The second item being compared.
    • Novel: Syntax that is an addition or a removal (it exists in only one of the two items being compared).
    • Hunk: A group of lines displayed together in the diff output. The size of a hunk is determined by the number of context lines.
    • Slider: A specific diffing scenario where multiple minimal diffs are possible due to adjacent content. Difftastic can 'slide' to find a better structural match.
    • Line-oriented: A reference to traditional diff tools (like GNU diff or GitHub's default view) that compare line additions/removals rather than syntax tree structures.
  9. Improve binary file detection with a MIME database

    master

    Difftastic can use a MIME database (the same one used by the file command) to detect binary files more accurately. If a database is present at one of the standard XDG specification paths, Difftastic will automatically use it.

    Supported paths for the MIME database:

    • /usr/share/mime/magic
    • /usr/local/share/mime/magic
    • $HOME/.local/share/mime/magic
  10. How Difftastic's syntax tree conversion works

    master

    Difftastic transforms detailed Tree-sitter parse trees into a simplified syntax tree via a recursive tree walk. This process follows these rules:

    1. Leaf Nodes to Atoms: Most Tree-sitter leaf nodes are converted into atoms.
    2. Handling Unwanted Structure: Certain node types that should be flat (like string literals) are explicitly marked as atom_nodes in tree_sitter_parser.rs to prevent them from being treated as complex structures.
    3. Delimiters as List Content: To prevent delimiters (like [ or ]) from being matched as independent atoms—which would cause unbalanced diffs—Tree-sitter tokens identified in open_delimiter_tokens are treated as the open_content or close_content of a List rather than separate atoms.

    Note on Lossy Trees: The simplified syntax tree is 'lossy'. It stores node content and position, but it does not store whitespace between nodes, and node position is ignored during the actual diffing process.

  11. How Difftastic handles blank lines

    master

    Because Difftastic performs syntactic diffing (comparing tokens), it does not 'see' blank lines by default.

    • Standard Behavior: Generally, syntactic diffing ignores blank lines, meaning adding or removing them typically shows no change.
    • Limitations: This can occasionally hide accidental reformatting. If a user inserts code and a blank line simultaneously, the syntactic diff may only show the code addition, potentially hiding the fact that blank lines were also modified.
  12. How Difftastic performs diffing

    master

    Difftastic calculates diffs by treating the problem as a route-finding task on a directed acyclic graph (DAG).

    Graph Representation

    • Vertices: Each vertex represents a specific position in two syntax trees simultaneously.
    • Start Vertex: Points to the first syntax node in both trees.
    • End Vertex: Points to the position immediately after the last syntax node in both trees.
    • Edges (Transitions):
      • Novel Atom (Left/Right): Represents marking a syntax node as new (novel) in one of the trees and advancing that tree's position.
      • Nodes Match: Represents finding a matching syntax node in both trees and advancing both positions.

    Cost and Optimization

    Difftastic assigns costs to these edges to determine the most meaningful diff:

    • Matching nodes have a lower cost than marking nodes as novel.
    • The goal is to find the lowest cost route from the start vertex to the end vertex.

    Algorithm

    Difftastic uses Dijkstra's algorithm to find the optimal route. To avoid exponential memory usage, the graph is not constructed upfront; instead, vertex neighbors are generated dynamically as the algorithm explores the graph.