@nkzw/codiff-core 1.13.0 → 1.14.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.
@@ -0,0 +1,3 @@
1
+ import type { LineAnnotation } from '@pierre/diffs';
2
+ import type { ReviewAnnotationMetadata } from '../../lib/app-types.ts';
3
+ export declare function useCodeViewAnnotations(): (id: string, key: string, annotations: Array<LineAnnotation<ReviewAnnotationMetadata>>) => LineAnnotation<ReviewAnnotationMetadata>[];
@@ -0,0 +1,2 @@
1
+ import type { FileContents } from '@pierre/diffs';
2
+ export declare function useCodeViewPlaceholderFile(): (name: string, contents?: '' | ' ') => FileContents;
@@ -31,7 +31,7 @@ type WalkthroughHeaderAnnotationMetadata = {
31
31
  type: 'walkthrough-header';
32
32
  };
33
33
  export type ReviewAnnotationMetadata = CodeQualityAnnotationMetadata | ImagePreviewAnnotationMetadata | MarkdownPreviewAnnotationMetadata | ReviewCommentAnnotationMetadata | WalkthroughHeaderAnnotationMetadata;
34
- export type CodeViewInstance = NonNullable<ReturnType<CodeViewHandle<ReviewAnnotationMetadata>['getInstance']>>;
34
+ export type CodeViewInstance = NonNullable<ReturnType<CodeViewHandle<ReviewAnnotationMetadata, undefined>['getInstance']>>;
35
35
  export type DiffSearchMatch = {
36
36
  filePath: string;
37
37
  itemId: string;
@@ -3,6 +3,7 @@ export type IdentifierAtOffset = {
3
3
  start: number;
4
4
  };
5
5
  export declare const isNavigableIdentifierToken: (text: string) => boolean;
6
+ export declare const clearIdentifierNavigationWrappers: (root: ParentNode) => void;
6
7
  export declare const applyIdentifierNavigationState: (renderedItems: ReadonlyArray<{
7
8
  element: HTMLElement;
8
9
  }>, active: boolean) => void;
@@ -29,5 +29,6 @@ export declare const getCommentKey: (comment: Pick<ReviewComment, 'anchor' | 'li
29
29
  export declare const getReviewCommentsDigest: (comments: ReadonlyArray<ReviewComment>) => string;
30
30
  export declare const buildReviewCommentsMarkdown: (files: ReadonlyArray<ChangedFile>, comments: ReadonlyArray<ReviewComment>, showWhitespace: boolean, prefix?: string) => string;
31
31
  export declare const getReviewCommentsFromState: (state: RepositoryState) => ReadonlyArray<ReviewComment>;
32
+ export declare const getRefreshedReviewComments: (state: RepositoryState, currentComments: ReadonlyArray<ReviewComment>) => ReadonlyArray<ReviewComment>;
32
33
  export declare const getVisibleReviewComments: (comments: ReadonlyArray<ReviewComment>, showOutdated: boolean) => ReadonlyArray<ReviewComment>;
33
34
  export declare const shouldDiscardReviewCommentOnEscape: (body: string, confirmDiscard?: (message: string) => boolean) => boolean;
package/dist/react.mjs CHANGED
@@ -1505,7 +1505,7 @@ const parseSectionDiffWithOptions = (file, section, showWhitespace) => {
1505
1505
  }
1506
1506
  else if (section.patch.trim().length === 0) fileDiff = createEmptyFileDiff(file, section);
1507
1507
  else {
1508
- const parsedFileDiff = parsePatchFiles(section.patch)[0]?.files[0];
1508
+ const parsedFileDiff = parsePatchFiles(section.patch, cacheKey)[0]?.files[0];
1509
1509
  if (parsedFileDiff) {
1510
1510
  const loaded = parsedFileDiff.isPartial ? getLoadedSectionContents(file, section) : void 0;
1511
1511
  let hydrated = null;
@@ -3887,11 +3887,10 @@ const wrapIdentifierNavigationText = (root) => {
3887
3887
  };
3888
3888
  const applyIdentifierNavigationState = (renderedItems, active) => {
3889
3889
  for (const { element } of renderedItems) {
3890
- element.toggleAttribute(definitionModifierAttribute, active);
3891
3890
  const root = element.shadowRoot;
3892
- if (!root) continue;
3893
- clearIdentifierNavigationWrappers(root);
3894
- if (active) wrapIdentifierNavigationText(root);
3891
+ const enabled = active && !hasActiveTextSelection(root);
3892
+ element.toggleAttribute(definitionModifierAttribute, enabled);
3893
+ if (root && enabled) wrapIdentifierNavigationText(root);
3895
3894
  }
3896
3895
  };
3897
3896
  const getIdentifierAtOffset = (text, offset) => {
@@ -4039,6 +4038,7 @@ const highlightTextContainer = (container, normalizedQuery, activeMatch) => {
4039
4038
  const applySearchHighlights = (renderedItems, query, activeMatch) => {
4040
4039
  const normalizedQuery = query.trim().toLowerCase();
4041
4040
  for (const { element, id } of renderedItems) for (const root of getSearchableRoots(element)) {
4041
+ if (normalizedQuery) clearIdentifierNavigationWrappers(root);
4042
4042
  clearSearchHighlights(root);
4043
4043
  if (!normalizedQuery) continue;
4044
4044
  const matchForItem = activeMatch && activeMatch.itemId === id ? activeMatch : null;
@@ -4052,6 +4052,45 @@ const getSourceKey = (source) => source.type === "commit" ? `commit:${source.ref
4052
4052
  const getShortRef = (ref) => ref.slice(0, 7);
4053
4053
  const getSourceLabel = (source) => source.type === "commit" ? getShortRef(source.ref) : source.type === "branch" || source.type === "branch-diff" ? `Branch vs ${source.ref}` : source.type === "branch-working-tree" ? `Local + branch vs ${source.ref}` : source.type === "range" ? rangeLabel(source) : source.type === "pull-request" ? source.number ? `${source.provider === "gitlab" ? "MR" : "PR"} #${source.number}` : source.provider === "gitlab" ? "Merge request" : "Pull request" : "Uncommitted";
4054
4054
  //#endregion
4055
+ //#region app/hooks/useCodeViewAnnotations.ts
4056
+ function useCodeViewAnnotations() {
4057
+ const [getAnnotations] = useState(() => {
4058
+ const cache = /* @__PURE__ */ new Map();
4059
+ return (id, key, annotations) => {
4060
+ const previous = cache.get(id);
4061
+ if (previous?.key === key) return previous.annotations;
4062
+ cache.set(id, {
4063
+ annotations,
4064
+ key
4065
+ });
4066
+ return annotations;
4067
+ };
4068
+ });
4069
+ return getAnnotations;
4070
+ }
4071
+ //#endregion
4072
+ //#region app/hooks/useCodeViewPlaceholderFile.ts
4073
+ function useCodeViewPlaceholderFile() {
4074
+ const [getPlaceholderFile] = useState(() => {
4075
+ const files = /* @__PURE__ */ new Map();
4076
+ return (name, contents = " ") => {
4077
+ const cacheKey = `placeholder:${contents.length}:${name}`;
4078
+ let file = files.get(cacheKey);
4079
+ if (!file) {
4080
+ file = {
4081
+ cacheKey,
4082
+ contents,
4083
+ lang: "text",
4084
+ name
4085
+ };
4086
+ files.set(cacheKey, file);
4087
+ }
4088
+ return file;
4089
+ };
4090
+ });
4091
+ return getPlaceholderFile;
4092
+ }
4093
+ //#endregion
4055
4094
  //#region app/components/DefinitionPopover.tsx
4056
4095
  function DefinitionPopover({ anchor, getDestination, identifier, onClose, onOpen, result }) {
4057
4096
  const popoverRef = useRef(null);
@@ -5223,28 +5262,48 @@ const formatBytes = (size) => {
5223
5262
  }
5224
5263
  return `${size} B`;
5225
5264
  };
5226
- function MarkdownPreview({ contents, editable, layoutKey, onEditorRef, onLayoutReady, path, sectionId }) {
5227
- useLayoutEffect(() => {
5228
- onLayoutReady(sectionId);
5265
+ function MarkdownPreview({ cacheKey, contents, editable, heightCache, onEditorRef, onLayoutReady, path, sectionId }) {
5266
+ const contentRef = useRef(null);
5267
+ const [ready, setReady] = useState(false);
5268
+ const measure = useCallback((reportedHeight) => {
5269
+ const content = contentRef.current;
5270
+ if (!content?.isConnected || content.querySelector(".codiff-markdown-editor-message:not(.error), .codiff-readonly-markdown-loading")) return;
5271
+ const height = content.getBoundingClientRect().height || reportedHeight;
5272
+ if (height == null || height <= 0) return;
5273
+ setReady(true);
5274
+ if (heightCache.get(cacheKey) !== height) {
5275
+ heightCache.set(cacheKey, height);
5276
+ onLayoutReady(sectionId);
5277
+ }
5229
5278
  }, [
5230
- layoutKey,
5279
+ cacheKey,
5280
+ heightCache,
5231
5281
  onLayoutReady,
5232
5282
  sectionId
5233
5283
  ]);
5234
- return editable ? /* @__PURE__ */ jsx("div", {
5235
- className: "codiff-markdown-preview editable",
5236
- children: /* @__PURE__ */ jsx(RepositoryMarkdownEditor, {
5237
- onHeightChange: () => onLayoutReady(sectionId),
5238
- path,
5239
- ref: (editor) => onEditorRef(sectionId, editor)
5240
- })
5241
- }) : /* @__PURE__ */ jsx("div", {
5242
- className: "codiff-markdown-preview",
5243
- children: /* @__PURE__ */ jsx(ReadOnlyMarkdown, {
5244
- ariaLabel: `Preview ${path}`,
5245
- className: "codiff-markdown-preview-editor",
5246
- onHeightChange: () => onLayoutReady(sectionId),
5247
- value: contents
5284
+ useLayoutEffect(() => {
5285
+ const content = contentRef.current;
5286
+ if (!content) return;
5287
+ const observer = new ResizeObserver(() => measure());
5288
+ observer.observe(content);
5289
+ measure();
5290
+ return () => observer.disconnect();
5291
+ }, [measure]);
5292
+ return /* @__PURE__ */ jsx("div", {
5293
+ style: { minHeight: ready ? void 0 : heightCache.get(cacheKey) },
5294
+ children: /* @__PURE__ */ jsx("div", {
5295
+ className: `codiff-markdown-preview${editable ? " editable" : ""}`,
5296
+ ref: contentRef,
5297
+ children: editable ? /* @__PURE__ */ jsx(RepositoryMarkdownEditor, {
5298
+ onHeightChange: measure,
5299
+ path,
5300
+ ref: (editor) => onEditorRef(sectionId, editor)
5301
+ }) : /* @__PURE__ */ jsx(ReadOnlyMarkdown, {
5302
+ ariaLabel: `Preview ${path}`,
5303
+ className: "codiff-markdown-preview-editor",
5304
+ onHeightChange: measure,
5305
+ value: contents
5306
+ })
5248
5307
  })
5249
5308
  });
5250
5309
  }
@@ -6644,6 +6703,9 @@ const getHunkSelectionRange = (hunk) => {
6644
6703
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
6645
6704
  function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedReviewKeys = emptyExpandedReviewKeys, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds = emptyMarkdownPreviewSectionIds, isReadOnly = false, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread = noopResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription = true, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme = "system", viewed, walkthroughNotes, wordWrap }) {
6646
6705
  const codeViewRef = useRef(null);
6706
+ const getPlaceholderFile = useCodeViewPlaceholderFile();
6707
+ const getAnnotations = useCodeViewAnnotations();
6708
+ const [markdownPreviewHeights] = useState(() => /* @__PURE__ */ new Map());
6647
6709
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
6648
6710
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
6649
6711
  const deferredTimersRef = useRef(/* @__PURE__ */ new Set());
@@ -6751,12 +6813,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6751
6813
  }
6752
6814
  }],
6753
6815
  collapsed: false,
6754
- file: {
6755
- cacheKey: `walkthrough-header:${block.id}`,
6756
- contents: " ",
6757
- lang: "text",
6758
- name: headerId
6759
- },
6816
+ file: getPlaceholderFile(headerId),
6760
6817
  id: headerId,
6761
6818
  type: "file",
6762
6819
  version: getItemVersion(`${block.id}:walkthrough-header`)
@@ -6861,12 +6918,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6861
6918
  }
6862
6919
  }],
6863
6920
  collapsed: isCollapsed,
6864
- file: {
6865
- cacheKey: `image-preview:${file.fingerprint}:${section.id}`,
6866
- contents: " ",
6867
- lang: "text",
6868
- name: file.path
6869
- },
6921
+ file: getPlaceholderFile(file.path),
6870
6922
  id,
6871
6923
  type: "file",
6872
6924
  version: getItemVersion(`${reviewVersionPrefix}:image:${sectionStateVersionKey}:${imagePreviewLayoutPassBySection[section.id] ?? 0}`)
@@ -6877,7 +6929,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6877
6929
  const markdownPreviewAddedLinesDigest = getAddedLinesDigest(markdownPreview.addedLines);
6878
6930
  const markdownPreviewLayoutKey = `${section.id}:${markdownPreview.contents.length}:${markdownPreviewAddedLinesDigest}`;
6879
6931
  nextItems.push({
6880
- annotations: [...fileCommentAnnotations, {
6932
+ annotations: getAnnotations(id, JSON.stringify([
6933
+ markdownPreview.contents,
6934
+ markdownPreviewAddedLinesDigest,
6935
+ canEditMarkdown,
6936
+ fileCommentAnnotations
6937
+ ]), [...fileCommentAnnotations, {
6881
6938
  lineNumber: 1,
6882
6939
  metadata: {
6883
6940
  addedLines: markdownPreview.addedLines,
@@ -6888,14 +6945,9 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6888
6945
  sectionId: section.id,
6889
6946
  type: "markdown-preview"
6890
6947
  }
6891
- }],
6948
+ }]),
6892
6949
  collapsed: isCollapsed,
6893
- file: {
6894
- cacheKey: `markdown-preview:${section.newFile?.cacheKey ?? file.fingerprint}:${markdownPreview.contents.length}:${markdownPreviewAddedLinesDigest}`,
6895
- contents: " ",
6896
- lang: "text",
6897
- name: file.path
6898
- },
6950
+ file: getPlaceholderFile(file.path),
6899
6951
  id,
6900
6952
  type: "file",
6901
6953
  version: getItemVersion(`${reviewVersionPrefix}:markdown:${sectionStateVersionKey}:${markdownPreviewLayoutKey}:${markdownPreviewLayoutPassBySection[section.id] ?? 0}`)
@@ -6931,6 +6983,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6931
6983
  diffStyle,
6932
6984
  expandedReviewKeys,
6933
6985
  forceExpandedPaths,
6986
+ getAnnotations,
6987
+ getPlaceholderFile,
6934
6988
  imagePreviewLayoutPassBySection,
6935
6989
  isReadOnly,
6936
6990
  itemVersionByKey,
@@ -6949,16 +7003,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6949
7003
  if (items.length > 0 || !sourceDescriptionItemId) return items;
6950
7004
  return [{
6951
7005
  collapsed: true,
6952
- file: {
6953
- cacheKey: sourceDescriptionItemId,
6954
- contents: "",
6955
- lang: "text",
6956
- name: shouldShowCommitMessage ? "commit-message.md" : "source-description.md"
6957
- },
7006
+ file: getPlaceholderFile(shouldShowCommitMessage ? "commit-message.md" : "source-description.md", ""),
6958
7007
  id: sourceDescriptionItemId,
6959
7008
  type: "file"
6960
7009
  }];
6961
7010
  }, [
7011
+ getPlaceholderFile,
6962
7012
  items,
6963
7013
  shouldShowCommitMessage,
6964
7014
  sourceDescriptionItemId
@@ -7621,30 +7671,44 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7621
7671
  ]);
7622
7672
  useEffect(() => {
7623
7673
  if (!onFindDefinitions) return;
7674
+ let pointerDown = false;
7624
7675
  const updateDefinitionModifierState = (active) => {
7625
7676
  if (definitionModifierActiveRef.current === active) return;
7626
7677
  definitionModifierActiveRef.current = active;
7627
7678
  updateRenderedIdentifierNavigation(active);
7628
7679
  };
7629
7680
  const handleModifierChange = (event) => {
7630
- updateDefinitionModifierState(isPrimaryModifier(event));
7681
+ updateDefinitionModifierState(isPrimaryModifier(event) && !pointerDown);
7631
7682
  };
7632
7683
  const handleModifierPointer = (event) => {
7633
- updateDefinitionModifierState(isPrimaryModifier(event));
7684
+ pointerDown = (event.buttons & 1) !== 0;
7685
+ updateDefinitionModifierState(isPrimaryModifier(event) && !pointerDown);
7686
+ };
7687
+ const handleSelectionChange = () => {
7688
+ updateRenderedIdentifierNavigation(definitionModifierActiveRef.current);
7689
+ };
7690
+ const clearDefinitionModifierState = () => {
7691
+ pointerDown = false;
7692
+ updateDefinitionModifierState(false);
7634
7693
  };
7635
- const clearDefinitionModifierState = () => updateDefinitionModifierState(false);
7636
7694
  window.addEventListener("keydown", handleModifierChange);
7637
7695
  window.addEventListener("keyup", handleModifierChange);
7638
7696
  window.addEventListener("pointerdown", handleModifierPointer);
7639
7697
  window.addEventListener("pointermove", handleModifierPointer);
7698
+ window.addEventListener("pointerup", handleModifierPointer);
7699
+ window.addEventListener("pointercancel", clearDefinitionModifierState);
7640
7700
  window.addEventListener("blur", clearDefinitionModifierState);
7701
+ document.addEventListener("selectionchange", handleSelectionChange);
7641
7702
  return () => {
7642
7703
  clearDefinitionModifierState();
7643
7704
  window.removeEventListener("keydown", handleModifierChange);
7644
7705
  window.removeEventListener("keyup", handleModifierChange);
7645
7706
  window.removeEventListener("pointerdown", handleModifierPointer);
7646
7707
  window.removeEventListener("pointermove", handleModifierPointer);
7708
+ window.removeEventListener("pointerup", handleModifierPointer);
7709
+ window.removeEventListener("pointercancel", clearDefinitionModifierState);
7647
7710
  window.removeEventListener("blur", clearDefinitionModifierState);
7711
+ document.removeEventListener("selectionchange", handleSelectionChange);
7648
7712
  };
7649
7713
  }, [onFindDefinitions, updateRenderedIdentifierNavigation]);
7650
7714
  const invalidateDefinitionLookup = useCallback(() => {
@@ -7758,9 +7822,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7758
7822
  }) : null;
7759
7823
  }
7760
7824
  if (annotation.metadata.type === "markdown-preview") return /* @__PURE__ */ jsx(MarkdownPreview, {
7825
+ cacheKey: `${sourceKey}:${item.id}`,
7761
7826
  contents: annotation.metadata.contents,
7762
7827
  editable: annotation.metadata.editable,
7763
- layoutKey: annotation.metadata.layoutKey,
7828
+ heightCache: markdownPreviewHeights,
7764
7829
  onEditorRef: setMarkdownEditorRef,
7765
7830
  onLayoutReady: markMarkdownPreviewLayoutReady,
7766
7831
  path: annotation.metadata.path,
@@ -7802,6 +7867,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7802
7867
  itemMetadata,
7803
7868
  keymap,
7804
7869
  markMarkdownPreviewLayoutReady,
7870
+ markdownPreviewHeights,
7805
7871
  markImagePreviewLayoutReady,
7806
7872
  markCommentLayoutChanged,
7807
7873
  onAskCodex,
@@ -7815,6 +7881,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7815
7881
  replyToThread,
7816
7882
  setMarkdownEditorRef,
7817
7883
  source,
7884
+ sourceKey,
7818
7885
  supportsReviewCommentActions
7819
7886
  ]);
7820
7887
  const handleScroll = useCallback((_scrollTop, viewer) => {
package/dist/style.css CHANGED
@@ -2807,6 +2807,10 @@ form._multiFieldForm_sre1d_1628 {
2807
2807
  padding-left: 20px;
2808
2808
  }
2809
2809
 
2810
+ .mdx-editor .mdx-editor-content li > ul, .mdx-editor .mdx-editor-content li > ol {
2811
+ margin-block: 0;
2812
+ }
2813
+
2810
2814
  .mdx-editor-content li {
2811
2815
  margin: 0;
2812
2816
  padding-left: 2px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/codiff-core",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "A fast diff viewer.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -51,16 +51,16 @@
51
51
  }
52
52
  },
53
53
  "dependencies": {
54
- "@nkzw/mdx-editor": "^1.0.1",
54
+ "@nkzw/mdx-editor": "^1.0.3",
55
55
  "@nkzw/use-relative-time": "^1.2.3",
56
56
  "@phosphor-icons/react": "^2.1.10",
57
- "@pierre/diffs": "1.3.6",
57
+ "@pierre/diffs": "1.4.2",
58
58
  "@pierre/trees": "1.0.0-beta.6",
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.39.0",
63
- "valibot": "^1.4.2"
62
+ "lucide-react": "^1.46.0",
63
+ "valibot": "^1.5.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "react": "^19.2.0",