GitLab MR Preview Scroll Synchronization¶
Tài liệu này giải thích cách browser extension giữ hai bản Markdown Before và After ở 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:
- đồng bộ theo heading và diagram tương ứng;
- 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ể¶
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¶
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.
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¶
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 A và Heading 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¶
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