breadcrumbs.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import {
  2. findInTree,
  3. findInTreeOrThrow,
  4. } from '@/features/file-tree/util/find-in-tree'
  5. import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
  6. import { useOutlineContext } from '@/features/ide-react/context/outline-context'
  7. import useNestedOutline from '@/features/outline/hooks/use-nested-outline'
  8. import getChildrenLines from '@/features/outline/util/get-children-lines'
  9. import MaterialIcon from '@/shared/components/material-icon'
  10. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  11. import { Fragment, useMemo } from 'react'
  12. import { Outline } from '@/features/source-editor/utils/tree-operations/outline'
  13. const constructOutlineHierarchy = (
  14. items: Outline[],
  15. highlightedLine: number,
  16. outlineHierarchy: Outline[] = []
  17. ) => {
  18. for (const item of items) {
  19. if (item.line === highlightedLine) {
  20. outlineHierarchy.push(item)
  21. return outlineHierarchy
  22. }
  23. const childLines = getChildrenLines(item.children)
  24. if (childLines.includes(highlightedLine)) {
  25. outlineHierarchy.push(item)
  26. return constructOutlineHierarchy(
  27. item.children as Outline[],
  28. highlightedLine,
  29. outlineHierarchy
  30. )
  31. }
  32. }
  33. return outlineHierarchy
  34. }
  35. export default function Breadcrumbs() {
  36. const { openEntity } = useFileTreeOpenContext()
  37. const { fileTreeData } = useFileTreeData()
  38. const outline = useNestedOutline()
  39. const { highlightedLine, canShowOutline } = useOutlineContext()
  40. const folderHierarchy = useMemo(() => {
  41. if (openEntity?.type !== 'doc' || !fileTreeData) {
  42. return []
  43. }
  44. try {
  45. return openEntity.path
  46. .filter(id => id !== fileTreeData._id) // Filter out the root folder
  47. .map(id => {
  48. return findInTreeOrThrow(fileTreeData, id)?.entity
  49. })
  50. } catch {
  51. // If any of the folders in the path are not found, the entire hierarchy
  52. // is invalid.
  53. return []
  54. }
  55. }, [openEntity, fileTreeData])
  56. const fileName = useMemo(() => {
  57. // NOTE: openEntity.entity.name may not always be accurate, so we read it
  58. // from the file tree data instead.
  59. if (openEntity?.type !== 'doc' || !fileTreeData) {
  60. return undefined
  61. }
  62. return findInTree(fileTreeData, openEntity.entity._id)?.entity.name
  63. }, [fileTreeData, openEntity])
  64. const outlineHierarchy = useMemo(() => {
  65. if (openEntity?.type !== 'doc' || !canShowOutline || !outline) {
  66. return []
  67. }
  68. return constructOutlineHierarchy(outline.items, highlightedLine)
  69. }, [outline, highlightedLine, canShowOutline, openEntity])
  70. if (openEntity?.type !== 'doc' || !fileTreeData) {
  71. return null
  72. }
  73. const numOutlineItems = outlineHierarchy.length
  74. return (
  75. <div className="ol-cm-breadcrumbs" translate="no">
  76. {folderHierarchy.map(folder => (
  77. <Fragment key={folder._id}>
  78. <div>{folder.name}</div>
  79. <Chevron />
  80. </Fragment>
  81. ))}
  82. <MaterialIcon unfilled type="description" />
  83. <div>{fileName}</div>
  84. {numOutlineItems > 0 && <Chevron />}
  85. {outlineHierarchy.map((section, idx) => (
  86. <Fragment key={section.line}>
  87. <div>{section.title}</div>
  88. {idx < numOutlineItems - 1 && <Chevron />}
  89. </Fragment>
  90. ))}
  91. </div>
  92. )
  93. }
  94. const Chevron = () => (
  95. <MaterialIcon className="ol-cm-breadcrumb-chevron" type="chevron_right" />
  96. )