Merge pull request #10111 from overleaf/mj-lezer-file-outline

[web] Parser backed file outline

GitOrigin-RevId: 0825f4887ba4dae24a14dd1880d07b791d0b4cd9
This commit is contained in:
Mathias Jakobsen 2022-11-03 12:43:59 +00:00 committed by Copybot
parent 986d52b8d4
commit a54b633726
10 changed files with 82 additions and 4 deletions

View file

@ -34,4 +34,5 @@ aside.editor-sidebar.full-size(
on-toggle="onToggle" on-toggle="onToggle"
event-tracking="eventTracking" event-tracking="eventTracking"
highlighted-line="highlightedLine" highlighted-line="highlightedLine"
show="show"
) )

View file

@ -1,4 +1,5 @@
window.i18n = { currentLangCode: 'en' } window.i18n = { currentLangCode: 'en' }
window.ExposedSettings = { window.ExposedSettings = {
appName: 'Overleaf', appName: 'Overleaf',
validRootDocExtensions: ['tex', 'Rtex', 'ltx'],
} as typeof window.ExposedSettings } as typeof window.ExposedSettings

View file

@ -107,6 +107,9 @@ OutlineItem.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
level: PropTypes.number, level: PropTypes.number,
children: PropTypes.array, children: PropTypes.array,
// Used for caching in CM6
from: PropTypes.number,
to: PropTypes.number,
}).isRequired, }).isRequired,
jumpToLine: PropTypes.func.isRequired, jumpToLine: PropTypes.func.isRequired,
highlightedLine: PropTypes.number, highlightedLine: PropTypes.number,

View file

