Skip to content

Apply .gitattributes syntax highlighting to code-review #10157

Description

@Primajin

Description

A file called .gitattributes in the root of a project tells Github how to color-code / syntax highlight unknown files.

*.myext linguist-language=JavaScript

Would tell Github to display all *myext files like it would display Javascript files - e.g. const abc would make the word const reddish and abc blueish.

However this only applies when looking at a file in the code file viewer. When reviewing such a file in a PR in the code-review panel it uses the plain, non-color coded text only mode.

I would like that it applies the same color coding in a code review as it does in the file view 🙏🏻

Example URLs

https://github.com/refined-github/refined-github/pull/1234/changes?w=1

Activity

  1. fregante commented on Sep 30, 2026

    @fregante
    Member

    This is not a GitHub support repo. Extensions can't change what GitHub does with files.

  2. Primajin commented on Sep 30, 2026

    @Primajin
    Author

    Huh, so the extension can do amazing things like show and hide white spaces in the code-review tab, but it can not apply colors?

  3. fregante commented on Sep 30, 2026

    @fregante
    Member

    Correct. Space is just two characters; syntax highlighting means having to understand all the languages you're looking to highlight. Completely different effort. It's possible but it's not what Refined GitHub does.

  4. fregante commented on Sep 30, 2026

    @fregante
    Member

    By the way, the PR Files view is in beta, so please do report it to GitHub, it's possible that they just haven't noticed yet.

  5. Primajin commented on Sep 30, 2026

    @Primajin
    Author

    Alright will do - just so that this isn't a misunderstanding - I don't mean this should work with every file for every repo. I just meant it could check if there are mappings set up in the current repo for the current file and if so allows for color coding.

    I dug into this a bit more, and I think it is possible for an extension, because GitHub already computes the highlighting and ships it to the browser. It just does so on the file view, not on the new "Files changed" view.

    The gap. On rust-lang/rust#163493 (public), rust-lang's .gitattributes has *.fixed linguist-language=Rust. In the new /changes view, tests/ui/resolve/struct-shorthand-typo.fixed renders with zero pl-* spans, while its near-identical twin struct-shorthand-typo.rs in the same PR has 172. The classic diff view does color the .fixed file.

    The data is already there. The file view (/<owner>/<repo>/blob/<sha>/<path>) embeds "language":"Rust" (with the override applied) and a colorizedLines array: one ready-made HTML string per line, using the same pl-* classes the diff view's CSS already styles.

    The new view is easy to map onto:

    • table[aria-label="Diff for: <path>"] per file
    • td.diff-text-cell[data-diff-side="left|right"][data-line-number] per line
    • .diff-text-inner holds the text; the parent <code> already has syntax-highlighted-line
    • The page's embedded data has pullRequestsChangesRoute.comparison.fullDiff.baseOid / headOid

    Proof of concept. Fetch the blob page at baseOid for the left side and at headOid for the right side. For each diff line, take colorizedLines[lineNumber - 1], and replace .diff-text-inner only if the text content is identical. That guard means a wrong mapping can never change what the reviewer reads.

    Result on the PR above: 44/44 lines of the .fixed file painted, 0 mismatches. On a private repo with *.aktion linguist-language=JavaScript: 52/52, 0 mismatches.

    Before / after (same page, new Files changed view):

    Image Image
    DevTools snippet (paste on any /pull/<n>/changes page)
    (async () => {
      const route = JSON.parse(document.querySelector('script[data-target="react-app.embeddedData"]').textContent)
        .payload.pullRequestsChangesRoute;
      const {baseOid, headOid} = route.comparison.fullDiff;
      const repo = location.pathname.split('/').slice(0, 3).join('/');
    
      const findKey = (object, key) => {
        if (object && typeof object === 'object') {
          if (key in object) return object[key];
          for (const value of Object.values(object)) {
            const found = findKey(value, key);
            if (found !== undefined) return found;
          }
        }
      };
    
      const colorizedLines = async (oid, path) => {
        const response = await fetch(`${repo}/blob/${oid}/${path}`, {credentials: 'include'});
        if (!response.ok) return null;
        const page = new DOMParser().parseFromString(await response.text(), 'text/html');
        for (const script of page.querySelectorAll('script[type="application/json"]')) {
          try {
            const lines = findKey(JSON.parse(script.textContent), 'colorizedLines');
            if (Array.isArray(lines)) return lines;
          } catch {}
        }
        return null;
      };
    
      const report = {};
      for (const table of document.querySelectorAll('table[aria-label^="Diff for: "]')) {
        const path = table.getAttribute('aria-label').slice('Diff for: '.length);
        if (table.querySelector('.diff-text-inner [class^="pl-"]')) continue; // Already highlighted by GitHub
        const sides = {left: await colorizedLines(baseOid, path), right: await colorizedLines(headOid, path)};
        let painted = 0;
        let mismatched = 0;
        for (const cell of table.querySelectorAll('td.diff-text-cell[data-diff-side][data-line-number]')) {
          const html = sides[cell.dataset.diffSide]?.[Number(cell.dataset.lineNumber) - 1];
          const inner = cell.querySelector('.diff-text-inner');
          if (!inner || html == null) continue;
          const probe = document.createElement('div');
          probe.innerHTML = html;
          if (probe.textContent.replaceAll(/\s/g, '') !== inner.textContent.replaceAll(/\s/g, '')) {
            mismatched++;
            continue;
          }
          inner.innerHTML = html;
          painted++;
        }
        report[path] = {painted, mismatched};
      }
      console.table(report);
    })();

    What a real feature would still need:

    • Re-apply after React re-renders and as the virtualized list mounts rows (a MutationObserver)
    • Play nicely with show-whitespace, which the naive innerHTML swap currently overwrites
    • Accept the cost of up to two extra same-origin requests per uncolored file, and a dependency on the embedded-data shape, which isn't a public API

    So it's fair to say it's more effort or risk than you want to take on. But "extensions can't" doesn't hold: GitHub does the highlighting, and the extension only has to move it from one page to the other. I've also reported the underlying gap to GitHub, since the real fix belongs there: https://github.com/orgs/community/discussions/163932#discussioncomment-18682066

    Thanks for looking at this anyways - and kudos to this great extension, I really like it and it helps me a lot every day 🙏🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions