Bỏ qua

GitLab MR Preview Scroll Synchronization

Tài liệu này giải thích cách browser extension giữ hai bản Markdown BeforeAfter ở vị trí tương ứng trong View preview before/after. Tính năng được bật mặc định bằng nút Sync scroll: On và hoạt động theo hai tầng:

  1. đồng bộ theo heading và diagram tương ứng;
  2. fallback theo tỷ lệ cuộn khi không có mốc chung.

Mã nguồn chính:

  • browser-extension/src/preview-scroll-sync.js: thu thập mốc, ghép mốc, tính vị trí và điều phối sự kiện cuộn;
  • browser-extension/src/content-script.js: tạo hai preview, nút bật/tắt và nối controller vào giao diện;
  • browser-extension/src/diagram-parser.js: ghép diagram Before/After trước khi scroll synchronization sử dụng kết quả.

Xem thêm GitLab MR Diagram Pairing để hiểu cách hai diagram được xác định là cùng một đối tượng.

1. Vấn đề cần giải quyết

Hai file Markdown có thể có chiều cao rất khác nhau. Sao chép trực tiếp before.scrollTop sang after.scrollTop chỉ đúng khi nội dung hai phía gần như giống hệt nhau.

Ví dụ, After được thêm một đoạn dài trước phần Payment:

Before                         After
----------------------         ----------------------
Introduction                   Introduction
                               New deployment guide
Payment                        ... 40 dòng mới ...
Diagram payment                Payment
                               Diagram payment

Nếu sao chép cùng số pixel, khi Before đang ở Payment, After vẫn có thể đang ở giữa New deployment guide. Extension vì vậy dùng các mốc nội dung tương ứng thay cho tọa độ tuyệt đối.

2. Luồng tổng thể

Kroki

Pane được người dùng cuộn gần nhất là source pane. Pane còn lại là target pane. Cơ chế chạy được theo cả hai chiều Before → After và After → Before.

3. Semantic anchor là gì?

Semantic anchor là một vị trí có ý nghĩa trong nội dung, có thể nhận diện ở cả hai phiên bản.

Heading anchor

Extension đọc các thẻ h1 đến h6, chuẩn hóa khoảng trắng và lowercase nội dung. Khóa của heading có dạng:

heading:h2:payment flow

Heading chỉ ghép khi cấp heading và text sau chuẩn hóa giống nhau. Nếu cùng một heading được lặp lại nhiều lần, lần thứ nhất ghép với lần thứ nhất, lần thứ hai ghép với lần thứ hai.

Diagram anchor

Extension nhận diện ảnh diagram từ engine trong URL ảnh, sau đó liên kết ảnh với danh sách diagram đã parse từ Markdown. Chỉ diagram có cả Before và After mới tạo anchor chung.

Khóa được tạo như sau:

Cách diagram được ghép Khóa scroll anchor
Stable ID diagram:id:<id>
Legacy identity hoặc occurrence fallback diagram:pair:<pair-index>
Chỉ có Before hoặc chỉ có After Không tạo anchor chung

Ví dụ, hai diagram cùng {id=checkout-flow} tạo cùng khóa:

Before: diagram:id:checkout-flow
After:  diagram:id:checkout-flow

Stable ID không bắt buộc để sync scroll hoạt động, nhưng giúp diagram vẫn là một mốc chung khi đổi title, vị trí hoặc renderer.

4. Cách ghép và lọc anchor

Kroki

Scroll mapping phải là một hàm tăng dần: cuộn xuống ở Before không được khiến After nhảy ngược lên. Vì vậy, nếu các anchor thể hiện nội dung đã đổi thứ tự và tạo đường chéo, extension giữ chuỗi anchor không đi lùi và bỏ các anchor giao nhau còn lại.

Đây là cơ chế an toàn cho thao tác cuộn, không thay đổi kết quả diagram pairing và không tuyên bố rằng diagram bị bỏ khỏi anchor đã bị ghép sai.

5. Nội suy giữa hai anchor

Extension thêm đầu và cuối tài liệu làm hai mốc ẩn. Với vị trí đang cuộn, nó tìm:

  • anchor gần nhất ở phía trên;
  • anchor gần nhất ở phía dưới;
  • tỷ lệ tiến triển giữa hai anchor.

Sau đó tỷ lệ đó được áp dụng vào khoảng tương ứng ở target pane.

Kroki

Trong ví dụ:

progress = (300 - 200) / (600 - 200) = 0.25
target   = 800 + 0.25 × (1200 - 800) = 900

Đây là piecewise interpolation: mỗi khoảng giữa hai anchor có tỷ lệ riêng. Nó chính xác hơn dùng một tỷ lệ duy nhất cho toàn bộ file khi chỉ một vài section thay đổi chiều cao.

6. Xử lý sự kiện cuộn

Kroki

requestAnimationFrame gộp nhiều scroll event trong cùng một frame. Nếu người dùng cuộn nhanh, phía phát sinh sự kiện gần nhất trở thành source cho lần tính tiếp theo.

Extension ghi nhớ vị trí vừa đặt bằng chương trình. Khi target pane phát sinh scroll event tương ứng, event đó bị bỏ qua để tránh Before và After liên tục kích hoạt lẫn nhau.

7. Các trường hợp thực tế

Case 1 — Hai file gần giống nhau

Before                         After
Introduction                   Introduction
Architecture                   Architecture
Diagram A                      Diagram A

Heading và diagram đều tạo anchor. Hai pane bám sát nhau; kết quả gần giống đồng bộ theo pixel nhưng ổn định hơn khi ảnh có kích thước khác nhau.

Case 2 — Thêm prose giữa hai section

Before                         After
Heading A                      Heading A
                               Đoạn mới rất dài
Heading B                      Heading B

