diff --git a/package-lock.json b/package-lock.json index 6a1f076994..2577008bf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7637,6 +7637,17 @@ "resolved": "services/clsi-perf", "link": true }, + "node_modules/@overleaf/codemirror-tree-view": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@overleaf/codemirror-tree-view/-/codemirror-tree-view-0.1.3.tgz", + "integrity": "sha512-/ysOnX+ovObqj0uR78tumQtK/y0qFwbawcCGxT9JDeyJPgfPrK3PYTIvoZ1SgmSxaXpOPkds7aL+4Hv6VWZqSw==", + "dev": true, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/@overleaf/contacts": { "resolved": "services/contacts", "link": true @@ -39635,6 +39646,7 @@ "@opentelemetry/sdk-trace-base": "^1.15.2", "@opentelemetry/sdk-trace-web": "^1.15.2", "@opentelemetry/semantic-conventions": "^1.15.2", + "@overleaf/codemirror-tree-view": "^0.1.3", "@overleaf/ranges-tracker": "*", "@overleaf/stream-utils": "*", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", @@ -46393,6 +46405,13 @@ } } }, + "@overleaf/codemirror-tree-view": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@overleaf/codemirror-tree-view/-/codemirror-tree-view-0.1.3.tgz", + "integrity": "sha512-/ysOnX+ovObqj0uR78tumQtK/y0qFwbawcCGxT9JDeyJPgfPrK3PYTIvoZ1SgmSxaXpOPkds7aL+4Hv6VWZqSw==", + "dev": true, + "requires": {} + }, "@overleaf/contacts": { "version": "file:services/contacts", "requires": { @@ -47778,6 +47797,7 @@ "@opentelemetry/sdk-trace-web": "^1.15.2", "@opentelemetry/semantic-conventions": "^1.15.2", "@overleaf/access-token-encryptor": "*", + "@overleaf/codemirror-tree-view": "^0.1.3", "@overleaf/fetch-utils": "*", "@overleaf/logger": "*", "@overleaf/metrics": "*", diff --git a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts index 1e7ef068aa..f2cf8a6134 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/codemirror-dev-tools.ts @@ -1,22 +1,8 @@ -import { - Annotation, - Compartment, - EditorSelection, - EditorState, - StateEffect, - StateField, - Transaction, -} from '@codemirror/state' -import { - Decoration, - DecorationSet, - EditorView, - ViewPlugin, -} from '@codemirror/view' -import { syntaxTree } from '@codemirror/language' import { toggleVisualEffect } from '../../extensions/visual/visual' -import { hasLanguageLoadedEffect } from '../../extensions/language' import customLocalStorage from '../../../../infrastructure/local-storage' +import { Compartment } from '@codemirror/state' +import { EditorView, ViewPlugin } from '@codemirror/view' +import { treeView } from '@overleaf/codemirror-tree-view' // to enable: window.localStorage.setItem('cm6-dev-tools', '"on"') // to disable: window.localStorage.removeItem('cm6-dev-tools') @@ -90,233 +76,12 @@ const toggleDevTools = () => { } } -const createExtension = () => - isActive() ? [devToolsView, highlightSelectedNode, devToolsTheme] : [] - -const devToolsTheme = EditorView.baseTheme({ - '.ol-cm-dev-tools-container': { - padding: '8px 8px 0', - backgroundColor: '#222', - color: '#eee', - fontSize: '13px', - flexShrink: '0', - fontFamily: '"SF Mono", monospace', - height: 'calc(100% - 32px)', - overflow: 'auto', - position: 'absolute', +const treeViewTheme = EditorView.baseTheme({ + // note: duplicate selector to ensure extension theme styles are overriden + '.cm-tree-view-container.cm-tree-view-container': { top: '32px', - right: 0, - width: '50%', - }, - '.ol-cm-dev-tools-item': { - cursor: 'pointer', - borderTop: '2px solid transparent', - borderBottom: '2px solid transparent', - scrollMargin: '2em', - }, - '.ol-cm-selected-node-highlight': { - backgroundColor: 'yellow', - }, - '.ol-cm-dev-tools-covered-item': { - backgroundColor: 'rgba(255, 255, 0, 0.2)', - }, - '.ol-cm-dev-tools-selected-item': { - backgroundColor: 'rgba(255, 255, 0, 0.5)', - color: '#000', - }, - '.ol-cm-dev-tools-cursor-before': { - borderTopColor: 'rgba(255, 255, 0, 1)', - '& + .ol-cm-dev-tools-cursor-before': { - borderTopColor: 'transparent', - }, - }, - '.ol-cm-dev-tools-positions': { - position: 'sticky', - bottom: '0', - backgroundColor: 'inherit', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'flex-end', - }, - '.ol-cm-dev-tools-position': { - padding: '4px 0', + minHeight: 'unset', }, }) -const fromDevTools = Annotation.define() - -const transactionIsFromDevTools = (tr: Transaction) => - tr.annotation(fromDevTools) - -const devToolsView = ViewPlugin.define(view => { - const scroller = document.querySelector('.cm-scroller') - - if (!scroller) { - return {} - } - - const container = document.createElement('div') - container.classList.add('ol-cm-dev-tools-container') - scroller.after(container) - scroller.style.width = '50%' - - const highlightNodeRange = (from: number, to: number) => { - view.dispatch({ - effects: [selectedNodeEffect.of({ from, to })], - }) - } - - const selectNodeRange = (from: number, to: number) => { - view.dispatch({ - annotations: [fromDevTools.of(true)], - selection: EditorSelection.single(from, to), - effects: EditorView.scrollIntoView(from, { y: 'center' }), - }) - view.focus() - } - - buildPanel(view.state, container, highlightNodeRange, selectNodeRange, true) - - return { - update(update) { - if ( - update.docChanged || - update.selectionSet || - hasLanguageLoadedEffect(update) - ) { - const scroll = !update.transactions.some(transactionIsFromDevTools) - buildPanel( - update.state, - container, - highlightNodeRange, - selectNodeRange, - scroll - ) - } - }, - destroy() { - container.remove() - scroller.style.width = 'unset' - }, - } -}) - -const buildPanel = ( - state: EditorState, - container: HTMLDivElement, - highlightNodeRange: (from: number, to: number) => void, - selectNodeRange: (from: number, to: number) => void, - scroll: boolean -) => { - container.textContent = '' // clear - - const tree = syntaxTree(state) - const { selection } = state - let itemToCenter: HTMLDivElement - - let depth = 0 - tree.iterate({ - enter(nodeRef) { - const { from, to, name } = nodeRef - - const element = document.createElement('div') - element.classList.add('ol-cm-dev-tools-item') - element.style.paddingLeft = `${depth * 16}px` - element.textContent = name - - element.addEventListener('mouseover', () => { - highlightNodeRange(from, to) - }) - - element.addEventListener('click', () => { - selectNodeRange(from, to) - }) - - container.append(element) - - for (const range of selection.ranges) { - // completely covered by selection - if (range.from <= from && range.to >= to) { - element.classList.add('ol-cm-dev-tools-selected-item') - itemToCenter = element - } else if ( - (range.from > from && range.from < to) || - (range.to > from && range.to < to) - ) { - element.classList.add('ol-cm-dev-tools-covered-item') - itemToCenter = element - } - - if (range.head === from) { - element.classList.add('ol-cm-dev-tools-cursor-before') - itemToCenter = element - } - } - depth++ - }, - leave(node) { - depth-- - }, - }) - - const positions = document.createElement('div') - positions.classList.add('ol-cm-dev-tools-positions') - container.append(positions) - - for (const range of state.selection.ranges) { - const line = state.doc.lineAt(range.head) - const column = range.head - line.from + 1 - const position = document.createElement('div') - position.classList.add('ol-cm-dev-tools-position') - position.textContent = `line ${line.number}, col ${column}, pos ${range.head}` - positions.append(position) - } - - if (scroll && itemToCenter!) { - window.setTimeout(() => { - itemToCenter.scrollIntoView({ - block: 'center', - inline: 'center', - }) - }) - } -} - -const selectedNodeEffect = StateEffect.define<{ - from: number - to: number -} | null>() - -const highlightSelectedNode = StateField.define({ - create() { - return Decoration.none - }, - update(value, tr) { - if (tr.selection) { - value = Decoration.none - } - for (const effect of tr.effects) { - if (effect.is(selectedNodeEffect)) { - if (effect.value) { - const { from, to } = effect.value - - // TODO: widget decoration if no range to decorate? - if (to > from) { - value = Decoration.set([ - Decoration.mark({ - class: 'ol-cm-selected-node-highlight', - }).range(from, to), - ]) - } - } else { - value = Decoration.none - } - } - } - return value - }, - provide(f) { - return EditorView.decorations.from(f) - }, -}) +const createExtension = () => (isActive() ? [treeView, treeViewTheme] : []) diff --git a/services/web/package.json b/services/web/package.json index 189be05d2c..bb2c1659a3 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -198,6 +198,7 @@ "@opentelemetry/sdk-trace-base": "^1.15.2", "@opentelemetry/sdk-trace-web": "^1.15.2", "@opentelemetry/semantic-conventions": "^1.15.2", + "@overleaf/codemirror-tree-view": "^0.1.3", "@overleaf/ranges-tracker": "*", "@overleaf/stream-utils": "*", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",