@ -16,6 +16,7 @@ const OutlinePane = React.memo(function OutlinePane({
onToggle, onToggle,
eventTracking, eventTracking,
highlightedLine, highlightedLine,
show,
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -46,6 +47,12 @@ const OutlinePane = React.memo(function OutlinePane({
} }
} }
// NOTE: This flag is for disabling the rendering of the component. Used while
// both an Angular and React-based file outline is present in the code base.
if (!show) {
return null
}
return ( return (
<div className={headerClasses}> <div className={headerClasses}>
<header className="outline-header"> <header className="outline-header">
@ -82,6 +89,7 @@ OutlinePane.propTypes = {
onToggle: PropTypes.func.isRequired, onToggle: PropTypes.func.isRequired,
eventTracking: PropTypes.object.isRequired, eventTracking: PropTypes.object.isRequired,
highlightedLine: PropTypes.number, highlightedLine: PropTypes.number,
show: PropTypes.bool.isRequired,
} }
export default withErrorBoundary(OutlinePane) export default withErrorBoundary(OutlinePane)

View file

@ -8,6 +8,23 @@ App.controller('OutlineController', function ($scope, ide, eventTracking) {
$scope.outline = [] $scope.outline = []
$scope.eventTracking = eventTracking $scope.eventTracking = eventTracking
function shouldShowOutline() {
if ($scope.editor.showRichText) {
return true
}
return !$scope.editor.newSourceEditor
}
$scope.show = shouldShowOutline()
$scope.$watch('editor.newSourceEditor', function () {
$scope.show = shouldShowOutline()
})
$scope.$watch('editor.showRichText', function () {
$scope.show = shouldShowOutline()
})
$scope.$on('outline-manager:outline-changed', onOutlineChange) $scope.$on('outline-manager:outline-changed', onOutlineChange)
function onOutlineChange(e, outlineInfo) { function onOutlineChange(e, outlineInfo) {
@ -40,5 +57,6 @@ App.component(
'eventTracking', 'eventTracking',
'onToggle', 'onToggle',
'isTexFile', 'isTexFile',
'show',
]) ])
) )

View file

@ -16,6 +16,13 @@ class OutlineManager {
this.ignoreNextScroll = false this.ignoreNextScroll = false
this.ignoreNextCursorUpdate = false this.ignoreNextCursorUpdate = false
scope.$watch('editor.newSourceEditor', (now, before) => {
if (before && !now) {
this.updateOutline()
this.broadcastChangeEvent()
}
})
scope.$on('doc:after-opened', (ev, { isNewDoc }) => { scope.$on('doc:after-opened', (ev, { isNewDoc }) => {
if (isNewDoc) { if (isNewDoc) {
// if a new doc is opened a cursor updates will be triggered before the // if a new doc is opened a cursor updates will be triggered before the
@ -65,10 +72,25 @@ class OutlineManager {
scope.$watch('editor.showRichText', () => { scope.$watch('editor.showRichText', () => {
this.ignoreNextScroll = true this.ignoreNextScroll = true
this.ignoreNextCursorUpdate = true this.ignoreNextCursorUpdate = true
if (this.shouldShowOutline()) {
this.updateOutline()
this.broadcastChangeEvent()
}
}) })
} }
shouldShowOutline() {
if (this.scope.editor.showRichText) {
return true
}
return !this.scope.editor.newSourceEditor
}
updateOutline() { updateOutline() {
// Disable if using CM6
if (!this.shouldShowOutline()) {
return
}
if (this.isTexFile) { if (this.isTexFile) {
const content = this.ide.editorManager.getCurrentDocValue() const content = this.ide.editorManager.getCurrentDocValue()
if (content) { if (content) {
@ -103,6 +125,10 @@ class OutlineManager {
} }
broadcastChangeEvent() { broadcastChangeEvent() {
// Disable if using CM6
if (!this.shouldShowOutline()) {
return
}
this.scope.$broadcast('outline-manager:outline-changed', { this.scope.$broadcast('outline-manager:outline-changed', {
isTexFile: this.isTexFile, isTexFile: this.isTexFile,
outline: this.outline, outline: this.outline,

View file

@ -231,7 +231,7 @@ export default EditorManager = (function () {
const done = isNewDoc => { const done = isNewDoc => {
const eventName = 'doc:after-opened' const eventName = 'doc:after-opened'
this.$scope.$broadcast(eventName, { isNewDoc }) this.$scope.$broadcast(eventName, { isNewDoc })
window.dispatchEvent(new CustomEvent(eventName, { isNewDoc })) window.dispatchEvent(new CustomEvent(eventName, { detail: isNewDoc }))
if (options.gotoLine != null) { if (options.gotoLine != null) {
// allow Ace to display document before moving, delay until next tick // allow Ace to display document before moving, delay until next tick
// added delay to make this happen later that gotoStoredPosition in // added delay to make this happen later that gotoStoredPosition in

View file

@ -0,0 +1,18 @@
import { DependencyList, useMemo, useRef } from 'react'
import { isEqual } from 'lodash'
function useDeepCompare(dependencies: DependencyList) {
const ref = useRef<DependencyList>([])
if (!isEqual(ref.current, dependencies)) {
ref.current = dependencies
}
return ref.current
}
export default function useDeepCompareMemo<T>(
factory: () => T,
dependencies: DependencyList
) {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(factory, useDeepCompare(dependencies))
}

View file

@ -10,11 +10,11 @@ export default function useScopeEventEmitter(eventName, broadcast = true) {
const { $scope } = useIdeContext() const { $scope } = useIdeContext()
return useCallback( return useCallback(
detail => { (...detail) => {
if (broadcast) { if (broadcast) {
$scope.$broadcast(eventName, detail) $scope.$broadcast(eventName, ...detail)
} else { } else {
$scope.$emit(eventName, detail) $scope.$emit(eventName, ...detail)
} }
}, },
[$scope, eventName, broadcast] [$scope, eventName, broadcast]

View file

@ -55,6 +55,7 @@ describe('<OutlinePane />', function () {
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
onToggle={onToggle} onToggle={onToggle}
eventTracking={eventTracking} eventTracking={eventTracking}
show
/> />
) )
@ -70,6 +71,7 @@ describe('<OutlinePane />', function () {
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
onToggle={onToggle} onToggle={onToggle}
eventTracking={eventTracking} eventTracking={eventTracking}
show
/> />
) )
@ -98,6 +100,7 @@ describe('<OutlinePane />', function () {
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
onToggle={onToggle} onToggle={onToggle}
eventTracking={eventTracking} eventTracking={eventTracking}
show
/> />
) )