Heading AHeading B là hai anchor biên. Khi cuộn trong khoảng này, extension nội suy theo tiến độ của từng phía. Before không bị kéo vào cùng số pixel với đoạn mới của After.

Case 3 — Thêm hoặc xóa một diagram

Before                         After
Diagram A                      Diagram A
                               Diagram New
Diagram B                      Diagram B

Diagram New không có cặp nên không trở thành anchor chung. Vị trí trong vùng này được nội suy giữa Diagram A và Diagram B. Với diagram bị xóa, cách xử lý đối xứng.

Case 4 — Diagram đổi renderer nhưng giữ stable ID

```plantuml {id=system-context}
...
```

```blockdiag {id=system-context}
...
```

Diagram pairing ghép hai block bằng ID dù engine khác nhau. Ảnh PlantUML và BlockDiag được gắn cùng diagram:id:system-context, nên vẫn tạo một anchor scroll chung.

Case 5 — Heading đổi tên nhưng diagram giữ ID

Before                         After
## System Design               ## Architecture Design
Diagram {id=context}           Diagram {id=context}

Hai heading không ghép vì text khác nhau. Diagram có ID vẫn là anchor chung, vì vậy scroll synchronization tiếp tục căn theo diagram và các anchor lân cận.

Case 6 — Heading trùng nhau

Before                         After
## Details (lần 1)             ## Details (lần 1)
## Details (lần 2)             ## Details (lần 2)

Extension ghép theo occurrence. Nếu After chèn thêm một ## Details ở đầu, occurrence có thể bị lệch. Đây là giới hạn của heading không có stable ID; stable diagram ID gần section giúp bổ sung anchor đáng tin cậy hơn.

Case 7 — Diagram hoặc section được di chuyển

Before                         After
Diagram A                      Diagram B
Diagram B                      Diagram A

Hai cặp anchor tạo đường giao nhau. Extension bỏ anchor làm offset After đi ngược để đảm bảo thao tác cuộn luôn tiến xuống. Pane còn lại được căn bằng các anchor không giao nhau và đầu/cuối tài liệu.

Case 8 — Không có anchor chung

Ví dụ Before và After đổi toàn bộ heading, không có diagram được ghép. Extension dùng:

targetTop = sourceTop / sourceMax × targetMax

Đây là fallback theo tỷ lệ toàn tài liệu. Nó không hiểu ngữ nghĩa nhưng vẫn thuận tiện hơn hai pane hoàn toàn độc lập.

Case 9 — File mới hoặc file bị xóa

Một phía hiển thị nội dung rỗng và không có vùng cuộn hữu ích. Khi một pane có scrollMax = 0, vị trí đích là đầu pane. Người dùng vẫn có thể tắt sync để thao tác độc lập.

Case 10 — Ảnh diagram tải muộn

Ảnh lazy-loaded có thể làm chiều cao Markdown thay đổi sau khi preview xuất hiện. Extension không giữ tọa độ anchor cố định: danh sách và offset được tính lại trong lần đồng bộ tiếp theo, vì vậy lần cuộn sau sử dụng layout mới.

Case 11 — Ảnh thông thường hoặc diagram không nhận diện được

Chỉ ảnh có URL chứa engine được hỗ trợ và liên kết được với diagram đã parse mới trở thành diagram anchor. Ảnh Markdown thông thường hoặc ảnh không xác định engine không tham gia ghép anchor; heading và ratio fallback vẫn hoạt động.

Case 12 — Người dùng tắt đồng bộ

Khi chọn Sync scroll: Off:

  • tác vụ đồng bộ đang chờ bị hủy;
  • hai pane cuộn độc lập;
  • bật lại không tự kéo hai pane về cùng vị trí ngay lập tức;
  • lần cuộn tiếp theo sẽ tiếp tục đồng bộ theo semantic anchor.

Trạng thái mặc định được tạo lại là On khi preview hoặc trang được khởi tạo lại; hiện tại extension không lưu lựa chọn này qua các phiên.

8. Thứ tự ưu tiên và fallback

Kroki

Tóm tắt độ tin cậy:

Cơ chế Khi dùng Độ chính xác kỳ vọng
Diagram anchor từ stable ID Diagram có cùng ID hợp lệ Cao nhất cho diagram
Diagram anchor từ pairing fallback Diagram đã được parser ghép Phụ thuộc độ tin cậy của pairing
Heading anchor Cùng cấp, cùng text và occurrence Tốt khi cấu trúc heading ổn định
Ratio fallback Không có semantic anchor Xấp xỉ

9. Giới hạn hiện tại

  • Không có nút Previous/Next change trong phạm vi tính năng này.
  • Heading không có stable ID; heading trùng hoặc đổi tên có thể mất hoặc lệch anchor.
  • Anchor bị di chuyển qua nhau phải bị loại để giữ ánh xạ cuộn đơn điệu.
  • Diagram anchor phụ thuộc cả kết quả pairing và khả năng nhận diện ảnh renderer trong HTML do GitLab trả về.
  • Sync scroll căn theo vị trí dọc, không đồng bộ horizontal scroll của code/table rộng.
  • Tính năng hỗ trợ review, không thay thế việc đọc source diff khi cần xác minh chính xác dòng thay đổi.

10. Kiểm thử liên quan

browser-extension/test/preview-scroll-sync.test.js kiểm tra:

  • nội suy giữa anchor;
  • ratio fallback;
  • heading trùng và anchor giao nhau;
  • đồng bộ hai chiều và bật/tắt;
  • tạo khóa diagram từ pairing;
  • thu thập heading và diagram anchor từ Markdown đã render.

Chạy riêng:

Set-Location browser-extension
node --test test/preview-scroll-sync.test.js

Chạy toàn bộ extension:

npm test
npm run build