diff --git a/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx
new file mode 100644
index 0000000000..18b7567f41
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx
@@ -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(, 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 (
+
+ {folderHierarchy.map(folder => (
+
+ {folder.name}
+
+
+ ))}
+
+
{openEntity.entity.name}
+ {numOutlineItems > 0 &&
}
+ {outlineHierarchy.map((section, idx) => (
+
+ {section.title}
+ {idx < numOutlineItems - 1 && }
+
+ ))}
+
+ )
+}
+
+const Chevron = () => (
+
+)
diff --git a/services/web/frontend/js/features/outline/components/outline-container.tsx b/services/web/frontend/js/features/outline/components/outline-container.tsx
index d4b833ee8f..71a27db753 100644
--- a/services/web/frontend/js/features/outline/components/outline-container.tsx
+++ b/services/web/frontend/js/features/outline/components/outline-container.tsx
@@ -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(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 (
@@ -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
-}
diff --git a/services/web/frontend/js/features/outline/components/outline-list.tsx b/services/web/frontend/js/features/outline/components/outline-list.tsx
index fc8117c5ca..92282fba59 100644
--- a/services/web/frontend/js/features/outline/components/outline-list.tsx
+++ b/services/web/frontend/js/features/outline/components/outline-list.tsx
@@ -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,
diff --git a/services/web/frontend/js/features/outline/hooks/use-nested-outline.ts b/services/web/frontend/js/features/outline/hooks/use-nested-outline.ts
new file mode 100644
index 0000000000..22ade8f8d6
--- /dev/null
+++ b/services/web/frontend/js/features/outline/hooks/use-nested-outline.ts
@@ -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(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
+}
diff --git a/services/web/frontend/js/features/outline/util/get-children-lines.ts b/services/web/frontend/js/features/outline/util/get-children-lines.ts
new file mode 100644
index 0000000000..3b272ee763
--- /dev/null
+++ b/services/web/frontend/js/features/outline/util/get-children-lines.ts
@@ -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()
+}
diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
index 527acb56d0..6441a0358c 100644
--- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
+++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
@@ -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(null)
@@ -72,6 +74,7 @@ function CodeMirrorEditor() {
)
)}
+ {newEditor && }
diff --git a/services/web/frontend/js/features/source-editor/extensions/breadcrumbs-panel.ts b/services/web/frontend/js/features/source-editor/extensions/breadcrumbs-panel.ts
new file mode 100644
index 0000000000..a6756987ec
--- /dev/null
+++ b/services/web/frontend/js/features/source-editor/extensions/breadcrumbs-panel.ts
@@ -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)]
+}
diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts
index cd75b21b9a..ab63755802 100644
--- a/services/web/frontend/js/features/source-editor/extensions/index.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/index.ts
@@ -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): 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.
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
index 57c823fd52..e4f4b6193c 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
@@ -44,3 +44,4 @@
@import 'upgrade-prompt';
@import 'integrations-panel';
@import 'group-members';
+@import 'breadcrumbs';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/breadcrumbs.scss b/services/web/frontend/stylesheets/bootstrap-5/components/breadcrumbs.scss
new file mode 100644
index 0000000000..5df335d7de
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/breadcrumbs.scss
@@ -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);
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss
index bc94934ec6..4eed3c633f 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss
@@ -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 {