mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-04 06:39:02 +02:00
Merge pull request #23747 from overleaf/dp-breadcrumbs
Add file breadcrumbs to new editor GitOrigin-RevId: 54bde446ad632976503a2c4aff915c862bad710e
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { findInTreeOrThrow } from '@/features/file-tree/util/find-in-tree'
|
||||
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
|
||||
import { useOutlineContext } from '@/features/ide-react/context/outline-context'
|
||||
import useNestedOutline from '@/features/outline/hooks/use-nested-outline'
|
||||
import getChildrenLines from '@/features/outline/util/get-children-lines'
|
||||
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { getPanel } from '@codemirror/view'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import { Outline } from '@/features/source-editor/utils/tree-operations/outline'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { createBreadcrumbsPanel } from '@/features/source-editor/extensions/breadcrumbs-panel'
|
||||
|
||||
const constructOutlineHierarchy = (
|
||||
items: Outline[],
|
||||
highlightedLine: number,
|
||||
outlineHierarchy: Outline[] = []
|
||||
) => {
|
||||
for (const item of items) {
|
||||
if (item.line === highlightedLine) {
|
||||
outlineHierarchy.push(item)
|
||||
return outlineHierarchy
|
||||
}
|
||||
|
||||
const childLines = getChildrenLines(item.children)
|
||||
if (childLines.includes(highlightedLine)) {
|
||||
outlineHierarchy.push(item)
|
||||
return constructOutlineHierarchy(
|
||||
item.children as Outline[],
|
||||
highlightedLine,
|
||||
outlineHierarchy
|
||||
)
|
||||
}
|
||||
}
|
||||
return outlineHierarchy
|
||||
}
|
||||
|
||||
export default function Breadcrumbs() {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const panel = getPanel(view, createBreadcrumbsPanel)
|
||||
|
||||
if (!panel) {
|
||||
return null
|
||||
}
|
||||
return createPortal(<BreadcrumbsContent />, panel.dom)
|
||||
}
|
||||
|
||||
function BreadcrumbsContent() {
|
||||
const { openEntity } = useFileTreeOpenContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const outline = useNestedOutline()
|
||||
const { highlightedLine, canShowOutline } = useOutlineContext()
|
||||
|
||||
const folderHierarchy = useMemo(() => {
|
||||
if (!openEntity || !fileTreeData) {
|
||||
return []
|
||||
}
|
||||
|
||||
return openEntity.path
|
||||
.filter(id => id !== fileTreeData._id) // Filter out the root folder
|
||||
.map(id => {
|
||||
return findInTreeOrThrow(fileTreeData, id)?.entity
|
||||
})
|
||||
}, [openEntity, fileTreeData])
|
||||
|
||||
const outlineHierarchy = useMemo(() => {
|
||||
if (!canShowOutline || !outline) {
|
||||
return []
|
||||
}
|
||||
|
||||
return constructOutlineHierarchy(outline.items, highlightedLine)
|
||||
}, [outline, highlightedLine, canShowOutline])
|
||||
|
||||
if (!openEntity || !fileTreeData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const numOutlineItems = outlineHierarchy.length
|
||||
|
||||
return (
|
||||
<div className="ol-cm-breadcrumbs">
|
||||
{folderHierarchy.map(folder => (
|
||||
<Fragment key={folder._id}>
|
||||
<div>{folder.name}</div>
|
||||
<Chevron />
|
||||
</Fragment>
|
||||
))}
|
||||
<MaterialIcon unfilled type="description" />
|
||||
<div>{openEntity.entity.name}</div>
|
||||
{numOutlineItems > 0 && <Chevron />}
|
||||
{outlineHierarchy.map((section, idx) => (
|
||||
<Fragment key={section.line}>
|
||||
<div>{section.title}</div>
|
||||
{idx < numOutlineItems - 1 && <Chevron />}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Chevron = () => (
|
||||
<MaterialIcon className="ol-cm-breadcrumb-chevron" type="chevron_right" />
|
||||
)
|
||||
@@ -1,20 +1,11 @@
|
||||
import { FC, memo, useEffect, useRef, useState } from 'react'
|
||||
import { FC, memo } from 'react'
|
||||
import OutlinePane from '@/features/outline/components/outline-pane'
|
||||
import {
|
||||
FlatOutlineState,
|
||||
PartialFlatOutline,
|
||||
useOutlineContext,
|
||||
} from '@/features/ide-react/context/outline-context'
|
||||
import {
|
||||
nestOutline,
|
||||
Outline,
|
||||
} from '@/features/source-editor/utils/tree-operations/outline'
|
||||
import { useOutlineContext } from '@/features/ide-react/context/outline-context'
|
||||
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import useNestedOutline from '../hooks/use-nested-outline'
|
||||
|
||||
export const OutlineContainer: FC = memo(() => {
|
||||
const {
|
||||
flatOutline,
|
||||
highlightedLine,
|
||||
canShowOutline,
|
||||
jumpToLine,
|
||||
@@ -24,31 +15,7 @@ export const OutlineContainer: FC = memo(() => {
|
||||
|
||||
const outlineToggledEmitter = useScopeEventEmitter('outline-toggled')
|
||||
|
||||
const [outline, setOutline] = useState<{
|
||||
items: Outline[]
|
||||
partial: boolean
|
||||
}>(() => ({ items: [], partial: false }))
|
||||
|
||||
const prevFlatOutlineRef = useRef<FlatOutlineState>(undefined)
|
||||
|
||||
// when the flat outline changes, calculate the nested outline
|
||||
// TODO: only calculate when outlineExpanded is true
|
||||
useEffect(() => {
|
||||
const prevFlatOutline = prevFlatOutlineRef.current
|
||||
prevFlatOutlineRef.current = flatOutline
|
||||
|
||||
if (flatOutline) {
|
||||
if (outlineChanged(prevFlatOutline?.items, flatOutline.items)) {
|
||||
debugConsole.log('Rebuilding changed outline')
|
||||
setOutline({
|
||||
items: nestOutline(flatOutline.items),
|
||||
partial: flatOutline.partial,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setOutline({ items: [], partial: false })
|
||||
}
|
||||
}, [flatOutline])
|
||||
const outline = useNestedOutline()
|
||||
|
||||
return (
|
||||
<div className="outline-container">
|
||||
@@ -66,30 +33,3 @@ export const OutlineContainer: FC = memo(() => {
|
||||
)
|
||||
})
|
||||
OutlineContainer.displayName = 'OutlineContainer'
|
||||
|
||||
const outlineChanged = (
|
||||
a: PartialFlatOutline | undefined,
|
||||
b: PartialFlatOutline
|
||||
): boolean => {
|
||||
if (!a) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const aItem = a[i]
|
||||
const bItem = b[i]
|
||||
if (
|
||||
aItem.level !== bItem.level ||
|
||||
aItem.line !== bItem.line ||
|
||||
aItem.title !== bItem.title
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,14 +2,7 @@ import classNames from 'classnames'
|
||||
import OutlineItem from './outline-item'
|
||||
import { memo } from 'react'
|
||||
import { OutlineItemData } from '@/features/ide-react/types/outline'
|
||||
|
||||
function getChildrenLines(children?: OutlineItemData[]): number[] {
|
||||
return (children || [])
|
||||
.map(child => {
|
||||
return getChildrenLines(child.children).concat(child.line)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
import getChildrenLines from '../util/get-children-lines'
|
||||
|
||||
const OutlineList = memo(function OutlineList({
|
||||
outline,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
FlatOutlineState,
|
||||
PartialFlatOutline,
|
||||
useOutlineContext,
|
||||
} from '@/features/ide-react/context/outline-context'
|
||||
import {
|
||||
nestOutline,
|
||||
Outline,
|
||||
} from '@/features/source-editor/utils/tree-operations/outline'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
const outlineChanged = (
|
||||
a: PartialFlatOutline | undefined,
|
||||
b: PartialFlatOutline
|
||||
): boolean => {
|
||||
if (!a) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const aItem = a[i]
|
||||
const bItem = b[i]
|
||||
if (
|
||||
aItem.level !== bItem.level ||
|
||||
aItem.line !== bItem.line ||
|
||||
aItem.title !== bItem.title
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default function useNestedOutline() {
|
||||
const { flatOutline } = useOutlineContext()
|
||||
|
||||
const [nestedOutline, setNestedOutline] = useState<{
|
||||
items: Outline[]
|
||||
partial: boolean
|
||||
}>(() => ({ items: [], partial: false }))
|
||||
|
||||
const prevFlatOutlineRef = useRef<FlatOutlineState>(undefined)
|
||||
|
||||
// when the flat outline changes, calculate the nested outline
|
||||
// TODO: only calculate when outlineExpanded is true
|
||||
useEffect(() => {
|
||||
const prevFlatOutline = prevFlatOutlineRef.current
|
||||
prevFlatOutlineRef.current = flatOutline
|
||||
|
||||
if (flatOutline) {
|
||||
if (outlineChanged(prevFlatOutline?.items, flatOutline.items)) {
|
||||
debugConsole.log('Rebuilding changed outline')
|
||||
setNestedOutline({
|
||||
items: nestOutline(flatOutline.items),
|
||||
partial: flatOutline.partial,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setNestedOutline({ items: [], partial: false })
|
||||
}
|
||||
}, [flatOutline])
|
||||
|
||||
return nestedOutline
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OutlineItemData } from '@/features/ide-react/types/outline'
|
||||
|
||||
export default function getChildrenLines(
|
||||
children?: OutlineItemData[]
|
||||
): number[] {
|
||||
return (children || [])
|
||||
.map(child => {
|
||||
return getChildrenLines(child.children).concat(child.line)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CodeMirrorViewContext,
|
||||
} from './codemirror-context'
|
||||
import MathPreviewTooltip from './math-preview-tooltip'
|
||||
import Breadcrumbs from '@/features/ide-redesign/components/breadcrumbs'
|
||||
|
||||
// TODO: remove this when definitely no longer used
|
||||
export * from './codemirror-context'
|
||||
@@ -39,6 +40,7 @@ function CodeMirrorEditor() {
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const newReviewPanel = useFeatureFlag('review-panel-redesign')
|
||||
const newEditor = useFeatureFlag('editor-redesign')
|
||||
|
||||
// create the view using the initial state and intercept transactions
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
@@ -72,6 +74,7 @@ function CodeMirrorEditor() {
|
||||
<Component key={path} />
|
||||
)
|
||||
)}
|
||||
{newEditor && <Breadcrumbs />}
|
||||
<CodeMirrorCommandTooltip />
|
||||
|
||||
<MathPreviewTooltip />
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { showPanel } from '@codemirror/view'
|
||||
|
||||
export function createBreadcrumbsPanel() {
|
||||
const dom = document.createElement('div')
|
||||
dom.classList.add('ol-cm-breadcrumbs-portal')
|
||||
return { dom, top: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* A panel which contains the editor breadcrumbs
|
||||
*/
|
||||
export const breadcrumbPanel = () => {
|
||||
if (!isSplitTestEnabled('editor-redesign')) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [showPanel.of(createBreadcrumbsPanel)]
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import { shortcuts } from './shortcuts'
|
||||
import { effectListeners } from './effect-listeners'
|
||||
import { highlightSpecialChars } from './highlight-special-chars'
|
||||
import { toolbarPanel } from './toolbar/toolbar-panel'
|
||||
import { breadcrumbPanel } from './breadcrumbs-panel'
|
||||
import { geometryChangeEvent } from './geometry-change-event'
|
||||
import { docName } from './doc-name'
|
||||
import { fileTreeItemDrop } from './file-tree-item-drop'
|
||||
@@ -151,6 +152,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
|
||||
mathPreview(options.settings.mathPreview),
|
||||
reviewTooltip(),
|
||||
toolbarPanel(),
|
||||
breadcrumbPanel(),
|
||||
verticalOverflow(),
|
||||
highlightActiveLine(options.visual.visual),
|
||||
// The built-in extension that highlights the active line in the gutter.
|
||||
|
||||
@@ -44,3 +44,4 @@
|
||||
@import 'upgrade-prompt';
|
||||
@import 'integrations-panel';
|
||||
@import 'group-members';
|
||||
@import 'breadcrumbs';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
:root {
|
||||
--breadcrumb-bg-color: var(--white);
|
||||
--breadcrumb-color: var(--content-secondary);
|
||||
--breadcrumb-chevron-color: var(--neutral-30);
|
||||
}
|
||||
|
||||
.ol-cm-breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-01);
|
||||
color: var(--breadcrumb-color);
|
||||
background-color: var(--breadcrumb-bg-color);
|
||||
font-size: var(--font-size-01);
|
||||
padding: var(--spacing-02);
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
& > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ol-cm-breadcrumb-chevron {
|
||||
color: var(--breadcrumb-chevron-color);
|
||||
}
|
||||
@@ -65,6 +65,14 @@
|
||||
--editor-toolbar-bg: var(--white);
|
||||
--toolbar-filetree-bg-color: var(--white);
|
||||
}
|
||||
|
||||
.cm-panels-top {
|
||||
border-bottom: none;
|
||||
|
||||
.ol-cm-toolbar-portal {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
||||
Reference in New Issue
Block a user