mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Migrate synctex controls to React (#5503)
GitOrigin-RevId: 80362a00ae6b73616a6fa9b3193b9b9974b5fd35
This commit is contained in:
parent
684efaaf5f
commit
913a62fbc8
20 changed files with 858 additions and 364 deletions
|
@ -24,6 +24,12 @@ div.full-size(
|
||||||
else
|
else
|
||||||
include ./pdf
|
include ./pdf
|
||||||
|
|
||||||
|
if showNewPdfPreview
|
||||||
|
.ui-layout-resizer-controls.synctex-controls(
|
||||||
|
ng-show="settings.pdfViewer !== 'native'"
|
||||||
|
)
|
||||||
|
pdf-synctex-controls()
|
||||||
|
else
|
||||||
.ui-layout-resizer-controls.synctex-controls(
|
.ui-layout-resizer-controls.synctex-controls(
|
||||||
ng-show="!!pdf.url && settings.pdfViewer !== 'native'"
|
ng-show="!!pdf.url && settings.pdfViewer !== 'native'"
|
||||||
ng-controller="PdfSynctexController"
|
ng-controller="PdfSynctexController"
|
||||||
|
|
|
@ -122,7 +122,9 @@
|
||||||
"go_next_page": "",
|
"go_next_page": "",
|
||||||
"go_page": "",
|
"go_page": "",
|
||||||
"go_prev_page": "",
|
"go_prev_page": "",
|
||||||
|
"go_to_code_location_in_pdf": "",
|
||||||
"go_to_error_location": "",
|
"go_to_error_location": "",
|
||||||
|
"go_to_pdf_location_in_code": "",
|
||||||
"have_an_extra_backup": "",
|
"have_an_extra_backup": "",
|
||||||
"headers": "",
|
"headers": "",
|
||||||
"hide_outline": "",
|
"hide_outline": "",
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { debounce } from 'lodash'
|
||||||
import PdfViewerControls from './pdf-viewer-controls'
|
import PdfViewerControls from './pdf-viewer-controls'
|
||||||
import { useProjectContext } from '../../../shared/context/project-context'
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
|
||||||
import { buildHighlightElement } from '../util/highlights'
|
import { buildHighlightElement } from '../util/highlights'
|
||||||
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
import PDFJSWrapper from '../util/pdf-js-wrapper'
|
||||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||||
|
@ -15,7 +14,12 @@ import getMeta from '../../../utils/meta'
|
||||||
function PdfJsViewer({ url }) {
|
function PdfJsViewer({ url }) {
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
|
|
||||||
const { setError, firstRenderDone } = useCompileContext()
|
const {
|
||||||
|
setError,
|
||||||
|
firstRenderDone,
|
||||||
|
highlights,
|
||||||
|
setPosition,
|
||||||
|
} = useCompileContext()
|
||||||
const [timePDFFetched, setTimePDFFetched] = useState()
|
const [timePDFFetched, setTimePDFFetched] = useState()
|
||||||
|
|
||||||
// state values persisted in localStorage to restore on load
|
// state values persisted in localStorage to restore on load
|
||||||
|
@ -24,10 +28,6 @@ function PdfJsViewer({ url }) {
|
||||||
'page-width'
|
'page-width'
|
||||||
)
|
)
|
||||||
|
|
||||||
// state values shared with Angular scope (highlights => editor, position => synctex buttons
|
|
||||||
const [highlights] = useScopeValue('pdf.highlights')
|
|
||||||
const [, setPosition] = useScopeValue('pdf.position')
|
|
||||||
|
|
||||||
// local state values
|
// local state values
|
||||||
const [pdfJsWrapper, setPdfJsWrapper] = useState()
|
const [pdfJsWrapper, setPdfJsWrapper] = useState()
|
||||||
const [initialised, setInitialised] = useState(false)
|
const [initialised, setInitialised] = useState(false)
|
||||||
|
@ -144,17 +144,17 @@ function PdfJsViewer({ url }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialised && pdfJsWrapper) {
|
if (initialised && pdfJsWrapper) {
|
||||||
setScale(scale => {
|
setScale(scale => {
|
||||||
pdfJsWrapper.viewer.currentScaleValue = scale
|
|
||||||
return scale
|
|
||||||
})
|
|
||||||
|
|
||||||
// restore the scroll position
|
|
||||||
setPosition(position => {
|
setPosition(position => {
|
||||||
if (position) {
|
if (position) {
|
||||||
pdfJsWrapper.currentPosition = position
|
pdfJsWrapper.scrollToPosition(position, scale)
|
||||||
|
} else {
|
||||||
|
pdfJsWrapper.viewer.currentScaleValue = scale
|
||||||
}
|
}
|
||||||
return position
|
return position
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return scale
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [initialised, setScale, setPosition, pdfJsWrapper])
|
}, [initialised, setScale, setPosition, pdfJsWrapper])
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,28 @@ import PropTypes from 'prop-types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
||||||
import PdfLogEntry from './pdf-log-entry'
|
import PdfLogEntry from './pdf-log-entry'
|
||||||
|
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||||
|
|
||||||
const LOG_PREVIEW_LIMIT = 100
|
const LOG_PREVIEW_LIMIT = 100
|
||||||
|
|
||||||
function PdfLogsEntries({ entries }) {
|
function PdfLogsEntries({ entries }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const syncToEntry = useCallback(entry => {
|
const ide = useIdeContext()
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('synctex:sync-to-entry', {
|
const syncToEntry = useCallback(
|
||||||
detail: entry,
|
entry => {
|
||||||
|
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
|
||||||
|
|
||||||
|
if (entity && entity.type === 'doc') {
|
||||||
|
ide.editorManager.openDoc(entity, {
|
||||||
|
gotoLine: entry.line ?? undefined,
|
||||||
|
gotoColumn: entry.column ?? undefined,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ide]
|
||||||
)
|
)
|
||||||
}, [])
|
|
||||||
|
|
||||||
const logEntries = entries.slice(0, LOG_PREVIEW_LIMIT)
|
const logEntries = entries.slice(0, LOG_PREVIEW_LIMIT)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||||
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
|
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||||
|
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||||
|
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||||
|
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useIsMounted from '../../../shared/hooks/use-is-mounted'
|
||||||
|
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||||
|
|
||||||
|
function PdfSynctexControls() {
|
||||||
|
const ide = useIdeContext()
|
||||||
|
|
||||||
|
const { _id: projectId } = useProjectContext()
|
||||||
|
|
||||||
|
const {
|
||||||
|
clsiServerId,
|
||||||
|
pdfUrl,
|
||||||
|
pdfViewer,
|
||||||
|
position,
|
||||||
|
setHighlights,
|
||||||
|
} = useCompileContext()
|
||||||
|
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(null)
|
||||||
|
|
||||||
|
const isMounted = useIsMounted()
|
||||||
|
|
||||||
|
const { signal } = useAbortController()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = event => setCursorPosition(event.detail)
|
||||||
|
window.addEventListener('cursor:editor:update', listener)
|
||||||
|
return () => window.removeEventListener('cursor:editor:update', listener)
|
||||||
|
}, [ide])
|
||||||
|
|
||||||
|
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
||||||
|
const [syncToCodeInFlight, setSyncToCodeInFlight] = useState(false)
|
||||||
|
|
||||||
|
const [, setSynctexError] = useScopeValue('sync_tex_error')
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getCurrentFilePath = useCallback(() => {
|
||||||
|
const docId = ide.editorManager.getCurrentDocId()
|
||||||
|
const doc = ide.fileTreeManager.findEntityById(docId)
|
||||||
|
|
||||||
|
let path = ide.fileTreeManager.getEntityPath(doc)
|
||||||
|
|
||||||
|
// If the root file is folder/main.tex, then synctex sees the path as folder/./main.tex
|
||||||
|
const rootDocDirname = ide.fileTreeManager.getRootDocDirname()
|
||||||
|
|
||||||
|
if (rootDocDirname) {
|
||||||
|
path = path.replace(RegExp(`^${rootDocDirname}`), `${rootDocDirname}/.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}, [ide])
|
||||||
|
|
||||||
|
const syncToPdf = useCallback(
|
||||||
|
cursorPosition => {
|
||||||
|
setSyncToPdfInFlight(true)
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
file: getCurrentFilePath(),
|
||||||
|
line: cursorPosition.row + 1,
|
||||||
|
column: cursorPosition.column,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (clsiServerId) {
|
||||||
|
params.set('clsiserverid', clsiServerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getJSON(`/project/${projectId}/sync/code?${params}`, { signal })
|
||||||
|
.then(data => {
|
||||||
|
setHighlights(data.pdf)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setSyncToPdfInFlight(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
clsiServerId,
|
||||||
|
projectId,
|
||||||
|
setHighlights,
|
||||||
|
getCurrentFilePath,
|
||||||
|
signal,
|
||||||
|
isMounted,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const syncToCode = useCallback(
|
||||||
|
(position, visualOffset = 0) => {
|
||||||
|
// FIXME: this actually works better if it's halfway across the
|
||||||
|
// page (or the visible part of the page). Synctex doesn't
|
||||||
|
// always find the right place in the file when the point is at
|
||||||
|
// the edge of the page, it sometimes returns the start of the
|
||||||
|
// next paragraph instead.
|
||||||
|
const h = position.offset.left
|
||||||
|
|
||||||
|
// Compute the vertical position to pass to synctex, which
|
||||||
|
// works with coordinates increasing from the top of the page
|
||||||
|
// down. This matches the browser's DOM coordinate of the
|
||||||
|
// click point, but the pdf position is measured from the
|
||||||
|
// bottom of the page so we need to invert it.
|
||||||
|
let v = 0
|
||||||
|
if (position.pageSize?.height) {
|
||||||
|
v += position.pageSize.height - position.offset.top // measure from pdf point (inverted)
|
||||||
|
} else {
|
||||||
|
v += position.offset.top // measure from html click position
|
||||||
|
}
|
||||||
|
v += visualOffset
|
||||||
|
|
||||||
|
setSyncToCodeInFlight(true)
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: position.page + 1,
|
||||||
|
h: h.toFixed(2),
|
||||||
|
v: v.toFixed(2),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (clsiServerId) {
|
||||||
|
params.set('clsiserverid', clsiServerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getJSON(`/project/${projectId}/sync/pdf?${params}`, { signal })
|
||||||
|
.then(data => {
|
||||||
|
const [{ file, line }] = data.code
|
||||||
|
if (file) {
|
||||||
|
const doc = ide.fileTreeManager.findEntityByPath(file)
|
||||||
|
|
||||||
|
ide.editorManager.openDoc(doc, {
|
||||||
|
gotoLine: line,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSynctexError(true)
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setSynctexError(false)
|
||||||
|
}
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setSyncToCodeInFlight(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[clsiServerId, ide, projectId, setSynctexError, signal, isMounted]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = event => syncToCode(event.detail)
|
||||||
|
window.addEventListener('synctex:sync-to-position', listener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('synctex:sync-to-position', listener)
|
||||||
|
}
|
||||||
|
}, [syncToCode])
|
||||||
|
|
||||||
|
if (!pdfUrl || pdfViewer === 'native') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="right"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sync-to-pdf-tooltip">
|
||||||
|
{t('go_to_code_location_in_pdf')}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
bsStyle="default"
|
||||||
|
bsSize="xs"
|
||||||
|
onClick={() => syncToPdf(cursorPosition)}
|
||||||
|
disabled={syncToPdfInFlight}
|
||||||
|
className="synctex-control"
|
||||||
|
aria-label={t('go_to_code_location_in_pdf')}
|
||||||
|
>
|
||||||
|
{syncToPdfInFlight ? (
|
||||||
|
<Icon type="refresh" spin classes={{ icon: 'synctex-spin-icon' }} />
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
type="arrow-right"
|
||||||
|
classes={{ icon: 'synctex-control-icon' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="right"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sync-to-code-tooltip">
|
||||||
|
{t('go_to_pdf_location_in_code')}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
bsStyle="default"
|
||||||
|
bsSize="xs"
|
||||||
|
onClick={() => syncToCode(position, 72)}
|
||||||
|
disabled={syncToCodeInFlight}
|
||||||
|
className="synctex-control"
|
||||||
|
aria-label={t('go_to_pdf_location_in_code')}
|
||||||
|
>
|
||||||
|
{syncToCodeInFlight ? (
|
||||||
|
<Icon type="refresh" spin classes={{ icon: 'synctex-spin-icon' }} />
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
type="arrow-left"
|
||||||
|
classes={{ icon: 'synctex-control-icon' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PdfSynctexControls)
|
|
@ -1,25 +1,12 @@
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
import { lazy, memo } from 'react'
|
||||||
import { lazy, memo, useEffect } from 'react'
|
|
||||||
import { useCompileContext } from '../../../shared/context/compile-context'
|
import { useCompileContext } from '../../../shared/context/compile-context'
|
||||||
|
|
||||||
const PdfJsViewer = lazy(() =>
|
const PdfJsViewer = lazy(() =>
|
||||||
import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
|
import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer')
|
||||||
)
|
)
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
function PdfViewer() {
|
function PdfViewer() {
|
||||||
const [pdfViewer, setPdfViewer] = useScopeValue('settings.pdfViewer')
|
const { pdfUrl, pdfViewer } = useCompileContext()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const viewer = params.get('viewer')
|
|
||||||
|
|
||||||
if (viewer) {
|
|
||||||
setPdfViewer(viewer)
|
|
||||||
}
|
|
||||||
}, [setPdfViewer])
|
|
||||||
|
|
||||||
const { pdfUrl } = useCompileContext()
|
|
||||||
|
|
||||||
if (!pdfUrl) {
|
if (!pdfUrl) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -3,5 +3,10 @@ import { react2angular } from 'react2angular'
|
||||||
|
|
||||||
import PdfPreview from '../components/pdf-preview'
|
import PdfPreview from '../components/pdf-preview'
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
import { rootContext } from '../../../shared/context/root-context'
|
||||||
|
import PdfSynctexControls from '../components/pdf-synctex-controls'
|
||||||
|
|
||||||
App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
|
App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
|
||||||
|
App.component(
|
||||||
|
'pdfSynctexControls',
|
||||||
|
react2angular(rootContext.use(PdfSynctexControls), [])
|
||||||
|
)
|
||||||
|
|
|
@ -160,7 +160,7 @@ export default class PDFJSWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set currentPosition(position) {
|
scrollToPosition(position, scale = null) {
|
||||||
const destArray = [
|
const destArray = [
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
|
@ -168,7 +168,7 @@ export default class PDFJSWrapper {
|
||||||
},
|
},
|
||||||
position.offset.left,
|
position.offset.left,
|
||||||
position.offset.top,
|
position.offset.top,
|
||||||
null,
|
scale,
|
||||||
]
|
]
|
||||||
|
|
||||||
this.viewer.scrollPageIntoView({
|
this.viewer.scrollPageIntoView({
|
||||||
|
|
|
@ -99,7 +99,12 @@ export default CursorPositionManager = class CursorPositionManager {
|
||||||
|
|
||||||
emitCursorUpdateEvent() {
|
emitCursorUpdateEvent() {
|
||||||
const cursor = this.adapter.getCursor()
|
const cursor = this.adapter.getCursor()
|
||||||
return this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(`cursor:${this.$scope.name}:update`, {
|
||||||
|
detail: cursor,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoStoredPosition() {
|
gotoStoredPosition() {
|
||||||
|
|
|
@ -38,15 +38,20 @@ CompileContext.Provider.propTypes = {
|
||||||
fileList: PropTypes.object,
|
fileList: PropTypes.object,
|
||||||
hasChanges: PropTypes.bool.isRequired,
|
hasChanges: PropTypes.bool.isRequired,
|
||||||
hasLintingError: PropTypes.bool,
|
hasLintingError: PropTypes.bool,
|
||||||
|
highlights: PropTypes.arrayOf(PropTypes.object),
|
||||||
logEntries: PropTypes.object,
|
logEntries: PropTypes.object,
|
||||||
logEntryAnnotations: PropTypes.object,
|
logEntryAnnotations: PropTypes.object,
|
||||||
pdfDownloadUrl: PropTypes.string,
|
pdfDownloadUrl: PropTypes.string,
|
||||||
pdfUrl: PropTypes.string,
|
pdfUrl: PropTypes.string,
|
||||||
|
pdfViewer: PropTypes.string,
|
||||||
|
position: PropTypes.object,
|
||||||
rawLog: PropTypes.string,
|
rawLog: PropTypes.string,
|
||||||
setAutoCompile: PropTypes.func.isRequired,
|
setAutoCompile: PropTypes.func.isRequired,
|
||||||
setDraft: PropTypes.func.isRequired,
|
setDraft: PropTypes.func.isRequired,
|
||||||
setError: PropTypes.func.isRequired,
|
setError: PropTypes.func.isRequired,
|
||||||
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
||||||
|
setHighlights: PropTypes.func.isRequired,
|
||||||
|
setPosition: PropTypes.func.isRequired,
|
||||||
setShowLogs: PropTypes.func.isRequired,
|
setShowLogs: PropTypes.func.isRequired,
|
||||||
setStopOnValidationError: PropTypes.func.isRequired,
|
setStopOnValidationError: PropTypes.func.isRequired,
|
||||||
showLogs: PropTypes.bool.isRequired,
|
showLogs: PropTypes.bool.isRequired,
|
||||||
|
@ -70,24 +75,27 @@ export function CompileProvider({ children }) {
|
||||||
const [compiling, setCompiling] = useState(false)
|
const [compiling, setCompiling] = useState(false)
|
||||||
|
|
||||||
// the log entries parsed from the compile output log
|
// the log entries parsed from the compile output log
|
||||||
const [logEntries, setLogEntries] = useScopeValue('pdf.logEntries')
|
const [logEntries, setLogEntries] = useState()
|
||||||
|
|
||||||
// annotations for display in the editor, built from the log entries
|
// annotations for display in the editor, built from the log entries
|
||||||
const [logEntryAnnotations, setLogEntryAnnotations] = useScopeValue(
|
const [logEntryAnnotations, setLogEntryAnnotations] = useScopeValue(
|
||||||
'pdf.logEntryAnnotations'
|
'pdf.logEntryAnnotations'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// the PDF viewer
|
||||||
|
const [pdfViewer] = useScopeValue('settings.pdfViewer')
|
||||||
|
|
||||||
// the URL for downloading the PDF
|
// the URL for downloading the PDF
|
||||||
const [pdfDownloadUrl, setPdfDownloadUrl] = useScopeValue('pdf.downloadUrl')
|
const [pdfDownloadUrl, setPdfDownloadUrl] = useState()
|
||||||
|
|
||||||
// the URL for loading the PDF in the preview pane
|
// the URL for loading the PDF in the preview pane
|
||||||
const [pdfUrl, setPdfUrl] = useScopeValue('pdf.url')
|
const [pdfUrl, setPdfUrl] = useState()
|
||||||
|
|
||||||
// the project is considered to be "uncompiled" if a doc has changed since the last compile started
|
// the project is considered to be "uncompiled" if a doc has changed since the last compile started
|
||||||
const [uncompiled, setUncompiled] = useScopeValue('pdf.uncompiled')
|
const [uncompiled, setUncompiled] = useState()
|
||||||
|
|
||||||
// the id of the CLSI server which ran the compile
|
// the id of the CLSI server which ran the compile
|
||||||
const [clsiServerId, setClsiServerId] = useScopeValue('pdf.clsiServerId')
|
const [clsiServerId, setClsiServerId] = useState()
|
||||||
|
|
||||||
// data received in response to a compile request
|
// data received in response to a compile request
|
||||||
const [data, setData] = useState()
|
const [data, setData] = useState()
|
||||||
|
@ -116,6 +124,12 @@ export function CompileProvider({ children }) {
|
||||||
// validation issues from CLSI
|
// validation issues from CLSI
|
||||||
const [validationIssues, setValidationIssues] = useState()
|
const [validationIssues, setValidationIssues] = useState()
|
||||||
|
|
||||||
|
// areas to highlight on the PDF, from synctex
|
||||||
|
const [highlights, setHighlights] = useState()
|
||||||
|
|
||||||
|
// scroll position of the PDF
|
||||||
|
const [position, setPosition] = usePersistedState(`pdf.position.${projectId}`)
|
||||||
|
|
||||||
// whether autocompile is switched on
|
// whether autocompile is switched on
|
||||||
const [autoCompile, _setAutoCompile] = usePersistedState(
|
const [autoCompile, _setAutoCompile] = usePersistedState(
|
||||||
`autocompile_enabled:${projectId}`,
|
`autocompile_enabled:${projectId}`,
|
||||||
|
@ -331,18 +345,18 @@ export function CompileProvider({ children }) {
|
||||||
|
|
||||||
const codeCheckFailed = stopOnValidationError && autoCompileLintingError
|
const codeCheckFailed = stopOnValidationError && autoCompileLintingError
|
||||||
|
|
||||||
// show that the project has pending changes
|
|
||||||
const hasChanges = Boolean(
|
|
||||||
autoCompile && uncompiled && compiledOnce && !codeCheckFailed
|
|
||||||
)
|
|
||||||
|
|
||||||
// the project is available for auto-compiling
|
// the project is available for auto-compiling
|
||||||
const canAutoCompile = Boolean(autoCompile && !compiling && !codeCheckFailed)
|
const canAutoCompile = Boolean(autoCompile && !codeCheckFailed)
|
||||||
|
|
||||||
|
// show that the project has pending changes
|
||||||
|
const hasChanges = Boolean(canAutoCompile && uncompiled && compiledOnce)
|
||||||
|
|
||||||
// call the debounced autocompile function if the project is available for auto-compiling and it has changed
|
// call the debounced autocompile function if the project is available for auto-compiling and it has changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canAutoCompile && changedAt > 0) {
|
if (canAutoCompile) {
|
||||||
|
if (changedAt > 0) {
|
||||||
compiler.debouncedAutoCompile()
|
compiler.debouncedAutoCompile()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
compiler.debouncedAutoCompile.cancel()
|
compiler.debouncedAutoCompile.cancel()
|
||||||
}
|
}
|
||||||
|
@ -409,10 +423,13 @@ export function CompileProvider({ children }) {
|
||||||
fileList,
|
fileList,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
hasLintingError,
|
hasLintingError,
|
||||||
|
highlights,
|
||||||
logEntries,
|
logEntries,
|
||||||
logEntryAnnotations,
|
logEntryAnnotations,
|
||||||
pdfDownloadUrl,
|
pdfDownloadUrl,
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
|
pdfViewer,
|
||||||
|
position,
|
||||||
rawLog,
|
rawLog,
|
||||||
recompileFromScratch,
|
recompileFromScratch,
|
||||||
setAutoCompile,
|
setAutoCompile,
|
||||||
|
@ -421,6 +438,8 @@ export function CompileProvider({ children }) {
|
||||||
setDraft,
|
setDraft,
|
||||||
setError,
|
setError,
|
||||||
setHasLintingError, // only for stories
|
setHasLintingError, // only for stories
|
||||||
|
setHighlights,
|
||||||
|
setPosition,
|
||||||
setShowLogs,
|
setShowLogs,
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
showLogs,
|
showLogs,
|
||||||
|
@ -443,16 +462,21 @@ export function CompileProvider({ children }) {
|
||||||
fileList,
|
fileList,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
hasLintingError,
|
hasLintingError,
|
||||||
|
highlights,
|
||||||
logEntries,
|
logEntries,
|
||||||
logEntryAnnotations,
|
logEntryAnnotations,
|
||||||
|
position,
|
||||||
pdfDownloadUrl,
|
pdfDownloadUrl,
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
|
pdfViewer,
|
||||||
rawLog,
|
rawLog,
|
||||||
recompileFromScratch,
|
recompileFromScratch,
|
||||||
setAutoCompile,
|
setAutoCompile,
|
||||||
setDraft,
|
setDraft,
|
||||||
setError,
|
setError,
|
||||||
setHasLintingError,
|
setHasLintingError,
|
||||||
|
setHighlights,
|
||||||
|
setPosition,
|
||||||
setStopOnValidationError,
|
setStopOnValidationError,
|
||||||
showLogs,
|
showLogs,
|
||||||
startCompile,
|
startCompile,
|
||||||
|
|
228
services/web/frontend/stories/fixtures/compile.js
Normal file
228
services/web/frontend/stories/fixtures/compile.js
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import examplePdf from './storybook-example.pdf'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
|
export const dispatchDocChanged = () => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const outputFiles = [
|
||||||
|
{
|
||||||
|
path: 'output.pdf',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.pdf',
|
||||||
|
type: 'pdf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'output.bbl',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.bbl',
|
||||||
|
type: 'bbl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'output.bib',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.bib',
|
||||||
|
type: 'bib',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'example.txt',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/example.txt',
|
||||||
|
type: 'txt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'output.log',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.log',
|
||||||
|
type: 'log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'output.blg',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.blg',
|
||||||
|
type: 'blg',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mockCompile = (fetchMock, delay = 1000) =>
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:projectId/compile',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
status: 'success',
|
||||||
|
clsiServerId: 'foo',
|
||||||
|
compileGroup: 'priority',
|
||||||
|
pdfDownloadDomain: '',
|
||||||
|
outputFiles: cloneDeep(outputFiles),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ delay }
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:projectId/compile',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
status,
|
||||||
|
clsiServerId: 'foo',
|
||||||
|
compileGroup: 'priority',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ delay, overwriteRoutes: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mockCompileValidationIssues = (
|
||||||
|
fetchMock,
|
||||||
|
validationProblems,
|
||||||
|
delay = 1000
|
||||||
|
) =>
|
||||||
|
fetchMock.post(
|
||||||
|
'express:/project/:projectId/compile',
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
status: 'validation-problems',
|
||||||
|
validationProblems,
|
||||||
|
clsiServerId: 'foo',
|
||||||
|
compileGroup: 'priority',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ delay, overwriteRoutes: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mockClearCache = fetchMock =>
|
||||||
|
fetchMock.delete('express:/project/:projectId/output', 204, {
|
||||||
|
delay: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockBuildFile = fetchMock =>
|
||||||
|
fetchMock.get(
|
||||||
|
'express:/build/:file',
|
||||||
|
(url, options, request) => {
|
||||||
|
const { pathname } = new URL(url, 'https://example.com')
|
||||||
|
|
||||||
|
switch (pathname) {
|
||||||
|
case '/build/output.blg':
|
||||||
|
return 'This is BibTeX, Version 4.0' // FIXME
|
||||||
|
|
||||||
|
case '/build/output.log':
|
||||||
|
return `
|
||||||
|
The LaTeX compiler output
|
||||||
|
* With a lot of details
|
||||||
|
|
||||||
|
Wrapped in an HTML <pre> element with
|
||||||
|
preformatted text which is to be presented exactly
|
||||||
|
as written in the HTML file
|
||||||
|
|
||||||
|
(whitespace included™)
|
||||||
|
|
||||||
|
The text is typically rendered using a non-proportional ("monospace") font.
|
||||||
|
|
||||||
|
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||||
|
(Font) <7> on input line 18.
|
||||||
|
LaTeX Font Info: External font \`cmex10' loaded for size
|
||||||
|
(Font) <5> on input line 18.
|
||||||
|
! Undefined control sequence.
|
||||||
|
<recently read> \\Zlpha
|
||||||
|
|
||||||
|
main.tex, line 23
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
case '/build/output.pdf':
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
resolve({
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||||
|
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||||
|
},
|
||||||
|
body: xhr.response,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
xhr.open('GET', examplePdf)
|
||||||
|
xhr.responseType = 'arraybuffer'
|
||||||
|
xhr.send()
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(pathname)
|
||||||
|
return 404
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ sendAsJson: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const mockHighlights = [
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 509.999878,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 486.089539,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 498.044708,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 521.955078,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mockEventTracking = fetchMock =>
|
||||||
|
fetchMock.get('express:/event/:event', 204)
|
||||||
|
|
||||||
|
export const mockValidPdf = fetchMock =>
|
||||||
|
fetchMock.get(
|
||||||
|
'express:/build/output.pdf',
|
||||||
|
(url, options, request) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
resolve({
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
||||||
|
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
},
|
||||||
|
body: xhr.response,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
xhr.open('GET', examplePdf)
|
||||||
|
xhr.responseType = 'arraybuffer'
|
||||||
|
xhr.send()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ sendAsJson: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mockSynctex = fetchMock =>
|
||||||
|
fetchMock
|
||||||
|
.get('express:/project/:projectId/sync/code', () => {
|
||||||
|
return { pdf: cloneDeep(mockHighlights) }
|
||||||
|
})
|
||||||
|
.get('express:/project/:projectId/sync/pdf', () => {
|
||||||
|
return { code: [{ file: 'main.tex', line: 100 }] }
|
||||||
|
})
|
|
@ -18,6 +18,9 @@ export function setupContext() {
|
||||||
chatOpen: true,
|
chatOpen: true,
|
||||||
pdfLayout: 'flat',
|
pdfLayout: 'flat',
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
pdfViewer: 'js',
|
||||||
|
},
|
||||||
toggleHistory: () => {},
|
toggleHistory: () => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,9 +32,17 @@ export function setupContext() {
|
||||||
removeListener: sinon.stub(),
|
removeListener: sinon.stub(),
|
||||||
},
|
},
|
||||||
fileTreeManager: {
|
fileTreeManager: {
|
||||||
|
findEntityById: () => null,
|
||||||
findEntityByPath: () => null,
|
findEntityByPath: () => null,
|
||||||
|
getEntityPath: () => null,
|
||||||
getRootDocDirname: () => undefined,
|
getRootDocDirname: () => undefined,
|
||||||
},
|
},
|
||||||
|
editorManager: {
|
||||||
|
getCurrentDocId: () => 'foo',
|
||||||
|
openDoc: (id, options) => {
|
||||||
|
console.log('open doc', id, options)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
window.ExposedSettings = window.ExposedSettings || {}
|
window.ExposedSettings = window.ExposedSettings || {}
|
||||||
window.ExposedSettings.appName = 'Overleaf'
|
window.ExposedSettings.appName = 'Overleaf'
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
import PdfJsViewer from '../js/features/pdf-preview/components/pdf-js-viewer'
|
|
||||||
import useFetchMock from './hooks/use-fetch-mock'
|
|
||||||
import examplePdf from './fixtures/storybook-example.pdf'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { withContextRoot } from './utils/with-context-root'
|
|
||||||
import { setupContext } from './fixtures/context'
|
|
||||||
import useScopeValue from '../js/shared/hooks/use-scope-value'
|
|
||||||
|
|
||||||
setupContext()
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'PDF Viewer',
|
|
||||||
component: PdfJsViewer,
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = {
|
|
||||||
_id: 'story-project',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockHighlights = [
|
|
||||||
{
|
|
||||||
page: 1,
|
|
||||||
h: 85.03936,
|
|
||||||
v: 509.999878,
|
|
||||||
width: 441.921265,
|
|
||||||
height: 8.855677,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
page: 1,
|
|
||||||
h: 85.03936,
|
|
||||||
v: 486.089539,
|
|
||||||
width: 441.921265,
|
|
||||||
height: 8.855677,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
page: 1,
|
|
||||||
h: 85.03936,
|
|
||||||
v: 498.044708,
|
|
||||||
width: 441.921265,
|
|
||||||
height: 8.855677,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
page: 1,
|
|
||||||
h: 85.03936,
|
|
||||||
v: 521.955078,
|
|
||||||
width: 441.921265,
|
|
||||||
height: 8.855677,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Interactive = () => {
|
|
||||||
useFetchMock(fetchMock => {
|
|
||||||
fetchMock.get(
|
|
||||||
'express:/build/output.pdf',
|
|
||||||
(url, options, request) => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const xhr = new XMLHttpRequest()
|
|
||||||
xhr.addEventListener('load', () => {
|
|
||||||
resolve({
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
|
||||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
|
||||||
'Accept-Ranges': 'bytes',
|
|
||||||
},
|
|
||||||
body: xhr.response,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
xhr.open('GET', examplePdf)
|
|
||||||
xhr.responseType = 'arraybuffer'
|
|
||||||
xhr.send()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ sendAsJson: false }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const Inner = () => {
|
|
||||||
const [, setHighlights] = useScopeValue('pdf.highlights')
|
|
||||||
|
|
||||||
const dispatchSyncFromCode = useCallback(() => {
|
|
||||||
setHighlights([])
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setHighlights(mockHighlights)
|
|
||||||
}, 0)
|
|
||||||
}, [setHighlights])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
zIndex: 10,
|
|
||||||
position: 'absolute',
|
|
||||||
top: 20,
|
|
||||||
right: 40,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', gap: 20 }}>
|
|
||||||
<Button onClick={dispatchSyncFromCode}>
|
|
||||||
sync position from editor
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return withContextRoot(
|
|
||||||
<div className="pdf-viewer">
|
|
||||||
<PdfJsViewer url="/build/output.pdf" />
|
|
||||||
<Inner />
|
|
||||||
</div>,
|
|
||||||
{ project }
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { withContextRoot } from './utils/with-context-root'
|
import { withContextRoot } from './utils/with-context-root'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import useFetchMock from './hooks/use-fetch-mock'
|
import useFetchMock from './hooks/use-fetch-mock'
|
||||||
import { setupContext } from './fixtures/context'
|
import { setupContext } from './fixtures/context'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
|
@ -9,10 +9,20 @@ import PdfPreviewToolbar from '../js/features/pdf-preview/components/pdf-preview
|
||||||
import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
|
import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
|
||||||
import { buildFileList } from '../js/features/pdf-preview/util/file-list'
|
import { buildFileList } from '../js/features/pdf-preview/util/file-list'
|
||||||
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
|
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
|
||||||
import examplePdf from './fixtures/storybook-example.pdf'
|
|
||||||
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
|
||||||
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||||
import { useCompileContext } from '../js/shared/context/compile-context'
|
import { useCompileContext } from '../js/shared/context/compile-context'
|
||||||
|
import {
|
||||||
|
dispatchDocChanged,
|
||||||
|
mockBuildFile,
|
||||||
|
mockClearCache,
|
||||||
|
mockCompile,
|
||||||
|
mockCompileError,
|
||||||
|
mockCompileValidationIssues,
|
||||||
|
mockEventTracking,
|
||||||
|
outputFiles,
|
||||||
|
} from './fixtures/compile'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
setupContext()
|
setupContext()
|
||||||
|
|
||||||
|
@ -55,167 +65,6 @@ const scope = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchProjectJoined = () => {
|
|
||||||
window.dispatchEvent(new CustomEvent('project:joined', { detail: project }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispatchDocChanged = () => {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputFiles = [
|
|
||||||
{
|
|
||||||
path: 'output.pdf',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/output.pdf',
|
|
||||||
type: 'pdf',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'output.bbl',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/output.bbl',
|
|
||||||
type: 'bbl',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'output.bib',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/output.bib',
|
|
||||||
type: 'bib',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'example.txt',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/example.txt',
|
|
||||||
type: 'txt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'output.log',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/output.log',
|
|
||||||
type: 'log',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'output.blg',
|
|
||||||
build: '123',
|
|
||||||
url: '/build/output.blg',
|
|
||||||
type: 'blg',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockCompile = (fetchMock, delay = 1000) =>
|
|
||||||
fetchMock.post(
|
|
||||||
'express:/project/:projectId/compile',
|
|
||||||
{
|
|
||||||
body: {
|
|
||||||
status: 'success',
|
|
||||||
clsiServerId: 'foo',
|
|
||||||
compileGroup: 'priority',
|
|
||||||
pdfDownloadDomain: '',
|
|
||||||
outputFiles,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ delay }
|
|
||||||
)
|
|
||||||
|
|
||||||
const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
|
|
||||||
fetchMock.post(
|
|
||||||
'express:/project/:projectId/compile',
|
|
||||||
{
|
|
||||||
body: {
|
|
||||||
status,
|
|
||||||
clsiServerId: 'foo',
|
|
||||||
compileGroup: 'priority',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ delay, overwriteRoutes: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const mockCompileValidationIssues = (
|
|
||||||
fetchMock,
|
|
||||||
validationProblems,
|
|
||||||
delay = 1000
|
|
||||||
) =>
|
|
||||||
fetchMock.post(
|
|
||||||
'express:/project/:projectId/compile',
|
|
||||||
() => {
|
|
||||||
return {
|
|
||||||
body: {
|
|
||||||
status: 'validation-problems',
|
|
||||||
validationProblems,
|
|
||||||
clsiServerId: 'foo',
|
|
||||||
compileGroup: 'priority',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ delay, overwriteRoutes: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const mockClearCache = fetchMock =>
|
|
||||||
fetchMock.delete('express:/project/:projectId/output', 204, {
|
|
||||||
delay: 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockBuildFile = fetchMock =>
|
|
||||||
fetchMock.get(
|
|
||||||
'express:/build/:file',
|
|
||||||
(url, options, request) => {
|
|
||||||
const { pathname } = new URL(url, 'https://example.com')
|
|
||||||
|
|
||||||
switch (pathname) {
|
|
||||||
case '/build/output.blg':
|
|
||||||
return 'This is BibTeX, Version 4.0' // FIXME
|
|
||||||
|
|
||||||
case '/build/output.log':
|
|
||||||
return `
|
|
||||||
The LaTeX compiler output
|
|
||||||
* With a lot of details
|
|
||||||
|
|
||||||
Wrapped in an HTML <pre> element with
|
|
||||||
preformatted text which is to be presented exactly
|
|
||||||
as written in the HTML file
|
|
||||||
|
|
||||||
(whitespace included™)
|
|
||||||
|
|
||||||
The text is typically rendered using a non-proportional ("monospace") font.
|
|
||||||
|
|
||||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
|
||||||
(Font) <7> on input line 18.
|
|
||||||
LaTeX Font Info: External font \`cmex10' loaded for size
|
|
||||||
(Font) <5> on input line 18.
|
|
||||||
! Undefined control sequence.
|
|
||||||
<recently read> \\Zlpha
|
|
||||||
|
|
||||||
main.tex, line 23
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
case '/build/output.pdf':
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const xhr = new XMLHttpRequest()
|
|
||||||
xhr.addEventListener('load', () => {
|
|
||||||
resolve({
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Length': xhr.getResponseHeader('Content-Length'),
|
|
||||||
'Content-Type': xhr.getResponseHeader('Content-Type'),
|
|
||||||
},
|
|
||||||
body: xhr.response,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
xhr.open('GET', examplePdf)
|
|
||||||
xhr.responseType = 'arraybuffer'
|
|
||||||
xhr.send()
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 404
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ sendAsJson: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Interactive = () => {
|
export const Interactive = () => {
|
||||||
useFetchMock(fetchMock => {
|
useFetchMock(fetchMock => {
|
||||||
mockCompile(fetchMock)
|
mockCompile(fetchMock)
|
||||||
|
@ -223,10 +72,6 @@ export const Interactive = () => {
|
||||||
mockClearCache(fetchMock)
|
mockClearCache(fetchMock)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatchProjectJoined()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const Inner = () => {
|
const Inner = () => {
|
||||||
const context = useCompileContext()
|
const context = useCompileContext()
|
||||||
|
|
||||||
|
@ -465,6 +310,10 @@ const compileErrors = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DisplayError = () => {
|
export const DisplayError = () => {
|
||||||
|
useFetchMock(fetchMock => {
|
||||||
|
mockCompile(fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
return withContextRoot(
|
return withContextRoot(
|
||||||
<>
|
<>
|
||||||
{compileErrors.map(error => (
|
{compileErrors.map(error => (
|
||||||
|
@ -482,7 +331,10 @@ export const DisplayError = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
useFetchMock(fetchMock => mockCompile(fetchMock, 500))
|
useFetchMock(fetchMock => {
|
||||||
|
mockCompile(fetchMock, 500)
|
||||||
|
mockBuildFile(fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
return withContextRoot(
|
return withContextRoot(
|
||||||
<div className="pdf">
|
<div className="pdf">
|
||||||
|
@ -496,6 +348,7 @@ export const HybridToolbar = () => {
|
||||||
useFetchMock(fetchMock => {
|
useFetchMock(fetchMock => {
|
||||||
mockCompile(fetchMock, 500)
|
mockCompile(fetchMock, 500)
|
||||||
mockBuildFile(fetchMock)
|
mockBuildFile(fetchMock)
|
||||||
|
mockEventTracking(fetchMock)
|
||||||
})
|
})
|
||||||
|
|
||||||
return withContextRoot(
|
return withContextRoot(
|
||||||
|
@ -508,7 +361,7 @@ export const HybridToolbar = () => {
|
||||||
|
|
||||||
export const FileList = () => {
|
export const FileList = () => {
|
||||||
const fileList = useMemo(() => {
|
const fileList = useMemo(() => {
|
||||||
return buildFileList(outputFiles)
|
return buildFileList(cloneDeep(outputFiles))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -559,9 +412,5 @@ export const ValidationIssues = () => {
|
||||||
mockBuildFile(fetchMock)
|
mockBuildFile(fetchMock)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatchProjectJoined()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return withContextRoot(<PdfPreviewPane />, scope)
|
return withContextRoot(<PdfPreviewPane />, scope)
|
||||||
}
|
}
|
||||||
|
|
62
services/web/frontend/stories/pdf-viewer.stories.js
Normal file
62
services/web/frontend/stories/pdf-viewer.stories.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import useFetchMock from './hooks/use-fetch-mock'
|
||||||
|
import { withContextRoot } from './utils/with-context-root'
|
||||||
|
import { setupContext } from './fixtures/context'
|
||||||
|
import PdfSynctexControls from '../js/features/pdf-preview/components/pdf-synctex-controls'
|
||||||
|
import PdfViewer from '../js/features/pdf-preview/components/pdf-viewer'
|
||||||
|
import {
|
||||||
|
mockBuildFile,
|
||||||
|
mockCompile,
|
||||||
|
mockSynctex,
|
||||||
|
mockValidPdf,
|
||||||
|
} from './fixtures/compile'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
setupContext()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'PDF Viewer',
|
||||||
|
component: PdfViewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = {
|
||||||
|
_id: 'story-project',
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
project,
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: {
|
||||||
|
doc_id: 'test-doc',
|
||||||
|
getSnapshot: () => 'some doc content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Interactive = () => {
|
||||||
|
useFetchMock(fetchMock => {
|
||||||
|
mockCompile(fetchMock)
|
||||||
|
mockBuildFile(fetchMock)
|
||||||
|
mockValidPdf(fetchMock)
|
||||||
|
mockSynctex(fetchMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(`cursor:editor:update`, {
|
||||||
|
detail: { row: 10, position: 10 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return withContextRoot(
|
||||||
|
<div>
|
||||||
|
<div className="pdf-viewer">
|
||||||
|
<PdfViewer />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: 'absolute', top: 150, left: 50 }}>
|
||||||
|
<PdfSynctexControls />
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
scope
|
||||||
|
)
|
||||||
|
}
|
|
@ -24,6 +24,9 @@ export function withContextRoot(Story, scope) {
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
|
$on: (eventName, callback) => {
|
||||||
|
//
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -362,6 +362,10 @@
|
||||||
background-color: fade(@btn-default-bg, 80%);
|
background-color: fade(@btn-default-bg, 80%);
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: fade(@btn-default-bg, 60%);
|
background-color: fade(@btn-default-bg, 60%);
|
||||||
|
|
|
@ -473,7 +473,7 @@ describe('<PdfPreview/>', function () {
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
nock('https://www.test-overleaf.com')
|
nock('https://clsi.test-overleaf.com')
|
||||||
.get(/^\/build\/output.pdf/)
|
.get(/^\/build\/output.pdf/)
|
||||||
.replyWithError({
|
.replyWithError({
|
||||||
message: 'something awful happened',
|
message: 'something awful happened',
|
||||||
|
@ -485,6 +485,8 @@ describe('<PdfPreview/>', function () {
|
||||||
await screen.findByText('Something went wrong while rendering this PDF.')
|
await screen.findByText('Something went wrong while rendering this PDF.')
|
||||||
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
||||||
|
|
||||||
|
expect(nock.isDone()).to.be.true
|
||||||
|
|
||||||
mockValidPdf()
|
mockValidPdf()
|
||||||
|
|
||||||
rerender(<PdfPreview />)
|
rerender(<PdfPreview />)
|
||||||
|
@ -498,7 +500,7 @@ describe('<PdfPreview/>', function () {
|
||||||
mockCompile()
|
mockCompile()
|
||||||
mockBuildFile()
|
mockBuildFile()
|
||||||
|
|
||||||
nock('https://www.test-overleaf.com')
|
nock('https://clsi.test-overleaf.com')
|
||||||
.get(/^\/build\/output.pdf/)
|
.get(/^\/build\/output.pdf/)
|
||||||
.replyWithFile(200, corruptPDF)
|
.replyWithFile(200, corruptPDF)
|
||||||
|
|
||||||
|
@ -507,6 +509,8 @@ describe('<PdfPreview/>', function () {
|
||||||
await screen.findByText('Something went wrong while rendering this PDF.')
|
await screen.findByText('Something went wrong while rendering this PDF.')
|
||||||
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
expect(screen.queryByLabelText('Page 1')).to.not.exist
|
||||||
|
|
||||||
|
expect(nock.isDone()).to.be.true
|
||||||
|
|
||||||
mockValidPdf()
|
mockValidPdf()
|
||||||
|
|
||||||
rerender(<PdfPreview />)
|
rerender(<PdfPreview />)
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
import PdfSynctexControls from '../../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
|
||||||
|
import { EditorProviders } from '../../../helpers/render-with-context'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import { fireEvent, screen, waitFor, render } from '@testing-library/react'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
settings: {
|
||||||
|
syntaxValidation: false,
|
||||||
|
pdfViewer: 'pdfjs',
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
sharejs_doc: {
|
||||||
|
doc_id: 'test-doc',
|
||||||
|
getSnapshot: () => 'some doc content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFiles = [
|
||||||
|
{
|
||||||
|
path: 'output.pdf',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.pdf',
|
||||||
|
type: 'pdf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'output.log',
|
||||||
|
build: '123',
|
||||||
|
url: '/build/output.log',
|
||||||
|
type: 'log',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockCompile = () =>
|
||||||
|
fetchMock.post('express:/project/:projectId/compile', {
|
||||||
|
body: {
|
||||||
|
status: 'success',
|
||||||
|
clsiServerId: 'foo',
|
||||||
|
compileGroup: 'standard',
|
||||||
|
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
|
||||||
|
outputFiles: cloneDeep(outputFiles),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileResponses = {
|
||||||
|
'/build/output.pdf': () => fs.createReadStream(examplePDF),
|
||||||
|
'/build/output.log': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockBuildFile = () =>
|
||||||
|
fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
|
||||||
|
const url = new URL(_url, 'https://clsi.test-overleaf.com')
|
||||||
|
|
||||||
|
if (url.pathname in fileResponses) {
|
||||||
|
return fileResponses[url.pathname]
|
||||||
|
}
|
||||||
|
|
||||||
|
return 404
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockHighlights = [
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 509.999878,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
h: 85.03936,
|
||||||
|
v: 486.089539,
|
||||||
|
width: 441.921265,
|
||||||
|
height: 8.855677,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockSynctex = () =>
|
||||||
|
fetchMock
|
||||||
|
.get('express:/project/:projectId/sync/code', () => {
|
||||||
|
return { pdf: cloneDeep(mockHighlights) }
|
||||||
|
})
|
||||||
|
.get('express:/project/:projectId/sync/pdf', () => {
|
||||||
|
return { code: [{ file: 'main.tex', line: 100 }] }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('<PdfSynctexControls/>', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
window.showNewPdfPreview = true
|
||||||
|
fetchMock.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
window.showNewPdfPreview = undefined
|
||||||
|
fetchMock.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles clicks on sync buttons', async function () {
|
||||||
|
mockCompile()
|
||||||
|
mockSynctex()
|
||||||
|
mockBuildFile()
|
||||||
|
|
||||||
|
const Inner = () => {
|
||||||
|
const { setPosition } = useCompileContext()
|
||||||
|
|
||||||
|
// mock PDF scroll position update
|
||||||
|
useEffect(() => {
|
||||||
|
setPosition({
|
||||||
|
page: 1,
|
||||||
|
offset: { top: 10, left: 10 },
|
||||||
|
pageSize: { height: 500, width: 500 },
|
||||||
|
})
|
||||||
|
}, [setPosition])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditorProviders scope={scope}>
|
||||||
|
<Inner />
|
||||||
|
<PdfSynctexControls />
|
||||||
|
</EditorProviders>
|
||||||
|
)
|
||||||
|
|
||||||
|
const syncToPdfButton = await screen.findByRole('button', {
|
||||||
|
name: 'Go to code location in PDF',
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncToCodeButton = await screen.findByRole('button', {
|
||||||
|
name: 'Go to PDF location in code',
|
||||||
|
})
|
||||||
|
|
||||||
|
// mock editor cursor position update
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new CustomEvent('cursor:editor:update', {
|
||||||
|
detail: { row: 100, column: 10 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(syncToPdfButton)
|
||||||
|
|
||||||
|
expect(syncToPdfButton.disabled).to.be.true
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be
|
||||||
|
.true
|
||||||
|
})
|
||||||
|
|
||||||
|
fireEvent.click(syncToCodeButton)
|
||||||
|
|
||||||
|
expect(syncToCodeButton.disabled).to.be.true
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be
|
||||||
|
.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -48,20 +48,28 @@ export function EditorProviders({
|
||||||
callback(get($scope, path))
|
callback(get($scope, path))
|
||||||
return () => null
|
return () => null
|
||||||
},
|
},
|
||||||
$applyAsync: () => {},
|
$applyAsync: sinon.stub(),
|
||||||
toggleHistory: () => {},
|
toggleHistory: sinon.stub(),
|
||||||
...scope,
|
...scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileTreeManager = {
|
const fileTreeManager = {
|
||||||
|
findEntityById: () => null,
|
||||||
findEntityByPath: () => null,
|
findEntityByPath: () => null,
|
||||||
|
getEntityPath: () => '',
|
||||||
getRootDocDirname: () => '',
|
getRootDocDirname: () => '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editorManager = {
|
||||||
|
getCurrentDocId: () => 'foo',
|
||||||
|
openDoc: sinon.stub(),
|
||||||
|
}
|
||||||
|
|
||||||
window._ide = {
|
window._ide = {
|
||||||
$scope,
|
$scope,
|
||||||
socket,
|
socket,
|
||||||
clsiServerId,
|
clsiServerId,
|
||||||
|
editorManager,
|
||||||
fileTreeManager,
|
fileTreeManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue