@nkzw/codiff-core 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,7 @@ declare const extractPatchHunks: (patch: string) => {
26
26
  added: number;
27
27
  deleted: number;
28
28
  header: string;
29
+ patch: string;
29
30
  }[];
30
31
  /** @param {ReadonlyArray<{added: number; deleted: number}>} hunks */
31
32
  declare const sumHunkLineCounts: (hunks: ReadonlyArray<{
@@ -95,6 +96,7 @@ declare const getSectionWalkthroughHunks: (file: {
95
96
  added: number;
96
97
  deleted: number;
97
98
  header: string;
99
+ patch: string;
98
100
  id: string;
99
101
  index: number;
100
102
  kind: string;
@@ -102,6 +102,7 @@ const extractPatchHunks = (patch) => {
102
102
  index += 1;
103
103
  continue;
104
104
  }
105
+ const hunkStart = index;
105
106
  let additions = 0;
106
107
  let deletions = 0;
107
108
  index += 1;
@@ -115,7 +116,8 @@ const extractPatchHunks = (patch) => {
115
116
  ...parsed,
116
117
  added: additions,
117
118
  deleted: deletions,
118
- header
119
+ header,
120
+ patch: lines.slice(hunkStart, index).join("\n")
119
121
  });
120
122
  }
121
123
  return hunks;
@@ -1,7 +1,7 @@
1
1
  import type { ChangedFile, PullRequestExistingReviewComment, PullRequestReviewComment, RepositoryState } from '../types.ts';
2
2
  import type { CodeViewInstance, ReviewComment } from './app-types.ts';
3
3
  export declare const isInteractiveReviewEvent: (event: PointerEvent) => boolean;
4
- export declare const hasActiveTextSelection: () => boolean;
4
+ export declare const hasActiveTextSelection: (target?: Node | null) => boolean;
5
5
  export declare const isFileReviewComment: (comment: Pick<ReviewComment, 'anchor' | 'lineNumber' | 'side'>) => boolean;
6
6
  export declare const isLineReviewComment: (comment: ReviewComment) => comment is ReviewComment & {
7
7
  lineNumber: number;
package/dist/react.mjs CHANGED
@@ -44,7 +44,6 @@ import { ArrowRightIcon } from "@phosphor-icons/react/ArrowRight";
44
44
  import { CaretLeftIcon } from "@phosphor-icons/react/CaretLeft";
45
45
  import { CaretRightIcon } from "@phosphor-icons/react/CaretRight";
46
46
  import { ArrowsClockwiseIcon } from "@phosphor-icons/react/ArrowsClockwise";
47
- import { Terminal, init } from "ghostty-web";
48
47
  //#region app/components/Button.tsx
49
48
  const buttonVariants = cva("codiff-button", {
50
49
  defaultVariants: {
@@ -2380,9 +2379,31 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2380
2379
  //#endregion
2381
2380
  //#region lib/review-comments.ts
2382
2381
  const isInteractiveReviewEvent = (event) => event.composedPath().some((target) => target instanceof HTMLElement && (target.closest("button, textarea, input, select, a") || target.closest(".review-comment-thread")));
2383
- const hasActiveTextSelection = () => {
2382
+ const rangeIntersectsTarget = (range, target) => {
2383
+ const targetRoot = target.getRootNode();
2384
+ if (range.startContainer.getRootNode() !== targetRoot || range.endContainer.getRootNode() !== targetRoot) return false;
2385
+ const selectionRange = (target.ownerDocument ?? document).createRange();
2386
+ selectionRange.setStart(range.startContainer, range.startOffset);
2387
+ selectionRange.setEnd(range.endContainer, range.endOffset);
2388
+ return selectionRange.intersectsNode(target);
2389
+ };
2390
+ const isExpandedSelection = (selection, target) => {
2391
+ if (selection == null || selection.rangeCount === 0) return false;
2392
+ const range = selection.getRangeAt(0);
2393
+ return (!selection.isCollapsed || selection.toString().length > 0 || !range.collapsed) && (!target || rangeIntersectsTarget(range, target));
2394
+ };
2395
+ const isExpandedRange = (range, target) => range != null && !range.collapsed && (!target || rangeIntersectsTarget(range, target));
2396
+ const hasActiveTextSelection = (target) => {
2384
2397
  const selection = window.getSelection();
2385
- return selection != null && selection.rangeCount > 0 && !selection.isCollapsed;
2398
+ const root = target?.getRootNode();
2399
+ const shadowRoot = root && "host" in root ? root : null;
2400
+ if (shadowRoot) {
2401
+ const composedRange = selection?.getComposedRanges?.({ shadowRoots: [shadowRoot] })[0];
2402
+ if (isExpandedRange(composedRange, target)) return true;
2403
+ const shadowSelection = shadowRoot.getSelection?.();
2404
+ if (isExpandedSelection(shadowSelection, target)) return true;
2405
+ }
2406
+ return isExpandedSelection(selection, target);
2386
2407
  };
2387
2408
  const isFileReviewComment = (comment) => comment.anchor === "file" || comment.lineNumber == null || comment.side == null;
2388
2409
  const isLineReviewComment = (comment) => !isFileReviewComment(comment);
@@ -5957,8 +5978,9 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5957
5978
  onSaveCommentEdit
5958
5979
  ]);
5959
5980
  const handleBlur = useCallback(() => {
5960
- onCommentBlur(flushDraft(), draft);
5981
+ onCommentBlur(comment, draft, flushDraft);
5961
5982
  }, [
5983
+ comment,
5962
5984
  draft,
5963
5985
  flushDraft,
5964
5986
  onCommentBlur
@@ -6590,6 +6612,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6590
6612
  const handledScrollRequestRef = useRef(null);
6591
6613
  const handledHunkNavRef = useRef(hunkNavigation?.request ?? null);
6592
6614
  const emptyCommentDeleteTimersRef = useRef(/* @__PURE__ */ new Map());
6615
+ const activePointerIdsRef = useRef(/* @__PURE__ */ new Set());
6616
+ const pendingCommentBlursRef = useRef(/* @__PURE__ */ new Map());
6617
+ const pendingCommentBlurTimerRef = useRef(null);
6618
+ const focusedCommentEditorIdRef = useRef(null);
6593
6619
  const definitionHighlightFrameRef = useRef(null);
6594
6620
  const highlightFrameRef = useRef(null);
6595
6621
  const ignoreNextLineSelectionEndRef = useRef(false);
@@ -6939,6 +6965,66 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6939
6965
  }
6940
6966
  emptyCommentDeleteTimersRef.current.clear();
6941
6967
  }, []);
6968
+ const flushPendingCommentBlurs = useCallback(() => {
6969
+ if (activePointerIdsRef.current.size > 0) return;
6970
+ const callbacks = [...pendingCommentBlursRef.current.values()];
6971
+ pendingCommentBlursRef.current.clear();
6972
+ for (const callback of callbacks) callback();
6973
+ }, []);
6974
+ const schedulePendingCommentBlurFlush = useCallback(() => {
6975
+ if (activePointerIdsRef.current.size > 0 || pendingCommentBlursRef.current.size === 0 || pendingCommentBlurTimerRef.current != null) return;
6976
+ const timer = window.setTimeout(() => {
6977
+ deferredTimersRef.current.delete(timer);
6978
+ pendingCommentBlurTimerRef.current = null;
6979
+ flushPendingCommentBlurs();
6980
+ }, 0);
6981
+ pendingCommentBlurTimerRef.current = timer;
6982
+ deferredTimersRef.current.add(timer);
6983
+ }, [flushPendingCommentBlurs]);
6984
+ useEffect(() => {
6985
+ const activePointerIds = activePointerIdsRef.current;
6986
+ const deferredTimers = deferredTimersRef.current;
6987
+ const pendingCommentBlurs = pendingCommentBlursRef.current;
6988
+ const handlePointerDown = (event) => {
6989
+ const timer = pendingCommentBlurTimerRef.current;
6990
+ if (timer != null) {
6991
+ window.clearTimeout(timer);
6992
+ deferredTimers.delete(timer);
6993
+ pendingCommentBlurTimerRef.current = null;
6994
+ }
6995
+ activePointerIds.add(event.pointerId);
6996
+ };
6997
+ const handlePointerEnd = (event) => {
6998
+ activePointerIds.delete(event.pointerId);
6999
+ schedulePendingCommentBlurFlush();
7000
+ };
7001
+ const handleWindowBlur = () => {
7002
+ activePointerIds.clear();
7003
+ schedulePendingCommentBlurFlush();
7004
+ };
7005
+ window.addEventListener("pointerdown", handlePointerDown, true);
7006
+ window.addEventListener("pointercancel", handlePointerEnd, true);
7007
+ window.addEventListener("pointerup", handlePointerEnd, true);
7008
+ window.addEventListener("blur", handleWindowBlur);
7009
+ return () => {
7010
+ window.removeEventListener("pointerdown", handlePointerDown, true);
7011
+ window.removeEventListener("pointercancel", handlePointerEnd, true);
7012
+ window.removeEventListener("pointerup", handlePointerEnd, true);
7013
+ window.removeEventListener("blur", handleWindowBlur);
7014
+ activePointerIds.clear();
7015
+ pendingCommentBlurs.clear();
7016
+ const timer = pendingCommentBlurTimerRef.current;
7017
+ if (timer != null) {
7018
+ window.clearTimeout(timer);
7019
+ deferredTimers.delete(timer);
7020
+ pendingCommentBlurTimerRef.current = null;
7021
+ }
7022
+ };
7023
+ }, [schedulePendingCommentBlurFlush]);
7024
+ const deferCommentBlurUntilPointerEnd = useCallback((commentId, callback) => {
7025
+ if (activePointerIdsRef.current.size > 0 || pendingCommentBlurTimerRef.current != null) pendingCommentBlursRef.current.set(commentId, callback);
7026
+ else callback();
7027
+ }, []);
6942
7028
  const scrollFileItemToTop = useCallback((itemId) => {
6943
7029
  const handle = codeViewRef.current;
6944
7030
  const viewer = handle?.getInstance();
@@ -7034,10 +7120,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7034
7120
  createCommentForRange(range, context);
7035
7121
  },
7036
7122
  onLineClick: (line, context) => {
7123
+ const lineElement = "lineElement" in line ? line.lineElement : null;
7037
7124
  if (isPrimaryModifier(line.event) && onFindDefinitions) {
7038
7125
  line.event.preventDefault();
7039
7126
  line.event.stopPropagation();
7040
- const lineElement = "lineElement" in line ? line.lineElement : null;
7041
7127
  const identifier = lineElement ? getIdentifierFromPointerEvent(line.event, lineElement) : null;
7042
7128
  const meta = itemMetadata.get(context.item.id);
7043
7129
  const side = "annotationSide" in line ? line.annotationSide : null;
@@ -7086,7 +7172,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7086
7172
  onLoadSection(meta.file, meta.section);
7087
7173
  return;
7088
7174
  }
7089
- if (hasActiveTextSelection()) return;
7175
+ if (hasActiveTextSelection(lineElement)) return;
7090
7176
  const side = "annotationSide" in line ? line.annotationSide : null;
7091
7177
  if (!side) return;
7092
7178
  cancelPendingEmptyCommentDeletes();
@@ -7147,6 +7233,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7147
7233
  wordWrap
7148
7234
  ]);
7149
7235
  const focusComment = useCallback((comment) => {
7236
+ focusedCommentEditorIdRef.current = comment.id;
7237
+ pendingCommentBlursRef.current.delete(comment.id);
7150
7238
  onCommentDraftChange?.({
7151
7239
  body: comment.body,
7152
7240
  id: comment.id
@@ -7157,25 +7245,32 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7157
7245
  deferredTimersRef.current.delete(timer);
7158
7246
  emptyCommentDeleteTimersRef.current.delete(comment.id);
7159
7247
  }, [onCommentDraftChange]);
7160
- const blurComment = useCallback((comment, body) => {
7161
- onCommentDraftChange?.(null);
7162
- clearCommentLineHighlight();
7163
- if (!comment.isReadOnly && body.trim().length === 0) {
7164
- const existingTimer = emptyCommentDeleteTimersRef.current.get(comment.id);
7165
- if (existingTimer != null) {
7166
- window.clearTimeout(existingTimer);
7167
- deferredTimersRef.current.delete(existingTimer);
7248
+ const blurComment = useCallback((comment, body, flushDraft) => {
7249
+ deferCommentBlurUntilPointerEnd(comment.id, () => {
7250
+ if (focusedCommentEditorIdRef.current === comment.id) {
7251
+ focusedCommentEditorIdRef.current = null;
7252
+ onCommentDraftChange?.(null);
7168
7253
  }
7169
- const timer = window.setTimeout(() => {
7170
- deferredTimersRef.current.delete(timer);
7171
- emptyCommentDeleteTimersRef.current.delete(comment.id);
7172
- onDeleteComment(comment.id);
7173
- }, 120);
7174
- deferredTimersRef.current.add(timer);
7175
- emptyCommentDeleteTimersRef.current.set(comment.id, timer);
7176
- }
7254
+ flushDraft();
7255
+ clearCommentLineHighlight();
7256
+ if (!comment.isReadOnly && body.trim().length === 0) {
7257
+ const existingTimer = emptyCommentDeleteTimersRef.current.get(comment.id);
7258
+ if (existingTimer != null) {
7259
+ window.clearTimeout(existingTimer);
7260
+ deferredTimersRef.current.delete(existingTimer);
7261
+ }
7262
+ const timer = window.setTimeout(() => {
7263
+ deferredTimersRef.current.delete(timer);
7264
+ emptyCommentDeleteTimersRef.current.delete(comment.id);
7265
+ onDeleteComment(comment.id);
7266
+ }, 120);
7267
+ deferredTimersRef.current.add(timer);
7268
+ emptyCommentDeleteTimersRef.current.set(comment.id, timer);
7269
+ }
7270
+ });
7177
7271
  }, [
7178
7272
  clearCommentLineHighlight,
7273
+ deferCommentBlurUntilPointerEnd,
7179
7274
  onCommentDraftChange,
7180
7275
  onDeleteComment
7181
7276
  ]);
@@ -7883,6 +7978,12 @@ function resolveBackdrop(element) {
7883
7978
  }
7884
7979
  return "#00000000";
7885
7980
  }
7981
+ let ghosttyModule = null;
7982
+ const loadGhostty = async () => {
7983
+ const ghostty = await (ghosttyModule ??= import("ghostty-web"));
7984
+ await ghostty.init();
7985
+ return ghostty;
7986
+ };
7886
7987
  /**
7887
7988
  * Read-only ghostty-web terminal that replays the streamed commit output, so
7888
7989
  * ANSI colors and cursor movement from pre-commit hooks render as they would
@@ -7897,7 +7998,7 @@ function CommitLogTerminal({ output }) {
7897
7998
  if (!container) return;
7898
7999
  let disposed = false;
7899
8000
  let instance = null;
7900
- init().then(() => {
8001
+ loadGhostty().then(({ Terminal }) => {
7901
8002
  if (disposed) return;
7902
8003
  const style = getComputedStyle(container);
7903
8004
  instance = new Terminal({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/codiff-core",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "A fast diff viewer.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "dependencies": {
54
- "@nkzw/mdx-editor": "^0.2.2",
54
+ "@nkzw/mdx-editor": "^1.0.1",
55
55
  "@nkzw/use-relative-time": "^1.2.3",
56
56
  "@phosphor-icons/react": "^2.1.10",
57
57
  "@pierre/diffs": "1.3.6",
@@ -59,7 +59,7 @@
59
59
  "@radix-ui/react-slot": "^1.3.3",
60
60
  "class-variance-authority": "^0.7.1",
61
61
  "ghostty-web": "^0.4.0",
62
- "lucide-react": "^1.34.0",
62
+ "lucide-react": "^1.39.0",
63
63
  "valibot": "^1.4.2"
64
64
  },
65
65
  "peerDependencies": {