mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Remove Ace (#14299)
GitOrigin-RevId: ec8788fdbc8aea73ca33ec2810f4e588fe9476b5
This commit is contained in:
parent
4636f40f03
commit
9875e55a27
72 changed files with 59 additions and 7299 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -14497,12 +14497,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ace-builds": {
|
||||
"version": "1.4.12",
|
||||
"resolved": "git+ssh://git@github.com/overleaf/ace-builds.git#80aa64e7098fead36c15a3f15c6cc6ca5f0e56b1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
|
@ -43692,7 +43686,6 @@
|
|||
"@uppy/utils": "^4.0.7",
|
||||
"@uppy/xhr-upload": "^1.6.8",
|
||||
"abort-controller": "^3.0.0",
|
||||
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
|
||||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"algoliasearch": "^3.35.1",
|
||||
|
@ -52208,7 +52201,6 @@
|
|||
"@xmldom/xmldom": "^0.7.13",
|
||||
"abort-controller": "^3.0.0",
|
||||
"accepts": "^1.3.7",
|
||||
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
|
||||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"algoliasearch": "^3.35.1",
|
||||
|
@ -57685,11 +57677,6 @@
|
|||
"negotiator": "0.6.3"
|
||||
}
|
||||
},
|
||||
"ace-builds": {
|
||||
"version": "git+ssh://git@github.com/overleaf/ace-builds.git#80aa64e7098fead36c15a3f15c6cc6ca5f0e56b1",
|
||||
"dev": true,
|
||||
"from": "ace-builds@overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9"
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
|
|
|
@ -166,7 +166,6 @@
|
|||
"__webpack_public_path__": true,
|
||||
"$": true,
|
||||
"angular": true,
|
||||
"ace": true,
|
||||
"ga": true,
|
||||
// Injected in layout.pug
|
||||
"user_id": true,
|
||||
|
|
|
@ -799,11 +799,6 @@ const ProjectController = {
|
|||
|
||||
const detachRole = req.params.detachRole
|
||||
|
||||
// Allow override via legacy_source_editor=true in query string
|
||||
const showLegacySourceEditor = shouldDisplayFeature(
|
||||
'legacy_source_editor'
|
||||
)
|
||||
|
||||
const showSymbolPalette =
|
||||
!Features.hasFeature('saas') ||
|
||||
(user.features && user.features.symbolPalette)
|
||||
|
@ -896,8 +891,6 @@ const ProjectController = {
|
|||
showTemplatesServerPro,
|
||||
pdfjsVariant: pdfjsAssignment.variant,
|
||||
debugPdfDetach,
|
||||
showLegacySourceEditor,
|
||||
showSourceToolbar: !showLegacySourceEditor,
|
||||
showSymbolPalette,
|
||||
symbolPaletteAvailable: Features.hasFeature('symbol-palette'),
|
||||
detachRole,
|
||||
|
@ -905,7 +898,6 @@ const ProjectController = {
|
|||
showUpgradePrompt,
|
||||
fixedSizeDocument: true,
|
||||
useOpenTelemetry: Settings.useOpenTelemetryClient,
|
||||
showCM6SwitchAwaySurvey: Settings.showCM6SwitchAwaySurvey,
|
||||
isReviewPanelReact: reviewPanelAssignment.variant === 'react',
|
||||
idePageReact,
|
||||
showPersonalAccessToken,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const version = {
|
||||
// Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace
|
||||
ace: '1.4.12',
|
||||
mathjax: '2.7.9',
|
||||
'mathjax-3': '3.2.2',
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
include ./file-view
|
||||
|
||||
.editor-container.full-size(
|
||||
class={"has-source-toolbar" : showSourceToolbar},
|
||||
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
|
||||
vertical-resizable-panes="south-pane-resizer"
|
||||
vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
|
||||
|
@ -44,14 +43,10 @@
|
|||
| #{translate("open_a_file_on_the_left")}
|
||||
|
||||
div(ng-controller="EditorLoaderController")
|
||||
if (!showSourceToolbar)
|
||||
include ./toolbar
|
||||
|
||||
div(ng-if="editor.newSourceEditor")
|
||||
include ../../source-editor/source-editor
|
||||
div(ng-if="!editor.newSourceEditor")
|
||||
include ./source-editor
|
||||
div(ng-if="!editor.newSourceEditor || !reviewPanel.isReact")
|
||||
|
||||
div(ng-if="!reviewPanel.isReact")
|
||||
if !isRestrictedTokenMember
|
||||
include ./review-panel
|
||||
|
||||
|
|
|
@ -12,9 +12,6 @@ div.full-size(
|
|||
custom-toggler-msg-when-open=translate("tooltip_hide_pdf")
|
||||
custom-toggler-msg-when-closed=translate("tooltip_show_pdf")
|
||||
)
|
||||
if (settings.showCM6SwitchAwaySurvey)
|
||||
cm6-switch-away-survey()
|
||||
|
||||
include ./editor-pane
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -24,15 +24,4 @@ aside.editor-sidebar.full-size(
|
|||
|
||||
.outline-container(
|
||||
vertical-resizable-bottom
|
||||
ng-controller="OutlineController"
|
||||
)
|
||||
outline-pane(
|
||||
is-tex-file="isTexFile"
|
||||
outline="outline"
|
||||
project-id="project_id"
|
||||
jump-to-line="jumpToLine"
|
||||
on-toggle="onToggle"
|
||||
event-tracking="eventTracking"
|
||||
highlighted-line="highlightedLine"
|
||||
show="show"
|
||||
)
|
||||
|
|
|
@ -13,16 +13,12 @@ meta(name="ol-wikiEnabled" data-type="boolean" content=settings.proxyLearn)
|
|||
meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl)
|
||||
meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled)
|
||||
meta(name="ol-compilesUserContentDomain" content=settings.compilesUserContentDomain)
|
||||
//- Set base path for Ace scripts loaded on demand/workers and don't use cdn
|
||||
meta(name="ol-aceBasePath" content="/js/" + lib('ace'))
|
||||
//- enable doc hash checking for all projects
|
||||
//- used in public/js/libs/sharejs.js
|
||||
meta(name="ol-useShareJsHash" data-type="boolean" content=true)
|
||||
meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandshake)
|
||||
meta(name="ol-pdfjsVariant" content=pdfjsVariant)
|
||||
meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
|
||||
meta(name="ol-showLegacySourceEditor", data-type="boolean" content=showLegacySourceEditor)
|
||||
meta(name="ol-showSourceToolbar", data-type="boolean" content=showSourceToolbar)
|
||||
meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
|
||||
meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
|
||||
meta(name="ol-detachRole" data-type="string" content=detachRole)
|
||||
|
@ -34,7 +30,6 @@ meta(name="ol-showUpgradePrompt" data-type="boolean" content=showUpgradePrompt)
|
|||
meta(name="ol-useOpenTelemetry" data-type="boolean" content=useOpenTelemetry)
|
||||
meta(name="ol-showSupport", data-type="boolean" content=showSupport)
|
||||
meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro)
|
||||
meta(name="ol-showCM6SwitchAwaySurvey", data-type="boolean" content=showCM6SwitchAwaySurvey)
|
||||
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
|
||||
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
|
||||
meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#review-panel(
|
||||
ng-class="{ 'rp-collapsed-displaying-entry': reviewPanel.entryHover, 'rp-offset-widgets': editor.showVisual }"
|
||||
ng-class="{ 'rp-collapsed-displaying-entry': reviewPanel.entryHover, 'rp-offset-widgets': true }"
|
||||
)
|
||||
.rp-in-editor-widgets
|
||||
a.rp-track-changes-indicator(
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
#editor(
|
||||
ace-editor="editor",
|
||||
ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state",
|
||||
theme="settings.editorTheme",
|
||||
keybindings="settings.mode",
|
||||
font-size="settings.fontSize",
|
||||
auto-complete="settings.autoComplete",
|
||||
auto-pair-delimiters="settings.autoPairDelimiters",
|
||||
spell-check="!anonymous",
|
||||
spell-check-language="project.spellCheckLanguage"
|
||||
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
|
||||
show-print-margin="false",
|
||||
sharejs-doc="editor.sharejs_doc",
|
||||
last-updated="editor.last_updated",
|
||||
cursor-position="editor.cursorPosition",
|
||||
goto-line="editor.gotoLine",
|
||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle,layout:flat-screen:toggle",
|
||||
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
|
||||
read-only="!permissions.write",
|
||||
file-name="editor.open_doc_name",
|
||||
on-ctrl-j="toggleReviewPanel",
|
||||
on-ctrl-shift-c="addNewCommentFromKbdShortcut",
|
||||
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
|
||||
syntax-validation="settings.syntaxValidation",
|
||||
review-panel="reviewPanel",
|
||||
events-bridge="reviewPanelEventsBridge"
|
||||
track-changes= "editor.trackChanges",
|
||||
doc-id="editor.open_doc_id"
|
||||
renderer-data="reviewPanel.rendererData"
|
||||
font-family="settings.fontFamily"
|
||||
line-height="settings.lineHeight"
|
||||
)
|
|
@ -1,43 +0,0 @@
|
|||
.toolbar.toolbar-editor(ng-controller="EditorToolbarController")
|
||||
.toggle-wrapper
|
||||
editor-switch
|
||||
|
||||
div(
|
||||
formatting-buttons
|
||||
ng-cloak
|
||||
ng-if="!editor.showVisual"
|
||||
buttons="editorButtons"
|
||||
opening="editor.opening"
|
||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle"
|
||||
is-fullscreen-editor="ui.view == 'editor' && ui.pdfLayout == 'flat'"
|
||||
class="formatting-buttons"
|
||||
)
|
||||
div(
|
||||
formatting-buttons
|
||||
ng-cloak
|
||||
ng-if="editor.showVisual"
|
||||
buttons="[]"
|
||||
opening="editor.opening"
|
||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle"
|
||||
is-fullscreen-editor="ui.view == 'editor' && ui.pdfLayout == 'flat'"
|
||||
class="formatting-buttons"
|
||||
)
|
||||
|
||||
.toolbar-pdf-right
|
||||
switch-to-pdf-button()
|
||||
detacher-synctex-control()
|
||||
editor-compile-button()
|
||||
|
||||
script(type="text/ng-template", id="formattingButtonsTpl")
|
||||
.formatting-buttons-wrapper
|
||||
|
|
||||
button.btn.formatting-btn.formatting-btn--icon(
|
||||
ng-repeat="button in shownButtons"
|
||||
ng-click="button.handleClick()"
|
||||
ng-class="{ active: button.active }",
|
||||
aria-label="{{button.title}}"
|
||||
tooltip="{{button.title}}"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
i(class="{{button.iconClass}}") {{button.iconText}}
|
|
@ -1,5 +1,27 @@
|
|||
import getMeta from '../../utils/meta'
|
||||
import { IGNORED_MISSPELLINGS } from '../../ide/editor/directives/aceEditor/spell-check/IgnoredMisspellings'
|
||||
|
||||
const IGNORED_MISSPELLINGS = [
|
||||
'Overleaf',
|
||||
'overleaf',
|
||||
'ShareLaTeX',
|
||||
'sharelatex',
|
||||
'LaTeX',
|
||||
'TeX',
|
||||
'BibTeX',
|
||||
'BibLaTeX',
|
||||
'XeTeX',
|
||||
'XeLaTeX',
|
||||
'LuaTeX',
|
||||
'LuaLaTeX',
|
||||
'http',
|
||||
'https',
|
||||
'www',
|
||||
'COVID',
|
||||
'Lockdown',
|
||||
'lockdown',
|
||||
'Coronavirus',
|
||||
'coronavirus',
|
||||
]
|
||||
|
||||
export class IgnoredWords {
|
||||
public learnedWords!: Set<string>
|
||||
|
|
|
@ -3,12 +3,10 @@ import { useTranslation } from 'react-i18next'
|
|||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
|
||||
export default function HelpShowHotkeys() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
|
||||
const { t } = useTranslation()
|
||||
const { features } = useProjectContext()
|
||||
const isMac = /Mac/.test(window.navigator?.platform)
|
||||
|
@ -34,7 +32,6 @@ export default function HelpShowHotkeys() {
|
|||
handleHide={() => setShowModal(false)}
|
||||
isMac={isMac}
|
||||
trackChangesVisible={features?.trackChangesVisible}
|
||||
newSourceEditor={newSourceEditor}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -10,17 +10,11 @@ export default function HotkeysModal({
|
|||
show,
|
||||
isMac = false,
|
||||
trackChangesVisible = false,
|
||||
newSourceEditor = false,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const goToLineSuffix = newSourceEditor ? 'Shift + L' : 'L'
|
||||
const ctrl = isMac ? 'Cmd' : 'Ctrl'
|
||||
|
||||
const modalTitle = newSourceEditor
|
||||
? `${t('hotkeys')} (Source editor)`
|
||||
: `${t('hotkeys')} (Legacy source editor)`
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
bsSize="large"
|
||||
|
@ -29,7 +23,7 @@ export default function HotkeysModal({
|
|||
animation={animation}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{modalTitle}</Modal.Title>
|
||||
<Modal.Title>{t('hotkeys')} (Source editor)</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body className="hotkeys-modal">
|
||||
|
@ -77,7 +71,7 @@ export default function HotkeysModal({
|
|||
</Col>
|
||||
<Col xs={4}>
|
||||
<Hotkey
|
||||
combination={`${ctrl} + ${goToLineSuffix}`}
|
||||
combination={`${ctrl} + Shift + L`}
|
||||
description={t('hotkey_go_to_line')}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -211,7 +205,6 @@ HotkeysModal.propTypes = {
|
|||
show: PropTypes.bool.isRequired,
|
||||
handleHide: PropTypes.func.isRequired,
|
||||
trackChangesVisible: PropTypes.bool,
|
||||
newSourceEditor: PropTypes.bool,
|
||||
}
|
||||
|
||||
function Hotkey({ combination, description }) {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import OutlinePane from '../components/outline-pane'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.controller('OutlineController', [
|
||||
'$scope',
|
||||
'ide',
|
||||
'eventTracking',
|
||||
function ($scope, ide, eventTracking) {
|
||||
$scope.isTexFile = false
|
||||
$scope.outline = []
|
||||
$scope.eventTracking = eventTracking
|
||||
|
||||
function shouldShowOutline() {
|
||||
return !$scope.editor.newSourceEditor
|
||||
}
|
||||
|
||||
$scope.show = shouldShowOutline()
|
||||
|
||||
$scope.$watch('editor.newSourceEditor', function () {
|
||||
$scope.show = shouldShowOutline()
|
||||
})
|
||||
|
||||
$scope.$on('outline-manager:outline-changed', onOutlineChange)
|
||||
|
||||
function onOutlineChange(e, outlineInfo) {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.isTexFile = outlineInfo.isTexFile
|
||||
$scope.outline = outlineInfo.outline
|
||||
$scope.highlightedLine = outlineInfo.highlightedLine
|
||||
})
|
||||
}
|
||||
|
||||
$scope.jumpToLine = (lineNo, syncToPdf) => {
|
||||
ide.outlineManager.jumpToLine(lineNo, syncToPdf)
|
||||
eventTracking.sendMB('outline-jump-to-line')
|
||||
}
|
||||
|
||||
$scope.onToggle = isOpen => {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.$emit('outline-toggled', isOpen)
|
||||
})
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
// Wrap React component as Angular component. Only needed for "top-level" component
|
||||
App.component(
|
||||
'outlinePane',
|
||||
react2angular(rootContext.use(OutlinePane), [
|
||||
'outline',
|
||||
'jumpToLine',
|
||||
'highlightedLine',
|
||||
'eventTracking',
|
||||
'onToggle',
|
||||
'isTexFile',
|
||||
'show',
|
||||
])
|
||||
)
|
|
@ -1,128 +0,0 @@
|
|||
import { isEqual, cloneDeep } from 'lodash'
|
||||
import './controllers/outline-controller'
|
||||
import { matchOutline, nestOutline } from './outline-parser'
|
||||
import isValidTeXFile from '../../main/is-valid-tex-file'
|
||||
|
||||
class OutlineManager {
|
||||
constructor(ide, scope) {
|
||||
this.ide = ide
|
||||
this.scope = scope
|
||||
this.shareJsDoc = null
|
||||
this.isTexFile = false
|
||||
this.flatOutline = []
|
||||
this.previousFlatOutline = []
|
||||
this.outline = []
|
||||
this.highlightedLine = null
|
||||
this.ignoreNextScroll = false
|
||||
this.ignoreNextCursorUpdate = false
|
||||
|
||||
scope.$watch('editor.newSourceEditor', (now, before) => {
|
||||
if (before && !now) {
|
||||
this.updateOutline()
|
||||
this.broadcastChangeEvent()
|
||||
}
|
||||
})
|
||||
|
||||
scope.$on('doc:after-opened', (ev, { isNewDoc }) => {
|
||||
if (isNewDoc) {
|
||||
// if a new doc is opened a cursor updates will be triggered before the
|
||||
// content is loaded. We have to ignore it or the outline highlight
|
||||
// will be incorrect. This doesn't happen when `doc:after-opened` is
|
||||
// fired without a new doc opened.
|
||||
this.ignoreNextCursorUpdate = true
|
||||
}
|
||||
// always ignore the next scroll update so the cursor update takes
|
||||
// precedence
|
||||
this.ignoreNextScroll = true
|
||||
this.shareJsDoc = scope.editor.sharejs_doc
|
||||
this.isTexFile = isValidTeXFile(scope.editor.open_doc_name)
|
||||
this.updateOutline()
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('doc:changed', () => {
|
||||
this.updateOutline()
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('file-view:file-opened', () => {
|
||||
this.isTexFile = false
|
||||
this.updateOutline()
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('cursor:editor:update', (event, cursorPosition) => {
|
||||
if (this.ignoreNextCursorUpdate) {
|
||||
this.ignoreNextCursorUpdate = false
|
||||
return
|
||||
}
|
||||
this.updateHighlightedLine(cursorPosition.row + 1)
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('scroll:editor:update', (event, middleVisibleRow) => {
|
||||
if (this.ignoreNextScroll) {
|
||||
this.ignoreNextScroll = false
|
||||
return
|
||||
}
|
||||
|
||||
this.updateHighlightedLine(middleVisibleRow + 1)
|
||||
})
|
||||
}
|
||||
|
||||
shouldShowOutline() {
|
||||
return !this.scope.editor.newSourceEditor
|
||||
}
|
||||
|
||||
updateOutline() {
|
||||
// Disable if using CM6
|
||||
if (!this.shouldShowOutline()) {
|
||||
return
|
||||
}
|
||||
if (this.isTexFile) {
|
||||
const content = this.ide.editorManager.getCurrentDocValue()
|
||||
if (content) {
|
||||
this.flatOutline = matchOutline(content)
|
||||
if (!isEqual(this.flatOutline, this.previousFlatOutline)) {
|
||||
this.previousFlatOutline = cloneDeep(this.flatOutline)
|
||||
this.outline = nestOutline(this.flatOutline)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
this.previousFlatOutline = []
|
||||
this.outline = []
|
||||
}
|
||||
|
||||
// set highlightedLine to the closest outline line above the editorLine
|
||||
updateHighlightedLine(editorLine) {
|
||||
let closestOutlineLine = null
|
||||
for (let lineId = 0; lineId < this.flatOutline.length; lineId++) {
|
||||
const outline = this.flatOutline[lineId]
|
||||
if (editorLine < outline.line) break // editorLine is above
|
||||
closestOutlineLine = outline.line
|
||||
}
|
||||
if (closestOutlineLine === this.highlightedLine) return
|
||||
this.highlightedLine = closestOutlineLine
|
||||
this.broadcastChangeEvent()
|
||||
}
|
||||
|
||||
jumpToLine(line, syncToPdf) {
|
||||
this.ignoreNextScroll = true
|
||||
this.ide.editorManager.jumpToLine({ gotoLine: line, syncToPdf })
|
||||
}
|
||||
|
||||
broadcastChangeEvent() {
|
||||
// Disable if using CM6
|
||||
if (!this.shouldShowOutline()) {
|
||||
return
|
||||
}
|
||||
this.scope.$broadcast('outline-manager:outline-changed', {
|
||||
isTexFile: this.isTexFile,
|
||||
outline: this.outline,
|
||||
highlightedLine: this.highlightedLine,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default OutlineManager
|
|
@ -1,133 +0,0 @@
|
|||
const COMMAND_LEVELS = {
|
||||
book: 10,
|
||||
part: 20,
|
||||
addpart: 20,
|
||||
chapter: 30,
|
||||
addchap: 30,
|
||||
section: 40,
|
||||
addsec: 40,
|
||||
subsection: 50,
|
||||
subsubsection: 60,
|
||||
paragraph: 70,
|
||||
subparagraph: 80,
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* RegExp matcher parts:
|
||||
*
|
||||
* REGEX_START: begining of line, any number of spaces, double \ (required)
|
||||
* REGEX_COMMAND: any of the listed commands (required)
|
||||
* REGEX_SPACING: spaces and * between groups (optional)
|
||||
* REGEX_SHORT_TITLE: a text between square brackets (optional)
|
||||
* REGEX_TITLE: a text between curly brackets (required)
|
||||
*
|
||||
*/
|
||||
const REGEX_START = '^\\s*\\\\'
|
||||
const REGEX_COMMAND = `(${Object.keys(COMMAND_LEVELS).join('|')})`
|
||||
const REGEX_SPACING = '\\s?\\*?\\s?'
|
||||
const REGEX_SHORT_TITLE = '(\\[([^\\]]+)\\])?'
|
||||
const REGEX_TITLE = '{(.*)}'
|
||||
const MATCHER = new RegExp(
|
||||
`${REGEX_START}${REGEX_COMMAND}${REGEX_SPACING}${REGEX_SHORT_TITLE}${REGEX_SPACING}${REGEX_TITLE}`
|
||||
)
|
||||
|
||||
function matchOutline(content) {
|
||||
const lines = content.split('\n')
|
||||
const flatOutline = matchOutlineFromLines(lines)
|
||||
return flatOutline
|
||||
}
|
||||
|
||||
function matchOutlineFromLines(lines) {
|
||||
const flatOutline = []
|
||||
lines.forEach((line, lineId) => {
|
||||
const match = line.match(MATCHER)
|
||||
if (!match) return
|
||||
const [, command, , shortTitle, title] = match
|
||||
|
||||
flatOutline.push({
|
||||
line: lineId + 1,
|
||||
title: matchDisplayTitle(shortTitle || title),
|
||||
level: COMMAND_LEVELS[command],
|
||||
})
|
||||
})
|
||||
return flatOutline
|
||||
}
|
||||
|
||||
const DISPLAY_TITLE_REGEX = /([^\\]*)\\([^{]+){([^}]+)}(.*)/
|
||||
const END_OF_TITLE_REGEX = /^([^{}]*?({[^{}]*?}[^{}]*?)*)}/
|
||||
/*
|
||||
* Attempt to improve the display of the outline title for titles with commands.
|
||||
* Either skip the command (for labels) or display the command's content instead
|
||||
* of the entire command.
|
||||
*
|
||||
* e.g. "Label \\label{foo} between" => "Label between"
|
||||
* e.g. "TT \\texttt{Bar}" => "TT Bar"
|
||||
*
|
||||
*/
|
||||
function matchDisplayTitle(title) {
|
||||
const closingBracketPosition = title.indexOf('}')
|
||||
|
||||
if (closingBracketPosition < 0) {
|
||||
// simple title (no commands)
|
||||
return title
|
||||
}
|
||||
|
||||
// if there is anything outside the title def on the line, remove it
|
||||
// before proceeding
|
||||
const titleOnlyMatch = title.match(END_OF_TITLE_REGEX)
|
||||
if (titleOnlyMatch) {
|
||||
title = titleOnlyMatch[1]
|
||||
}
|
||||
|
||||
const titleMatch = title.match(DISPLAY_TITLE_REGEX)
|
||||
if (!titleMatch) {
|
||||
// no contained commands; strip everything after the first closing bracket
|
||||
return title.substring(0, closingBracketPosition)
|
||||
}
|
||||
|
||||
const [, textBefore, command, commandContent, textAfter] = titleMatch
|
||||
if (command === 'label') {
|
||||
// label: don't display them at all
|
||||
title = `${textBefore}${textAfter}`
|
||||
} else {
|
||||
// display the content of the command. Works well for formatting commands
|
||||
title = `${textBefore}${commandContent}${textAfter}`
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
function nestOutline(flatOutline) {
|
||||
const parentOutlines = {}
|
||||
const nestedOutlines = []
|
||||
flatOutline.forEach(outline => {
|
||||
const parentOutlineLevels = Object.keys(parentOutlines)
|
||||
|
||||
// find the nearest parent outline
|
||||
const nearestParentLevel = parentOutlineLevels
|
||||
.reverse()
|
||||
.find(level => level < outline.level)
|
||||
const parentOutline = parentOutlines[nearestParentLevel]
|
||||
if (!parentOutline) {
|
||||
// top level
|
||||
nestedOutlines.push(outline)
|
||||
} else if (!parentOutline.children) {
|
||||
// first outline in this node
|
||||
parentOutline.children = [outline]
|
||||
} else {
|
||||
// push outline to node
|
||||
parentOutline.children.push(outline)
|
||||
}
|
||||
|
||||
// store the outline as new parent at its level and forget lower-level
|
||||
// outlines (if any) as they shouldn't get any children anymore
|
||||
parentOutlines[outline.level] = outline
|
||||
parentOutlineLevels
|
||||
.filter(level => level > outline.level)
|
||||
.forEach(level => delete parentOutlines[level])
|
||||
})
|
||||
return nestedOutlines
|
||||
}
|
||||
|
||||
export { matchOutline, matchOutlineFromLines, nestOutline }
|
|
@ -1,93 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import {
|
||||
hasSeenCM6SwitchAwaySurvey,
|
||||
setHasSeenCM6SwitchAwaySurvey,
|
||||
} from '../utils/switch-away-survey'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
|
||||
type CM6SwitchAwaySurveyState = 'disabled' | 'enabled' | 'shown'
|
||||
|
||||
export default function CM6SwitchAwaySurvey() {
|
||||
const [state, setState] = useState<CM6SwitchAwaySurveyState>('disabled')
|
||||
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
|
||||
|
||||
useEffect(() => {
|
||||
// If the user has previously seen any switch-away survey, then don't show
|
||||
// the current one
|
||||
if (hasSeenCM6SwitchAwaySurvey()) return
|
||||
|
||||
if (!newSourceEditor) {
|
||||
setState('enabled')
|
||||
} else {
|
||||
setState('disabled')
|
||||
}
|
||||
}, [newSourceEditor])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = () => {
|
||||
const TIME_FOR_SURVEY_TO_APPEAR = 3000
|
||||
|
||||
setTimeout(() => {
|
||||
if (state === 'enabled') {
|
||||
setState('shown')
|
||||
setHasSeenCM6SwitchAwaySurvey()
|
||||
}
|
||||
}, TIME_FOR_SURVEY_TO_APPEAR)
|
||||
}
|
||||
|
||||
// can't access the ace editor directly, so add the keydown event
|
||||
// to window
|
||||
window?.addEventListener('keydown', handleKeyDown, { once: true })
|
||||
|
||||
return () => window?.removeEventListener('keydown', handleKeyDown)
|
||||
}, [state])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setState('disabled')
|
||||
}, [])
|
||||
|
||||
const handleFollowLink = useCallback(() => {
|
||||
sendMB('cm6-switch-away-survey')
|
||||
setState('disabled')
|
||||
}, [])
|
||||
|
||||
if (state !== 'shown') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="alert alert-success cm6-switch-away-survey" role="alert">
|
||||
<Button
|
||||
className="close"
|
||||
data-dismiss="alert"
|
||||
aria-label="Close"
|
||||
onClick={handleClose}
|
||||
bsStyle={null}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
<div className="warning-content">
|
||||
<div>
|
||||
<div className="warning-text">
|
||||
We noticed that you're still using the{' '}
|
||||
<strong>Source (legacy)</strong> editor.
|
||||
</div>
|
||||
<div className="warning-text">Could you let us know why?</div>
|
||||
</div>
|
||||
<div style={{ display: 'inline-flex' }}>
|
||||
<a
|
||||
href="https://forms.gle/Ygv8gLZ4N8LepQj56"
|
||||
className="btn btn-sm btn-info"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleFollowLink}
|
||||
>
|
||||
Take survey
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -17,7 +17,6 @@ import EditorSwitch from './editor-switch'
|
|||
import SwitchToPDFButton from './switch-to-pdf-button'
|
||||
import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
|
||||
import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { isVisual } from '../extensions/visual/visual'
|
||||
import { language } from '@codemirror/language'
|
||||
import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
|
||||
|
@ -34,8 +33,6 @@ export const CodeMirrorToolbar = () => {
|
|||
}
|
||||
|
||||
const Toolbar = memo(function Toolbar() {
|
||||
const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar')
|
||||
|
||||
const state = useCodeMirrorStateContext()
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
|
@ -117,7 +114,7 @@ const Toolbar = memo(function Toolbar() {
|
|||
|
||||
return (
|
||||
<div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
|
||||
{showSourceToolbar && <EditorSwitch />}
|
||||
<EditorSwitch />
|
||||
{showActions && (
|
||||
<ToolbarItems
|
||||
state={state}
|
||||
|
@ -126,6 +123,7 @@ const Toolbar = memo(function Toolbar() {
|
|||
listDepth={listDepth}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
|
||||
{showActions && (
|
||||
<ToolbarOverflow
|
||||
|
@ -143,8 +141,10 @@ const Toolbar = memo(function Toolbar() {
|
|||
/>
|
||||
</ToolbarOverflow>
|
||||
)}
|
||||
|
||||
<div className="formatting-buttons-wrapper" />
|
||||
</div>
|
||||
|
||||
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
|
||||
<ToolbarButton
|
||||
id="toolbar-toggle-search"
|
||||
|
@ -153,13 +153,10 @@ const Toolbar = memo(function Toolbar() {
|
|||
active={searchPanelOpen(state)}
|
||||
icon="search"
|
||||
/>
|
||||
{showSourceToolbar && (
|
||||
<>
|
||||
|
||||
<SwitchToPDFButton />
|
||||
<DetacherSynctexControl />
|
||||
<DetachCompileButtonWrapper />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="ol-cm-toolbar-button-group hidden">
|
||||
<ToolbarButton
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import isValidTeXFile from '../../../main/is-valid-tex-file'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function Badge() {
|
||||
const content = (
|
||||
<>
|
||||
Overleaf has upgraded the source editor.
|
||||
<br />
|
||||
You can still use the old editor by selecting "Source (legacy)".
|
||||
<br />
|
||||
<br />
|
||||
Click to learn more and give feedback
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
id="editor-switch"
|
||||
description={content}
|
||||
overlayProps={{
|
||||
placement: 'bottom',
|
||||
delayHide: 100,
|
||||
}}
|
||||
tooltipProps={{ className: 'tooltip-wide' }}
|
||||
>
|
||||
<a
|
||||
href="https://forms.gle/GmSs6odZRKRp3VX98"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="info-badge"
|
||||
>
|
||||
<span className="sr-only">{content}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const showLegacySourceEditor: boolean = getMeta('ol-showLegacySourceEditor')
|
||||
|
||||
function EditorSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
|
||||
'editor.newSourceEditor'
|
||||
)
|
||||
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
||||
|
||||
const [docName] = useScopeValue('editor.open_doc_name')
|
||||
const richTextAvailable = isValidTeXFile(docName)
|
||||
// TODO: rename this after legacy & toolbar split tests are complete
|
||||
const richTextOrVisual = richTextAvailable && visual
|
||||
|
||||
const handleChange = useCallback(
|
||||
event => {
|
||||
const editorType = event.target.value
|
||||
|
||||
switch (editorType) {
|
||||
case 'ace':
|
||||
setVisual(false)
|
||||
setNewSourceEditor(false)
|
||||
break
|
||||
|
||||
case 'cm6':
|
||||
setVisual(false)
|
||||
setNewSourceEditor(true)
|
||||
break
|
||||
|
||||
case 'rich-text':
|
||||
setVisual(true)
|
||||
setNewSourceEditor(true)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
sendMB('editor-switch-change', { editorType })
|
||||
},
|
||||
[setVisual, setNewSourceEditor]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="editor-toggle-switch">
|
||||
{showLegacySourceEditor ? <Badge /> : null}
|
||||
|
||||
<fieldset className="toggle-switch">
|
||||
<legend className="sr-only">Editor mode.</legend>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
name="editor"
|
||||
value="cm6"
|
||||
id="editor-switch-cm6"
|
||||
className="toggle-switch-input"
|
||||
checked={!richTextOrVisual && !!newSourceEditor}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
|
||||
<span>{t('code_editor')}</span>
|
||||
</label>
|
||||
|
||||
{showLegacySourceEditor ? (
|
||||
<>
|
||||
<input
|
||||
type="radio"
|
||||
name="editor"
|
||||
value="ace"
|
||||
id="editor-switch-ace"
|
||||
className="toggle-switch-input"
|
||||
checked={!richTextOrVisual && !newSourceEditor}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor="editor-switch-ace" className="toggle-switch-label">
|
||||
<span>Source (legacy)</span>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<RichTextToggle
|
||||
checked={!!richTextOrVisual}
|
||||
disabled={!richTextAvailable}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RichTextToggle: FC<{
|
||||
checked: boolean
|
||||
disabled: boolean
|
||||
handleChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
}> = ({ checked, disabled, handleChange }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const toggle = (
|
||||
<span>
|
||||
<input
|
||||
type="radio"
|
||||
name="editor"
|
||||
value="rich-text"
|
||||
id="editor-switch-rich-text"
|
||||
className="toggle-switch-input"
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<label htmlFor="editor-switch-rich-text" className="toggle-switch-label">
|
||||
<span>{t('visual_editor')}</span>
|
||||
</label>
|
||||
</span>
|
||||
)
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<Tooltip
|
||||
description={t('visual_editor_is_only_available_for_tex_files')}
|
||||
id="rich-text-toggle-tooltip"
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
tooltipProps={{ className: 'tooltip-wide' }}
|
||||
>
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return toggle
|
||||
}
|
||||
|
||||
export default memo(EditorSwitch)
|
|
@ -9,40 +9,28 @@ import { FeedbackBadge } from '@/shared/components/feedback-badge'
|
|||
|
||||
function EditorSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
|
||||
'editor.newSourceEditor'
|
||||
)
|
||||
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
||||
|
||||
const [docName] = useScopeValue('editor.open_doc_name')
|
||||
|
||||
const richTextAvailable = isValidTeXFile(docName)
|
||||
// TODO: rename this after legacy & toolbar split tests are complete
|
||||
const richTextOrVisual = richTextAvailable && visual
|
||||
|
||||
const handleChange = useCallback(
|
||||
event => {
|
||||
const editorType = event.target.value
|
||||
|
||||
switch (editorType) {
|
||||
case 'ace':
|
||||
setVisual(false)
|
||||
setNewSourceEditor(false)
|
||||
break
|
||||
|
||||
case 'cm6':
|
||||
setVisual(false)
|
||||
setNewSourceEditor(true)
|
||||
break
|
||||
|
||||
case 'rich-text':
|
||||
setVisual(true)
|
||||
setNewSourceEditor(true)
|
||||
break
|
||||
}
|
||||
|
||||
sendMB('editor-switch-change', { editorType })
|
||||
},
|
||||
[setVisual, setNewSourceEditor]
|
||||
[setVisual]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -56,7 +44,7 @@ function EditorSwitch() {
|
|||
value="cm6"
|
||||
id="editor-switch-cm6"
|
||||
className="toggle-switch-input"
|
||||
checked={!richTextOrVisual && !!newSourceEditor}
|
||||
checked={!richTextAvailable || !visual}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
|
||||
|
@ -64,13 +52,13 @@ function EditorSwitch() {
|
|||
</label>
|
||||
|
||||
<RichTextToggle
|
||||
checked={!!richTextOrVisual}
|
||||
checked={richTextAvailable && visual}
|
||||
disabled={!richTextAvailable}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{!!richTextOrVisual && (
|
||||
{richTextAvailable && visual && (
|
||||
<FeedbackBadge
|
||||
id="visual-editor-feedback"
|
||||
url="https://forms.gle/AUqHmKNiEH3DRniPA"
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import customLocalStorage from '../../../infrastructure/local-storage'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
export const LegacyEditorWarning = memo(function LegacyEditorWarning({
|
||||
delay,
|
||||
}: {
|
||||
delay: number
|
||||
}) {
|
||||
const [show, setShow] = useState(false)
|
||||
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
|
||||
const hasDismissedLegacyEditor = customLocalStorage.getItem(
|
||||
'editor.has_dismissed_legacy_editor_warning'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const showLegacyEditor =
|
||||
!hasDismissedLegacyEditor && newSourceEditor === false
|
||||
|
||||
let timeoutId: number | undefined
|
||||
|
||||
if (showLegacyEditor) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
eventTracking.sendMB('legacy-editor-warning-prompt')
|
||||
setShow(true)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [hasDismissedLegacyEditor, newSourceEditor, delay])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setShow(false)
|
||||
customLocalStorage.setItem(
|
||||
'editor.has_dismissed_legacy_editor_warning',
|
||||
true
|
||||
)
|
||||
eventTracking.sendMB('legacy-editor-warning-dismiss')
|
||||
}, [])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
eventTracking.sendMB('legacy-editor-warning-link-click')
|
||||
}, [])
|
||||
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="alert alert-info legacy-editor-warning" role="alert">
|
||||
<Button
|
||||
className="close"
|
||||
data-dismiss="alert"
|
||||
aria-label="Close"
|
||||
onClick={handleClose}
|
||||
bsStyle={null}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
<div className="warning-content">
|
||||
<div>We're retiring our Source (legacy) editor in late May 2023.</div>
|
||||
<div>
|
||||
<a
|
||||
className="warning-link"
|
||||
href="https://www.overleaf.com/blog/were-retiring-our-legacy-source-editor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Read the blog post
|
||||
</a>{' '}
|
||||
to learn more and find out how to report any problems.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,9 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import CM6SwitchAwaySurvey from '../components/cm6-switch-away-survey'
|
||||
|
||||
App.component(
|
||||
'cm6SwitchAwaySurvey',
|
||||
react2angular(rootContext.use(CM6SwitchAwaySurvey))
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import EditorSwitch from '../components/editor-switch-legacy'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.component('editorSwitch', react2angular(rootContext.use(EditorSwitch)))
|
|
@ -1,9 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import { LegacyEditorWarning } from '../components/legacy-editor-warning'
|
||||
|
||||
App.component(
|
||||
'legacyEditorWarning',
|
||||
react2angular(rootContext.use(LegacyEditorWarning), ['delay'])
|
||||
)
|
|
@ -15,8 +15,7 @@ import getMeta from '../../../../utils/meta'
|
|||
|
||||
// If a toolbar row sits alongside the review panel, the review panel entries need to be shifted down by 32px.
|
||||
// Once the review panel is always inside the editor, this offset can be removed.
|
||||
const offsetTop =
|
||||
getMeta('ol-showSourceToolbar') && !getMeta('ol-isReviewPanelReact') ? 32 : 0
|
||||
const offsetTop = getMeta('ol-isReviewPanelReact') ? 0 : 32
|
||||
|
||||
// With less than this number of entries, don't bother culling to avoid
|
||||
// little UI jumps when scrolling.
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import localStorage from '../../../infrastructure/local-storage'
|
||||
|
||||
const surveyOneKey = 'editor.has_seen_cm6_switch_away_survey'
|
||||
|
||||
export function setHasSeenCM6SwitchAwaySurvey() {
|
||||
localStorage.setItem(surveyOneKey, true)
|
||||
}
|
||||
|
||||
export function hasSeenCM6SwitchAwaySurvey() {
|
||||
return !!localStorage.getItem(surveyOneKey)
|
||||
}
|
|
@ -28,8 +28,6 @@ import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
|
|||
import ReferencesManager from './ide/references/ReferencesManager'
|
||||
import MetadataManager from './ide/metadata/MetadataManager'
|
||||
import './ide/review-panel/ReviewPanelManager'
|
||||
import OutlineManager from './features/outline/outline-manager'
|
||||
import SafariScrollPatcher from './ide/SafariScrollPatcher'
|
||||
import './ide/cobranding/CobrandingDataService'
|
||||
import './ide/chat/index'
|
||||
import './ide/file-view/index'
|
||||
|
@ -59,14 +57,10 @@ import './shared/context/controllers/root-context-controller'
|
|||
import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
|
||||
import './features/pdf-preview/controllers/pdf-preview-controller'
|
||||
import './features/share-project-modal/controllers/react-share-project-modal-controller'
|
||||
import './features/source-editor/controllers/editor-switch-controller'
|
||||
import './features/source-editor/controllers/cm6-switch-away-survey-controller'
|
||||
import './features/source-editor/controllers/legacy-editor-warning-controller'
|
||||
import './features/history/controllers/history-controller'
|
||||
import './features/editor-left-menu/controllers/editor-left-menu-controller'
|
||||
import { cleanupServiceWorker } from './utils/service-worker-cleanup'
|
||||
import { reportCM6Perf } from './infrastructure/cm6-performance'
|
||||
import { reportAcePerf } from './ide/editor/ace-performance'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
App.controller('IdeController', [
|
||||
|
@ -215,7 +209,6 @@ App.controller('IdeController', [
|
|||
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
||||
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
||||
ide.metadataManager = new MetadataManager(ide, $scope, metadata)
|
||||
ide.outlineManager = new OutlineManager(ide, $scope)
|
||||
|
||||
let inited = false
|
||||
$scope.$on('project:joined', function () {
|
||||
|
@ -301,32 +294,6 @@ If the project has been renamed please look in your project list for a new proje
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (editorType === 'ace') {
|
||||
const acePerfData = reportAcePerf()
|
||||
|
||||
if (acePerfData.numberOfEntries > 0) {
|
||||
const perfProps = [
|
||||
'NumberOfEntries',
|
||||
'MeanKeypressPaint',
|
||||
'Grammarly',
|
||||
'SessionLength',
|
||||
'Memory',
|
||||
'Lags',
|
||||
'NonLags',
|
||||
'LongestLag',
|
||||
'MeanLagsPerMeasure',
|
||||
'MeanKeypressesPerMeasure',
|
||||
'Release',
|
||||
]
|
||||
|
||||
for (const prop of perfProps) {
|
||||
const perfValue =
|
||||
acePerfData[prop.charAt(0).toLowerCase() + prop.slice(1)]
|
||||
if (perfValue !== null) {
|
||||
segmentation['acePerf' + prop] = perfValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segmentation
|
||||
|
@ -374,8 +341,6 @@ If the project has been renamed please look in your project list for a new proje
|
|||
|
||||
ide.localStorage = localStorage
|
||||
|
||||
ide.browserIsSafari = false
|
||||
|
||||
$scope.switchToFlatLayout = function (view) {
|
||||
$scope.ui.pdfLayout = 'flat'
|
||||
$scope.ui.view = view
|
||||
|
@ -418,39 +383,6 @@ If the project has been renamed please look in your project list for a new proje
|
|||
// unused?
|
||||
}
|
||||
|
||||
try {
|
||||
;({ userAgent } = navigator)
|
||||
ide.browserIsSafari =
|
||||
userAgent &&
|
||||
/.*Safari\/.*/.test(userAgent) &&
|
||||
!/.*Chrome\/.*/.test(userAgent) &&
|
||||
!/.*Chromium\/.*/.test(userAgent)
|
||||
} catch (error) {
|
||||
err = error
|
||||
debugConsole.error(err)
|
||||
}
|
||||
|
||||
if (ide.browserIsSafari) {
|
||||
ide.safariScrollPatcher = new SafariScrollPatcher($scope)
|
||||
}
|
||||
|
||||
// Fix Chrome 61 and 62 text-shadow rendering
|
||||
let browserIsChrome61or62 = false
|
||||
try {
|
||||
const chromeVersion =
|
||||
parseFloat(navigator.userAgent.split(' Chrome/')[1]) || null
|
||||
browserIsChrome61or62 = chromeVersion != null
|
||||
if (browserIsChrome61or62) {
|
||||
document.styleSheets[0].insertRule(
|
||||
'.ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; font-weight: bold; }',
|
||||
1
|
||||
)
|
||||
}
|
||||
} catch (error1) {
|
||||
err = error1
|
||||
debugConsole.error(err)
|
||||
}
|
||||
|
||||
// User can append ?ft=somefeature to url to activate a feature toggle
|
||||
ide.featureToggle = __guard__(
|
||||
__guard__(
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let SafariScrollPatcher
|
||||
|
||||
export default SafariScrollPatcher = class SafariScrollPatcher {
|
||||
constructor($scope) {
|
||||
this.isOverAce = false // Flag to control if the pointer is over Ace.
|
||||
this.pdfDiv = null
|
||||
this.aceDiv = null
|
||||
|
||||
// Start listening to PDF wheel events when the pointer leaves the PDF region.
|
||||
// P.S. This is the problem in a nutshell: although the pointer is elsewhere,
|
||||
// wheel events keep being dispatched to the PDF.
|
||||
this.handlePdfDivMouseLeave = () => {
|
||||
return this.pdfDiv.addEventListener('wheel', this.dispatchToAce)
|
||||
}
|
||||
|
||||
// Stop listening to wheel events when the pointer enters the PDF region. If
|
||||
// the pointer is over the PDF, native behaviour is adequate.
|
||||
this.handlePdfDivMouseEnter = () => {
|
||||
return this.pdfDiv.removeEventListener('wheel', this.dispatchToAce)
|
||||
}
|
||||
|
||||
// Set the "pointer over Ace" flag as false, when the mouse leaves its area.
|
||||
this.handleAceDivMouseLeave = () => {
|
||||
return (this.isOverAce = false)
|
||||
}
|
||||
|
||||
// Set the "pointer over Ace" flag as true, when the mouse enters its area.
|
||||
this.handleAceDivMouseEnter = () => {
|
||||
return (this.isOverAce = true)
|
||||
}
|
||||
|
||||
// Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
|
||||
// If elements are already defined, clear existing event listeners and do
|
||||
// the process again (grab elements, set listeners).
|
||||
this.setListeners = () => {
|
||||
this.isOverAce = false
|
||||
|
||||
// If elements aren't null, remove existing listeners.
|
||||
if (this.pdfDiv != null) {
|
||||
this.pdfDiv.removeEventListener(
|
||||
'mouseleave',
|
||||
this.handlePdfDivMouseLeave
|
||||
)
|
||||
this.pdfDiv.removeEventListener(
|
||||
'mouseenter',
|
||||
this.handlePdfDivMouseEnter
|
||||
)
|
||||
}
|
||||
|
||||
if (this.aceDiv != null) {
|
||||
this.aceDiv.removeEventListener(
|
||||
'mouseleave',
|
||||
this.handleAceDivMouseLeave
|
||||
)
|
||||
this.aceDiv.removeEventListener(
|
||||
'mouseenter',
|
||||
this.handleAceDivMouseEnter
|
||||
)
|
||||
}
|
||||
|
||||
// Grab elements.
|
||||
this.pdfDiv = document.querySelector('.pdfjs-viewer') // Grab the PDF div.
|
||||
this.aceDiv = document.querySelector('.ace_content') // Also the editor.
|
||||
|
||||
// Set hover-related listeners.
|
||||
if (this.pdfDiv != null) {
|
||||
this.pdfDiv.addEventListener('mouseleave', this.handlePdfDivMouseLeave)
|
||||
this.pdfDiv.addEventListener('mouseenter', this.handlePdfDivMouseEnter)
|
||||
}
|
||||
if (this.aceDiv != null) {
|
||||
this.aceDiv.addEventListener('mouseleave', this.handleAceDivMouseLeave)
|
||||
this.aceDiv.addEventListener('mouseenter', this.handleAceDivMouseEnter)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for wheel events on the PDF.
|
||||
// If the pointer is over Ace, grab the event, prevent default behaviour
|
||||
// and dispatch it to Ace.
|
||||
this.dispatchToAce = e => {
|
||||
if (this.isOverAce) {
|
||||
// If this is logged, the problem just happened: the event arrived
|
||||
// here (the PDF wheel handler), but it should've gone to Ace.
|
||||
|
||||
// Small timeout - if we dispatch immediately, an exception is thrown.
|
||||
window.setTimeout(() => {
|
||||
// Dispatch the exact same event to Ace (this will keep values
|
||||
// values e.g. `wheelDelta` consistent with user interaction).
|
||||
return this.aceDiv.dispatchEvent(e)
|
||||
}, 1)
|
||||
|
||||
// Avoid scrolling the PDF, as we assume this was intended to the
|
||||
// editor.
|
||||
return e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
// "loaded" event is emitted from the pdfViewer controller $scope. This means
|
||||
// that the previous PDF DOM element was destroyed and a new one is available,
|
||||
// so we need to grab the elements and set the listeners again.
|
||||
$scope.$on('loaded', () => {
|
||||
return this.setListeners()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -85,46 +85,19 @@ export default Document = (function () {
|
|||
this.connected = this.ide.socket.socket.connected
|
||||
this.joined = false
|
||||
this.wantToBeJoined = false
|
||||
this._checkAceConsistency = () => this._checkConsistency(this.ace)
|
||||
this._checkCM6Consistency = () => this._checkConsistency(this.cm6)
|
||||
this._bindToEditorEvents()
|
||||
this._bindToSocketEvents()
|
||||
}
|
||||
|
||||
editorType() {
|
||||
if (this.ace) {
|
||||
return 'ace'
|
||||
} else if (this.cm6) {
|
||||
if (this.cm6) {
|
||||
return 'cm6'
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
attachToAce(ace) {
|
||||
this.ace = ace
|
||||
if (this.doc != null) {
|
||||
this.doc.attachToAce(this.ace)
|
||||
}
|
||||
const editorDoc = this.ace.getSession().getDocument()
|
||||
editorDoc.on('change', this._checkAceConsistency)
|
||||
return this.ide.$scope.$emit('document:opened', this.doc)
|
||||
}
|
||||
|
||||
detachFromAce() {
|
||||
if (this.doc != null) {
|
||||
this.doc.detachFromAce()
|
||||
}
|
||||
const editorDoc =
|
||||
this.ace != null ? this.ace.getSession().getDocument() : undefined
|
||||
if (editorDoc != null) {
|
||||
editorDoc.off('change', this._checkAceConsistency)
|
||||
}
|
||||
delete this.ace
|
||||
this.clearChaosMonkey()
|
||||
return this.ide.$scope.$emit('document:closed', this.doc)
|
||||
}
|
||||
|
||||
attachToCM6(cm6) {
|
||||
this.cm6 = cm6
|
||||
if (this.doc != null) {
|
||||
|
@ -328,9 +301,7 @@ export default Document = (function () {
|
|||
}
|
||||
char = copy[0]
|
||||
copy = copy.slice(1)
|
||||
if (this.ace) {
|
||||
this.ace.session.insert({ row: line, column: pos }, char)
|
||||
} else if (this.cm6) {
|
||||
if (this.cm6) {
|
||||
this.cm6.view.dispatch({
|
||||
changes: {
|
||||
from: Math.min(pos, this.cm6.view.state.doc.length),
|
||||
|
@ -757,7 +728,7 @@ export default Document = (function () {
|
|||
this.ranges.setIdSeed(old_id_seed)
|
||||
}
|
||||
if (remote_op) {
|
||||
// With remote ops, Ace hasn't been updated when we receive this op,
|
||||
// With remote ops, the editor hasn't been updated when we receive this op,
|
||||
// so defer updating track changes until it has
|
||||
return setTimeout(() => this.emit('ranges:dirty'))
|
||||
} else {
|
||||
|
|
|
@ -15,15 +15,12 @@ import _ from 'lodash'
|
|||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import Document from './Document'
|
||||
import './components/spellMenu'
|
||||
import './directives/aceEditor'
|
||||
import './directives/formattingButtons'
|
||||
import './directives/toggleSwitch'
|
||||
import './controllers/SavingNotificationController'
|
||||
import './controllers/CompileButton'
|
||||
import './controllers/SwitchToPDFButton'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { hasSeenCM6SwitchAwaySurvey } from '../../features/source-editor/utils/switch-away-survey'
|
||||
import '../metadata/services/metadata'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
let EditorManager
|
||||
|
@ -48,7 +45,6 @@ export default EditorManager = (function () {
|
|||
wantTrackChanges: false,
|
||||
docTooLongErrorShown: false,
|
||||
showVisual: this.showVisual(),
|
||||
newSourceEditor: this.newSourceEditor(),
|
||||
showSymbolPalette: false,
|
||||
toggleSymbolPalette: () => {
|
||||
const newValue = !this.$scope.editor.showSymbolPalette
|
||||
|
@ -171,35 +167,6 @@ export default EditorManager = (function () {
|
|||
)
|
||||
}
|
||||
|
||||
newSourceEditor() {
|
||||
// Use the new source editor if the legacy editor is disabled
|
||||
if (!getMeta('ol-showLegacySourceEditor')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const storedPrefIsCM6 = () => {
|
||||
const sourceEditor = this.localStorage(
|
||||
`editor.source_editor.${this.$scope.project_id}`
|
||||
)
|
||||
|
||||
return sourceEditor === 'cm6' || sourceEditor == null
|
||||
}
|
||||
|
||||
const showCM6SwitchAwaySurvey = getMeta('ol-showCM6SwitchAwaySurvey')
|
||||
|
||||
if (!showCM6SwitchAwaySurvey) {
|
||||
return storedPrefIsCM6()
|
||||
}
|
||||
|
||||
if (hasSeenCM6SwitchAwaySurvey()) {
|
||||
return storedPrefIsCM6()
|
||||
} else {
|
||||
// force user to switch to cm6 if they haven't seen either of the
|
||||
// switch-away surveys
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
autoOpenDoc() {
|
||||
const open_doc_id =
|
||||
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
|
||||
|
|
|
@ -357,19 +357,6 @@ export default ShareJsDoc = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
attachToAce(ace) {
|
||||
this._attachToEditor('Ace', ace, () => {
|
||||
this._doc.attach_ace(ace, window.maxDocLength)
|
||||
})
|
||||
}
|
||||
|
||||
detachFromAce() {
|
||||
this._maybeDetachEditorWatchdogManager()
|
||||
return typeof this._doc.detach_ace === 'function'
|
||||
? this._doc.detach_ace()
|
||||
: undefined
|
||||
}
|
||||
|
||||
attachToCM6(cm6) {
|
||||
this._attachToEditor('CM6', cm6, () => {
|
||||
cm6.attachShareJs(this._doc, window.maxDocLength)
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
import { round } from 'lodash'
|
||||
import grammarlyExtensionPresent from '../../shared/utils/grammarly'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
const TIMER_DOM_UPDATE_NAME = 'Ace-DomUpdate'
|
||||
const TIMER_MEASURE_NAME = 'Ace-Keypress-Measure'
|
||||
|
||||
const sessionStart = Date.now()
|
||||
|
||||
let performanceOptionsSupport = false
|
||||
|
||||
// Check that performance.mark and performance.measure accept an options object
|
||||
try {
|
||||
const testMarkName = 'featureTestMark'
|
||||
performance.mark(testMarkName, {
|
||||
startTime: performance.now(),
|
||||
detail: { test: 1 },
|
||||
})
|
||||
performance.clearMarks(testMarkName)
|
||||
|
||||
const testMeasureName = 'featureTestMeasure'
|
||||
performance.measure(testMeasureName, {
|
||||
start: performance.now(),
|
||||
detail: { test: 1 },
|
||||
})
|
||||
performance.clearMeasures(testMeasureName)
|
||||
|
||||
performanceOptionsSupport = true
|
||||
} catch (e) {}
|
||||
|
||||
let performanceMemorySupport = false
|
||||
|
||||
function measureMemoryUsage() {
|
||||
// @ts-ignore
|
||||
return performance.memory.usedJSHeapSize
|
||||
}
|
||||
|
||||
try {
|
||||
if ('memory' in window.performance) {
|
||||
measureMemoryUsage()
|
||||
performanceMemorySupport = true
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
let keypressesSinceDomUpdateCount = 0
|
||||
const unpaintedKeypressStartTimes: number[] = []
|
||||
let animationFrameRequest: number | null = null
|
||||
|
||||
function timeInputToRender() {
|
||||
if (!performanceOptionsSupport) return
|
||||
|
||||
++keypressesSinceDomUpdateCount
|
||||
|
||||
unpaintedKeypressStartTimes.push(performance.now())
|
||||
|
||||
if (!animationFrameRequest) {
|
||||
animationFrameRequest = window.requestAnimationFrame(() => {
|
||||
animationFrameRequest = null
|
||||
|
||||
performance.mark(TIMER_DOM_UPDATE_NAME, {
|
||||
detail: { keypressesSinceDomUpdateCount },
|
||||
})
|
||||
keypressesSinceDomUpdateCount = 0
|
||||
|
||||
const keypressEnd = performance.now()
|
||||
|
||||
for (const keypressStart of unpaintedKeypressStartTimes) {
|
||||
performance.measure(TIMER_MEASURE_NAME, {
|
||||
start: keypressStart,
|
||||
end: keypressEnd,
|
||||
})
|
||||
}
|
||||
unpaintedKeypressStartTimes.length = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function initAcePerfListener(textareaEl: HTMLTextAreaElement) {
|
||||
textareaEl?.addEventListener('beforeinput', timeInputToRender)
|
||||
}
|
||||
|
||||
export function tearDownAcePerfListener(textareaEl: HTMLTextAreaElement) {
|
||||
textareaEl?.removeEventListener('beforeinput', timeInputToRender)
|
||||
}
|
||||
|
||||
function calculateMean(durations: number[]) {
|
||||
if (durations.length === 0) return 0
|
||||
|
||||
const sum = durations.reduce((acc, entry) => acc + entry, 0)
|
||||
return sum / durations.length
|
||||
}
|
||||
|
||||
export function reportAcePerf() {
|
||||
const durations = performance
|
||||
.getEntriesByName(TIMER_MEASURE_NAME, 'measure')
|
||||
.map(({ duration }) => duration)
|
||||
|
||||
performance.clearMeasures(TIMER_MEASURE_NAME)
|
||||
|
||||
const meanKeypressPaint = round(calculateMean(durations), 2)
|
||||
|
||||
const grammarly = grammarlyExtensionPresent()
|
||||
const sessionLength = Math.floor((Date.now() - sessionStart) / 1000) // In seconds
|
||||
|
||||
const memory = performanceMemorySupport ? measureMemoryUsage() : null
|
||||
|
||||
// Get entries for keypress counts between DOM updates
|
||||
const domUpdateEntries = performance.getEntriesByName(
|
||||
TIMER_DOM_UPDATE_NAME,
|
||||
'mark'
|
||||
) as PerformanceMark[]
|
||||
|
||||
performance.clearMarks(TIMER_DOM_UPDATE_NAME)
|
||||
|
||||
let lags = 0
|
||||
let nonLags = 0
|
||||
let longestLag = 0
|
||||
let totalKeypressCount = 0
|
||||
|
||||
for (const entry of domUpdateEntries) {
|
||||
const keypressCount = entry.detail.keypressesSinceDomUpdateCount
|
||||
if (keypressCount === 1) {
|
||||
++nonLags
|
||||
} else if (keypressCount > 1) {
|
||||
++lags
|
||||
}
|
||||
if (keypressCount > longestLag) {
|
||||
longestLag = keypressCount
|
||||
}
|
||||
totalKeypressCount += keypressCount
|
||||
}
|
||||
|
||||
const meanLagsPerMeasure = round(lags / (lags + nonLags), 4)
|
||||
const meanKeypressesPerMeasure = round(
|
||||
totalKeypressCount / (lags + nonLags),
|
||||
4
|
||||
)
|
||||
|
||||
const release = getMeta('ol-ExposedSettings')?.sentryRelease || null
|
||||
|
||||
return {
|
||||
numberOfEntries: durations.length,
|
||||
meanKeypressPaint,
|
||||
grammarly,
|
||||
sessionLength,
|
||||
memory,
|
||||
lags,
|
||||
nonLags,
|
||||
longestLag,
|
||||
meanLagsPerMeasure,
|
||||
meanKeypressesPerMeasure,
|
||||
release,
|
||||
}
|
||||
}
|
||||
|
||||
window._reportAcePerf = () => {
|
||||
debugConsole.warn(reportAcePerf())
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.component('spellMenu', {
|
||||
bindings: {
|
||||
open: '<',
|
||||
top: '<',
|
||||
left: '<',
|
||||
layoutFromBottom: '<',
|
||||
highlight: '<',
|
||||
replaceWord: '&',
|
||||
learnWord: '&',
|
||||
},
|
||||
template: `\
|
||||
<div
|
||||
class="dropdown context-menu spell-check-menu"
|
||||
ng-show="$ctrl.open"
|
||||
ng-style="{top: $ctrl.top, left: $ctrl.left}"
|
||||
ng-class="{open: $ctrl.open, 'spell-check-menu-from-bottom': $ctrl.layoutFromBottom}"
|
||||
>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="suggestion in $ctrl.highlight.suggestions | limitTo:8">
|
||||
<button
|
||||
class="btn-link text-left dropdown-menu-button"
|
||||
ng-click="$ctrl.replaceWord({ highlight: $ctrl.highlight, suggestion: suggestion })"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href ng-click="$ctrl.learnWord({ highlight: $ctrl.highlight })">Add to Dictionary</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>\
|
||||
`,
|
||||
})
|
File diff suppressed because it is too large
Load diff
|
@ -1,473 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
import CommandManager from './CommandManager'
|
||||
import EnvironmentManager from './EnvironmentManager'
|
||||
import PackageManager from './PackageManager'
|
||||
import Helpers from './Helpers'
|
||||
import 'ace/ace'
|
||||
import 'ace/ext-language_tools'
|
||||
const { Range } = ace.require('ace/range')
|
||||
const aceSnippetManager = ace.require('ace/snippets').snippetManager
|
||||
|
||||
class AutoCompleteManager {
|
||||
constructor(
|
||||
$scope,
|
||||
editor,
|
||||
element,
|
||||
metadataManager,
|
||||
graphics,
|
||||
preamble,
|
||||
files
|
||||
) {
|
||||
this.$scope = $scope
|
||||
this.editor = editor
|
||||
this.element = element
|
||||
this.metadataManager = metadataManager
|
||||
this.graphics = graphics
|
||||
this.preamble = preamble
|
||||
this.files = files
|
||||
this.monkeyPatchAutocomplete()
|
||||
|
||||
this.$scope.$watch('autoComplete', autocomplete => {
|
||||
if (autocomplete) {
|
||||
this.enable()
|
||||
} else {
|
||||
this.disable()
|
||||
}
|
||||
})
|
||||
|
||||
const onChange = change => {
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
this.editor.on('changeSession', e => {
|
||||
e.oldSession.off('change', onChange)
|
||||
e.session.on('change', onChange)
|
||||
})
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: false,
|
||||
})
|
||||
|
||||
const CommandCompleter = new CommandManager(this.metadataManager)
|
||||
const SnippetCompleter = new EnvironmentManager()
|
||||
const PackageCompleter = new PackageManager(this.metadataManager, Helpers)
|
||||
|
||||
const Graphics = this.graphics
|
||||
const Preamble = this.preamble
|
||||
const Files = this.files
|
||||
|
||||
const GraphicsCompleter = {
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
||||
if (commandFragment) {
|
||||
const match = commandFragment.match(
|
||||
/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/
|
||||
)
|
||||
if (match) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const commandName = match[1]
|
||||
const graphicsPaths = Preamble.getGraphicsPaths()
|
||||
const result = []
|
||||
for (const graphic of Graphics.getGraphicsFiles()) {
|
||||
let { path } = graphic
|
||||
for (const graphicsPath of graphicsPaths) {
|
||||
if (path.indexOf(graphicsPath) === 0) {
|
||||
path = path.slice(graphicsPath.length)
|
||||
break
|
||||
}
|
||||
}
|
||||
result.push({
|
||||
caption: `\\${commandName}{${path}}`,
|
||||
value: `\\${commandName}{${path}}`,
|
||||
meta: 'graphic',
|
||||
score: 50,
|
||||
})
|
||||
}
|
||||
callback(null, result)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const { metadataManager } = this
|
||||
const FilesCompleter = {
|
||||
getCompletions: (editor, session, pos, prefix, callback) => {
|
||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
||||
if (commandFragment) {
|
||||
const match = commandFragment.match(/^\\(input|include){(\w*)/)
|
||||
if (match) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const commandName = match[1]
|
||||
const result = []
|
||||
for (const file of Files.getTeXFiles()) {
|
||||
if (file.id !== this.$scope.docId && !file.deleted && file.path) {
|
||||
const { path } = file
|
||||
const cleanPath = path.replace(/(.+)\.tex$/i, '$1')
|
||||
result.push({
|
||||
caption: `\\${commandName}{${path}}`,
|
||||
value: `\\${commandName}{${cleanPath}}`,
|
||||
meta: 'file',
|
||||
score: 50,
|
||||
})
|
||||
}
|
||||
}
|
||||
callback(null, result)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const LabelsCompleter = {
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
||||
if (commandFragment) {
|
||||
const refMatch = commandFragment.match(
|
||||
/^~?\\([a-zA-Z]*ref){([^}]*, *)?(\w*)/
|
||||
)
|
||||
if (refMatch) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const commandName = refMatch[1]
|
||||
const result = []
|
||||
if (commandName !== 'ref') {
|
||||
// ref is in top 100 commands
|
||||
result.push({
|
||||
caption: `\\${commandName}{}`,
|
||||
snippet: `\\${commandName}{}`,
|
||||
meta: 'cross-reference',
|
||||
score: 60,
|
||||
})
|
||||
}
|
||||
for (const label of metadataManager.getAllLabels()) {
|
||||
result.push({
|
||||
caption: `\\${commandName}{${label}}`,
|
||||
value: `\\${commandName}{${label}}`,
|
||||
meta: 'cross-reference',
|
||||
score: 50,
|
||||
})
|
||||
}
|
||||
callback(null, result)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const references = this.$scope.$root._references
|
||||
const ReferencesCompleter = {
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
||||
if (commandFragment) {
|
||||
const citeMatch = commandFragment.match(
|
||||
/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*, *)?(\w*)/
|
||||
)
|
||||
if (citeMatch) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [_ignore, commandName, previousArgs] = citeMatch
|
||||
if (previousArgs == null) {
|
||||
previousArgs = ''
|
||||
}
|
||||
const previousArgsCaption =
|
||||
previousArgs.length > 8 ? '…,' : previousArgs
|
||||
const result = []
|
||||
result.push({
|
||||
caption: `\\${commandName}{}`,
|
||||
snippet: `\\${commandName}{}`,
|
||||
meta: 'reference',
|
||||
score: 60,
|
||||
})
|
||||
if (references.keys && references.keys.length > 0) {
|
||||
references.keys.forEach(function (key) {
|
||||
if (key != null) {
|
||||
result.push({
|
||||
caption: `\\${commandName}{${previousArgsCaption}${key}}`,
|
||||
value: `\\${commandName}{${previousArgs}${key}}`,
|
||||
meta: 'reference',
|
||||
score: 50,
|
||||
})
|
||||
}
|
||||
})
|
||||
callback(null, result)
|
||||
} else {
|
||||
callback(null, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
this.editor.completers = [
|
||||
CommandCompleter,
|
||||
SnippetCompleter,
|
||||
PackageCompleter,
|
||||
ReferencesCompleter,
|
||||
LabelsCompleter,
|
||||
GraphicsCompleter,
|
||||
FilesCompleter,
|
||||
]
|
||||
}
|
||||
|
||||
disable() {
|
||||
return this.editor.setOptions({
|
||||
enableBasicAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
})
|
||||
}
|
||||
|
||||
onChange(change) {
|
||||
let i
|
||||
const cursorPosition = this.editor.getCursorPosition()
|
||||
const { end } = change
|
||||
const { lineUpToCursor, commandFragment } = Helpers.getContext(
|
||||
this.editor,
|
||||
end
|
||||
)
|
||||
if (
|
||||
(i = lineUpToCursor.indexOf('%')) > -1 &&
|
||||
lineUpToCursor[i - 1] !== '\\'
|
||||
) {
|
||||
return
|
||||
}
|
||||
const lastCharIsBackslash = lineUpToCursor.slice(-1) === '\\'
|
||||
const lastTwoChars = lineUpToCursor.slice(-2)
|
||||
// Don't offer autocomplete on double-backslash, backslash-colon, etc
|
||||
if (/^\\[^a-zA-Z]$/.test(lastTwoChars)) {
|
||||
if (this.editor.completer) {
|
||||
this.editor.completer.detach()
|
||||
}
|
||||
return
|
||||
}
|
||||
// Check that this change was made by us, not a collaborator
|
||||
// (Cursor is still one place behind)
|
||||
// NOTE: this is also the case when a user backspaces over a highlighted
|
||||
// region
|
||||
if (
|
||||
change.origin !== 'remote' &&
|
||||
change.action === 'insert' &&
|
||||
end.row === cursorPosition.row &&
|
||||
end.column === cursorPosition.column + 1
|
||||
) {
|
||||
if (
|
||||
(commandFragment != null ? commandFragment.length : undefined) > 2 ||
|
||||
lastCharIsBackslash
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.editor.execCommand('startAutocomplete')
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
const match = change.lines[0].match(/\\(\w+){}/)
|
||||
if (
|
||||
change.action === 'insert' &&
|
||||
match &&
|
||||
match[1] &&
|
||||
// eslint-disable-next-line max-len
|
||||
/(begin|end|[a-zA-Z]*ref|usepackage|[a-z]*cite[a-z]*|input|include)/.test(
|
||||
match[1]
|
||||
)
|
||||
) {
|
||||
return setTimeout(() => {
|
||||
this.editor.execCommand('startAutocomplete')
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
monkeyPatchAutocomplete() {
|
||||
const { Autocomplete } = ace.require('ace/autocomplete')
|
||||
const Util = ace.require('ace/autocomplete/util')
|
||||
|
||||
if (Autocomplete.prototype._insertMatch == null) {
|
||||
// Only override this once since it's global but we may create multiple
|
||||
// autocomplete handlers
|
||||
Autocomplete.prototype._insertMatch = Autocomplete.prototype.insertMatch
|
||||
Autocomplete.prototype.insertMatch = function (data) {
|
||||
const { editor } = this
|
||||
|
||||
const pos = editor.getCursorPosition()
|
||||
let range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
|
||||
const nextChar = editor.session.getTextRange(range)
|
||||
// If we are in \begin{it|}, then we need to remove the trailing }
|
||||
// since it will be adding in with the autocomplete of \begin{item}...
|
||||
if (
|
||||
/^\\\w+(\[[\w\\,=. ]*\])?{/.test(this.completions.filterText) &&
|
||||
nextChar === '}'
|
||||
) {
|
||||
editor.session.remove(range)
|
||||
}
|
||||
|
||||
// Provide our own `insertMatch` implementation.
|
||||
// See the `insertMatch` method of Autocomplete in
|
||||
// `ext-language_tools.js`.
|
||||
// We need this to account for editing existing commands, particularly
|
||||
// when adding a prefix.
|
||||
// We fix this by detecting when the cursor is in the middle of an
|
||||
// existing command, and adjusting the insertions/deletions
|
||||
// accordingly.
|
||||
// Example:
|
||||
// when changing `\ref{}` to `\href{}`, ace default behaviour
|
||||
// is likely to end up with `\href{}ref{}`
|
||||
if (data == null) {
|
||||
const { completions } = this
|
||||
const { popup } = this
|
||||
data = popup.getData(popup.getRow())
|
||||
data.completer = {
|
||||
insertMatch(editor, matchData) {
|
||||
for (range of editor.selection.getAllRanges()) {
|
||||
const leftRange = _.clone(range)
|
||||
const rightRange = _.clone(range)
|
||||
// trim to left of cursor
|
||||
const lineUpToCursor = editor
|
||||
.getSession()
|
||||
.getTextRange(
|
||||
new Range(
|
||||
range.start.row,
|
||||
0,
|
||||
range.start.row,
|
||||
range.start.column
|
||||
)
|
||||
)
|
||||
// Delete back to command start, as appropriate
|
||||
const commandStartIndex =
|
||||
Helpers.getLastCommandFragmentIndex(lineUpToCursor)
|
||||
if (commandStartIndex !== -1) {
|
||||
leftRange.start.column = commandStartIndex
|
||||
} else {
|
||||
leftRange.start.column -= completions.filterText.length
|
||||
}
|
||||
editor.session.remove(leftRange)
|
||||
// look at text after cursor
|
||||
const lineBeyondCursor = editor
|
||||
.getSession()
|
||||
.getTextRange(
|
||||
new Range(
|
||||
rightRange.start.row,
|
||||
rightRange.start.column,
|
||||
rightRange.end.row,
|
||||
99999
|
||||
)
|
||||
)
|
||||
|
||||
if (lineBeyondCursor) {
|
||||
const partialCommandMatch =
|
||||
lineBeyondCursor.match(/^([a-zA-Z0-9]+)\{/)
|
||||
if (partialCommandMatch) {
|
||||
// We've got a partial command after the cursor
|
||||
const commandTail = partialCommandMatch[1]
|
||||
// remove rest of the partial command, right of cursor
|
||||
rightRange.end.column +=
|
||||
commandTail.length - completions.filterText.length
|
||||
editor.session.remove(rightRange)
|
||||
// trim the completion text to just the command, without
|
||||
// braces or brackets
|
||||
// example: '\cite{}' -> '\cite'
|
||||
if (matchData.snippet != null) {
|
||||
matchData.snippet = matchData.snippet.replace(
|
||||
/[{[].*[}\]]/,
|
||||
''
|
||||
)
|
||||
}
|
||||
if (matchData.caption != null) {
|
||||
matchData.caption = matchData.caption.replace(
|
||||
/[{[].*[}\]]/,
|
||||
''
|
||||
)
|
||||
}
|
||||
if (matchData.value != null) {
|
||||
matchData.value = matchData.value.replace(
|
||||
/[{[].*[}\]]/,
|
||||
''
|
||||
)
|
||||
}
|
||||
}
|
||||
const inArgument = lineBeyondCursor.match(/^([\w._-]+)\}(.*)/)
|
||||
if (inArgument) {
|
||||
const argumentRightOfCursor = inArgument[1]
|
||||
const afterArgument = inArgument[2]
|
||||
if (afterArgument) {
|
||||
rightRange.end.column =
|
||||
rightRange.start.column +
|
||||
argumentRightOfCursor.length +
|
||||
1
|
||||
}
|
||||
editor.session.remove(rightRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
// finally, insert the match
|
||||
if (matchData.snippet) {
|
||||
aceSnippetManager.insertSnippet(editor, matchData.snippet)
|
||||
} else {
|
||||
editor.execCommand('insertstring', matchData.value || matchData)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Autocomplete.prototype._insertMatch.call(this, data)
|
||||
}
|
||||
|
||||
// Overwrite this to set autoInsert = false and set font size
|
||||
Autocomplete.startCommand = {
|
||||
name: 'startAutocomplete',
|
||||
exec: editor => {
|
||||
if (!editor.completer) {
|
||||
editor.completer = new Autocomplete()
|
||||
}
|
||||
editor.completer.autoInsert = false
|
||||
editor.completer.autoSelect = true
|
||||
editor.completer.showPopup(editor)
|
||||
editor.completer.cancelContextMenu()
|
||||
const container = $(
|
||||
editor.completer.popup != null
|
||||
? editor.completer.popup.container
|
||||
: undefined
|
||||
)
|
||||
container.css({ 'font-size': this.$scope.fontSize + 'px' })
|
||||
// Dynamically set width of autocomplete popup
|
||||
const filtered =
|
||||
editor.completer.completions &&
|
||||
editor.completer.completions.filtered
|
||||
if (filtered) {
|
||||
const longestCaption = _.max(filtered.map(c => c.caption.length))
|
||||
const longestMeta = _.max(filtered.map(c => c.meta.length))
|
||||
const charWidth = editor.renderer.characterWidth
|
||||
// between 280 and 700 px
|
||||
const width = Math.max(
|
||||
Math.min(
|
||||
Math.round(
|
||||
longestCaption * charWidth +
|
||||
longestMeta * charWidth +
|
||||
5 * charWidth
|
||||
),
|
||||
700
|
||||
),
|
||||
280
|
||||
)
|
||||
container.css({ width: `${width}px` })
|
||||
}
|
||||
if (filtered.length === 0) {
|
||||
editor.completer.detach()
|
||||
}
|
||||
},
|
||||
bindKey: 'Ctrl-Space|Ctrl-Shift-Space|Alt-Space',
|
||||
}
|
||||
}
|
||||
|
||||
Util.retrievePrecedingIdentifier = function (text, pos, regex) {
|
||||
let currentLineOffset = 0
|
||||
for (let i = pos - 1; i <= 0; i++) {
|
||||
if (text[i] === '\n') {
|
||||
currentLineOffset = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
const currentLine = text.slice(currentLineOffset, pos)
|
||||
const fragment = Helpers.getLastCommandFragment(currentLine) || ''
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AutoCompleteManager
|
|
@ -1,241 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import topHundred from './snippets/TopHundredSnippets'
|
||||
let CommandManager
|
||||
class Parser {
|
||||
static initClass() {
|
||||
// Ignore single letter commands since auto complete is moot then.
|
||||
this.prototype.commandRegex = /\\([a-zA-Z]{2,})/
|
||||
}
|
||||
|
||||
constructor(doc, prefix) {
|
||||
this.doc = doc
|
||||
this.prefix = prefix
|
||||
}
|
||||
|
||||
parse() {
|
||||
// Safari regex is super slow, freezes browser for minutes on end,
|
||||
// hacky solution: limit iterations
|
||||
let command
|
||||
let limit = null
|
||||
if (
|
||||
__guard__(
|
||||
typeof window !== 'undefined' && window !== null
|
||||
? window._ide
|
||||
: undefined,
|
||||
x => x.browserIsSafari
|
||||
)
|
||||
) {
|
||||
limit = 5000
|
||||
}
|
||||
|
||||
// fully formed commands
|
||||
const realCommands = []
|
||||
// commands which match the prefix exactly,
|
||||
// and could be partially typed or malformed
|
||||
const incidentalCommands = []
|
||||
const seen = {}
|
||||
let iterations = 0
|
||||
while ((command = this.nextCommand())) {
|
||||
iterations += 1
|
||||
if (limit && iterations > limit) {
|
||||
return realCommands
|
||||
}
|
||||
|
||||
const docState = this.doc
|
||||
|
||||
let optionalArgs = 0
|
||||
while (this.consumeArgument('[', ']')) {
|
||||
optionalArgs++
|
||||
}
|
||||
|
||||
let args = 0
|
||||
while (this.consumeArgument('{', '}')) {
|
||||
args++
|
||||
}
|
||||
|
||||
const commandHash = `${command}\\${optionalArgs}\\${args}`
|
||||
|
||||
if (this.prefix != null && `\\${command}` === this.prefix) {
|
||||
incidentalCommands.push([command, optionalArgs, args])
|
||||
} else {
|
||||
if (seen[commandHash] == null) {
|
||||
seen[commandHash] = true
|
||||
realCommands.push([command, optionalArgs, args])
|
||||
}
|
||||
}
|
||||
|
||||
// Reset to before argument to handle nested commands
|
||||
this.doc = docState
|
||||
}
|
||||
|
||||
// check incidentals, see if we should pluck out a match
|
||||
if (incidentalCommands.length > 1) {
|
||||
const bestMatch = incidentalCommands.sort(
|
||||
(a, b) => b[1] + b[2] - (a[1] + a[2])
|
||||
)[0]
|
||||
realCommands.push(bestMatch)
|
||||
}
|
||||
|
||||
return realCommands
|
||||
}
|
||||
|
||||
nextCommand() {
|
||||
const i = this.doc.search(this.commandRegex)
|
||||
if (i === -1) {
|
||||
return false
|
||||
} else {
|
||||
const match = this.doc.match(this.commandRegex)[1]
|
||||
this.doc = this.doc.substr(i + match.length + 1)
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
consumeWhitespace() {
|
||||
const match = this.doc.match(/^[ \t\n]*/m)[0]
|
||||
return (this.doc = this.doc.substr(match.length))
|
||||
}
|
||||
|
||||
consumeArgument(openingBracket, closingBracket) {
|
||||
this.consumeWhitespace()
|
||||
|
||||
if (this.doc[0] === openingBracket) {
|
||||
let i = 1
|
||||
let bracketParity = 1
|
||||
while (bracketParity > 0 && i < this.doc.length) {
|
||||
if (this.doc[i] === openingBracket) {
|
||||
bracketParity++
|
||||
} else if (this.doc[i] === closingBracket) {
|
||||
bracketParity--
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if (bracketParity === 0) {
|
||||
this.doc = this.doc.substr(i)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
Parser.initClass()
|
||||
|
||||
export default CommandManager = class CommandManager {
|
||||
constructor(metadataManager) {
|
||||
this.metadataManager = metadataManager
|
||||
}
|
||||
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
const commandNames = {}
|
||||
for (const snippet of Array.from(topHundred)) {
|
||||
commandNames[snippet.caption.match(/\w+/)[0]] = true
|
||||
}
|
||||
|
||||
const packages = this.metadataManager.getAllPackages()
|
||||
const packageCommands = []
|
||||
for (const pkg in packages) {
|
||||
const snippets = packages[pkg]
|
||||
for (const snippet of Array.from(snippets)) {
|
||||
packageCommands.push(snippet)
|
||||
commandNames[snippet.caption.match(/\w+/)[0]] = true
|
||||
}
|
||||
}
|
||||
|
||||
const doc = session.getValue()
|
||||
const parser = new Parser(doc, prefix)
|
||||
const commands = parser.parse()
|
||||
let completions = []
|
||||
for (const command of Array.from(commands)) {
|
||||
if (!commandNames[command[0]]) {
|
||||
let caption = `\\${command[0]}`
|
||||
const score = caption === prefix ? 99 : 50
|
||||
let snippet = caption
|
||||
let i = 1
|
||||
_.times(command[1], function () {
|
||||
snippet += `[\${${i}}]`
|
||||
caption += '[]'
|
||||
return i++
|
||||
})
|
||||
_.times(command[2], function () {
|
||||
snippet += `{\${${i}}}`
|
||||
caption += '{}'
|
||||
return i++
|
||||
})
|
||||
completions.push({
|
||||
caption,
|
||||
snippet,
|
||||
meta: 'cmd',
|
||||
score,
|
||||
})
|
||||
}
|
||||
}
|
||||
completions = completions.concat(topHundred, packageCommands)
|
||||
|
||||
return callback(null, completions)
|
||||
}
|
||||
|
||||
loadCommandsFromDoc(doc) {
|
||||
const parser = new Parser(doc)
|
||||
return (this.commands = parser.parse())
|
||||
}
|
||||
|
||||
getSuggestions(commandFragment) {
|
||||
const matchingCommands = _.filter(
|
||||
this.commands,
|
||||
command => command[0].slice(0, commandFragment.length) === commandFragment
|
||||
)
|
||||
|
||||
return _.map(matchingCommands, function (command) {
|
||||
let completionAfterCursor, completionBeforeCursor
|
||||
const base = `\\${commandFragment}`
|
||||
|
||||
let args = ''
|
||||
_.times(command[1], () => (args = args + '[]'))
|
||||
_.times(command[2], () => (args = args + '{}'))
|
||||
const completionBase = command[0].slice(commandFragment.length)
|
||||
|
||||
const squareArgsNo = command[1]
|
||||
const curlyArgsNo = command[2]
|
||||
const totalArgs = squareArgsNo + curlyArgsNo
|
||||
if (totalArgs === 0) {
|
||||
completionBeforeCursor = completionBase
|
||||
completionAfterCursor = ''
|
||||
} else {
|
||||
completionBeforeCursor = completionBase + args[0]
|
||||
completionAfterCursor = args.slice(1)
|
||||
}
|
||||
|
||||
return {
|
||||
base,
|
||||
completion: completionBase + args,
|
||||
completionBeforeCursor,
|
||||
completionAfterCursor,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import Environments from './snippets/Environments'
|
||||
let staticSnippets = Array.from(Environments.withoutSnippets).map(env => ({
|
||||
caption: `\\begin{${env}}...`,
|
||||
snippet: `\
|
||||
\\begin{${env}}
|
||||
\t$1
|
||||
\\end{${env}}\
|
||||
`,
|
||||
meta: 'env',
|
||||
}))
|
||||
|
||||
staticSnippets = staticSnippets.concat([
|
||||
{
|
||||
caption: '\\begin{array}...',
|
||||
snippet: `\
|
||||
\\begin{array}{\${1:cc}}
|
||||
\t$2 & $3 \\\\\\\\
|
||||
\t$4 & $5
|
||||
\\end{array}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{figure}...',
|
||||
snippet: `\
|
||||
\\begin{figure}
|
||||
\t\\centering
|
||||
\t\\includegraphics{$1}
|
||||
\t\\caption{\${2:Caption}}
|
||||
\t\\label{\${3:fig:my_label}}
|
||||
\\end{figure}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{tabular}...',
|
||||
snippet: `\
|
||||
\\begin{tabular}{\${1:c|c}}
|
||||
\t$2 & $3 \\\\\\\\
|
||||
\t$4 & $5
|
||||
\\end{tabular}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{table}...',
|
||||
snippet: `\
|
||||
\\begin{table}[$1]
|
||||
\t\\centering
|
||||
\t\\begin{tabular}{\${2:c|c}}
|
||||
\t\t$3 & $4 \\\\\\\\
|
||||
\t\t$5 & $6
|
||||
\t\\end{tabular}
|
||||
\t\\caption{\${7:Caption}}
|
||||
\t\\label{\${8:tab:my_label}}
|
||||
\\end{table}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{list}...',
|
||||
snippet: `\
|
||||
\\begin{list}
|
||||
\t\\item $1
|
||||
\\end{list}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{enumerate}...',
|
||||
snippet: `\
|
||||
\\begin{enumerate}
|
||||
\t\\item $1
|
||||
\\end{enumerate}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{itemize}...',
|
||||
snippet: `\
|
||||
\\begin{itemize}
|
||||
\t\\item $1
|
||||
\\end{itemize}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
{
|
||||
caption: '\\begin{frame}...',
|
||||
snippet: `\
|
||||
\\begin{frame}{\${1:Frame Title}}
|
||||
\t$2
|
||||
\\end{frame}\
|
||||
`,
|
||||
meta: 'env',
|
||||
},
|
||||
])
|
||||
|
||||
const documentSnippet = {
|
||||
caption: '\\begin{document}...',
|
||||
snippet: `\
|
||||
\\begin{document}
|
||||
$1
|
||||
\\end{document}\
|
||||
`,
|
||||
meta: 'env',
|
||||
}
|
||||
|
||||
const bibliographySnippet = {
|
||||
caption: '\\begin{thebibliography}...',
|
||||
snippet: `\
|
||||
\\begin{thebibliography}{$1}
|
||||
\\bibitem{$2}
|
||||
$3
|
||||
\\end{thebibliography}\
|
||||
`,
|
||||
meta: 'env',
|
||||
}
|
||||
staticSnippets.push(documentSnippet)
|
||||
|
||||
const parseCustomEnvironments = function (text) {
|
||||
let match
|
||||
const re = /^\\newenvironment{(\w+)}.*$/gm
|
||||
const result = []
|
||||
let iterations = 0
|
||||
while ((match = re.exec(text))) {
|
||||
result.push({ name: match[1], whitespace: null })
|
||||
iterations += 1
|
||||
if (iterations >= 1000) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const parseBeginCommands = function (text) {
|
||||
let match
|
||||
const re = /^([\t ]*)\\begin{(\w+)}.*\n([\t ]*)/gm
|
||||
const result = []
|
||||
let iterations = 0
|
||||
while ((match = re.exec(text))) {
|
||||
const whitespaceAlignment = match[3].replace(match[1] || '', '')
|
||||
if (
|
||||
!Array.from(Environments.all).includes(match[2]) &&
|
||||
match[2] !== 'document'
|
||||
) {
|
||||
result.push({ name: match[2], whitespace: whitespaceAlignment })
|
||||
iterations += 1
|
||||
if (iterations >= 1000) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
re.lastIndex = match.index + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const hasDocumentEnvironment = function (text) {
|
||||
const re = /^\\begin{document}/m
|
||||
return re.exec(text) !== null
|
||||
}
|
||||
|
||||
const hasBibliographyEnvironment = function (text) {
|
||||
const re = /^\\begin{thebibliography}/m
|
||||
return re.exec(text) !== null
|
||||
}
|
||||
|
||||
class EnvironmentManager {
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
let ind
|
||||
const docText = session.getValue()
|
||||
const customEnvironments = parseCustomEnvironments(docText)
|
||||
const beginCommands = parseBeginCommands(docText)
|
||||
if (hasDocumentEnvironment(docText)) {
|
||||
ind = staticSnippets.indexOf(documentSnippet)
|
||||
if (ind !== -1) {
|
||||
staticSnippets.splice(ind, 1)
|
||||
}
|
||||
} else {
|
||||
staticSnippets.push(documentSnippet)
|
||||
}
|
||||
|
||||
if (hasBibliographyEnvironment(docText)) {
|
||||
ind = staticSnippets.indexOf(bibliographySnippet)
|
||||
if (ind !== -1) {
|
||||
staticSnippets.splice(ind, 1)
|
||||
}
|
||||
} else {
|
||||
staticSnippets.push(bibliographySnippet)
|
||||
}
|
||||
|
||||
const parsedItemsMap = {}
|
||||
for (const environment of Array.from(customEnvironments)) {
|
||||
parsedItemsMap[environment.name] = environment
|
||||
}
|
||||
for (const command of Array.from(beginCommands)) {
|
||||
parsedItemsMap[command.name] = command
|
||||
}
|
||||
const parsedItems = _.values(parsedItemsMap)
|
||||
const snippets = staticSnippets
|
||||
.concat(
|
||||
parsedItems.map(item => ({
|
||||
caption: `\\begin{${item.name}}...`,
|
||||
snippet: `\
|
||||
\\begin{${item.name}}
|
||||
${item.whitespace || ''}$0
|
||||
\\end{${item.name}}\
|
||||
`,
|
||||
meta: 'env',
|
||||
}))
|
||||
)
|
||||
.concat(
|
||||
// arguably these `end` commands shouldn't be here, as they're not snippets
|
||||
// but this is where we have access to the `begin` environment names
|
||||
// *shrug*
|
||||
parsedItems.map(item => ({
|
||||
caption: `\\end{${item.name}}`,
|
||||
value: `\\end{${item.name}}`,
|
||||
meta: 'env',
|
||||
}))
|
||||
)
|
||||
return callback(null, snippets)
|
||||
}
|
||||
}
|
||||
|
||||
export default EnvironmentManager
|
|
@ -1,73 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-cond-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import 'ace/ace'
|
||||
import 'ace/ext-language_tools'
|
||||
const { Range } = ace.require('ace/range')
|
||||
|
||||
const Helpers = {
|
||||
getLastCommandFragment(lineUpToCursor) {
|
||||
let index
|
||||
if ((index = Helpers.getLastCommandFragmentIndex(lineUpToCursor)) > -1) {
|
||||
return lineUpToCursor.slice(index)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
getLastCommandFragmentIndex(lineUpToCursor) {
|
||||
// This is hack to let us skip over commands in arguments, and
|
||||
// go to the command on the same 'level' as us. E.g.
|
||||
// \includegraphics[width=\textwidth]{..
|
||||
// should not match the \textwidth.
|
||||
let m
|
||||
const blankArguments = lineUpToCursor.replace(/\[([^\]]*)\]/g, args =>
|
||||
Array(args.length + 1).join('.')
|
||||
)
|
||||
if ((m = blankArguments.match(/(\\[^\\]*)$/))) {
|
||||
return m.index
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
},
|
||||
|
||||
getCommandNameFromFragment(commandFragment) {
|
||||
return __guard__(
|
||||
commandFragment != null ? commandFragment.match(/\\(\w+)\{/) : undefined,
|
||||
x => x[1]
|
||||
)
|
||||
},
|
||||
|
||||
getContext(editor, pos) {
|
||||
const upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
|
||||
const lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
|
||||
const commandFragment = Helpers.getLastCommandFragment(lineUpToCursor)
|
||||
const commandName = Helpers.getCommandNameFromFragment(commandFragment)
|
||||
const beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
|
||||
const lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
|
||||
return {
|
||||
lineUpToCursor,
|
||||
commandFragment,
|
||||
commandName,
|
||||
lineBeyondCursor,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Helpers
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const packages = [
|
||||
'inputenc',
|
||||
'graphicx',
|
||||
'amsmath',
|
||||
'geometry',
|
||||
'amssymb',
|
||||
'hyperref',
|
||||
'babel',
|
||||
'color',
|
||||
'xcolor',
|
||||
'url',
|
||||
'natbib',
|
||||
'fontenc',
|
||||
'fancyhdr',
|
||||
'amsfonts',
|
||||
'booktabs',
|
||||
'amsthm',
|
||||
'float',
|
||||
'tikz',
|
||||
'caption',
|
||||
'setspace',
|
||||
'multirow',
|
||||
'array',
|
||||
'multicol',
|
||||
'titlesec',
|
||||
'enumitem',
|
||||
'ifthen',
|
||||
'listings',
|
||||
'blindtext',
|
||||
'subcaption',
|
||||
'times',
|
||||
'bm',
|
||||
'subfigure',
|
||||
'algorithm',
|
||||
'fontspec',
|
||||
'biblatex',
|
||||
'tabularx',
|
||||
'microtype',
|
||||
'etoolbox',
|
||||
'parskip',
|
||||
'calc',
|
||||
'verbatim',
|
||||
'mathtools',
|
||||
'epsfig',
|
||||
'wrapfig',
|
||||
'lipsum',
|
||||
'cite',
|
||||
'textcomp',
|
||||
'longtable',
|
||||
'textpos',
|
||||
'algpseudocode',
|
||||
'enumerate',
|
||||
'subfig',
|
||||
'pdfpages',
|
||||
'epstopdf',
|
||||
'latexsym',
|
||||
'lmodern',
|
||||
'pifont',
|
||||
'ragged2e',
|
||||
'rotating',
|
||||
'dcolumn',
|
||||
'xltxtra',
|
||||
'marvosym',
|
||||
'indentfirst',
|
||||
'xspace',
|
||||
'csquotes',
|
||||
'xparse',
|
||||
'changepage',
|
||||
'soul',
|
||||
'xunicode',
|
||||
'comment',
|
||||
'mathrsfs',
|
||||
'tocbibind',
|
||||
'lastpage',
|
||||
'algorithm2e',
|
||||
'pgfplots',
|
||||
'lineno',
|
||||
'graphics',
|
||||
'algorithmic',
|
||||
'fullpage',
|
||||
'mathptmx',
|
||||
'todonotes',
|
||||
'ulem',
|
||||
'tweaklist',
|
||||
'moderncvstyleclassic',
|
||||
'collection',
|
||||
'moderncvcompatibility',
|
||||
'gensymb',
|
||||
'helvet',
|
||||
'siunitx',
|
||||
'adjustbox',
|
||||
'placeins',
|
||||
'colortbl',
|
||||
'appendix',
|
||||
'makeidx',
|
||||
'supertabular',
|
||||
'ifpdf',
|
||||
'framed',
|
||||
'aliascnt',
|
||||
'layaureo',
|
||||
'authblk',
|
||||
]
|
||||
|
||||
class PackageManager {
|
||||
constructor(metadataManager) {
|
||||
this.metadataManager = metadataManager
|
||||
}
|
||||
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
const usedPackages = Object.keys(this.metadataManager.getAllPackages())
|
||||
const packageSnippets = []
|
||||
for (const pkg of Array.from(packages)) {
|
||||
if (!Array.from(usedPackages).includes(pkg)) {
|
||||
packageSnippets.push({
|
||||
caption: `\\usepackage{${pkg}}`,
|
||||
snippet: `\\usepackage{${pkg}}`,
|
||||
meta: 'pkg',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
packageSnippets.push({
|
||||
caption: '\\usepackage{}',
|
||||
snippet: '\\usepackage{$1}',
|
||||
meta: 'pkg',
|
||||
score: 70,
|
||||
})
|
||||
return callback(null, packageSnippets)
|
||||
}
|
||||
}
|
||||
|
||||
export default PackageManager
|
|
@ -1,34 +0,0 @@
|
|||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
const envs = [
|
||||
'abstract',
|
||||
'align',
|
||||
'align*',
|
||||
'equation',
|
||||
'equation*',
|
||||
'gather',
|
||||
'gather*',
|
||||
'multline',
|
||||
'multline*',
|
||||
'split',
|
||||
'verbatim',
|
||||
'quote',
|
||||
'center',
|
||||
]
|
||||
|
||||
const envsWithSnippets = [
|
||||
'array',
|
||||
'figure',
|
||||
'tabular',
|
||||
'table',
|
||||
'list',
|
||||
'enumerate',
|
||||
'itemize',
|
||||
'frame',
|
||||
'thebibliography',
|
||||
]
|
||||
|
||||
export default {
|
||||
all: envs.concat(envsWithSnippets),
|
||||
withoutSnippets: envs,
|
||||
}
|
|
@ -1,699 +0,0 @@
|
|||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
caption: '\\begin{}',
|
||||
snippet: '\\begin{$1}',
|
||||
meta: 'env',
|
||||
score: 7.849662248028187,
|
||||
},
|
||||
{
|
||||
caption: '\\end{}',
|
||||
snippet: '\\end{$1}',
|
||||
meta: 'env',
|
||||
score: 7.847906405228455,
|
||||
},
|
||||
{
|
||||
caption: '\\usepackage[]{}',
|
||||
snippet: '\\usepackage[$1]{$2}',
|
||||
meta: 'pkg',
|
||||
score: 5.427890758130527,
|
||||
},
|
||||
{
|
||||
caption: '\\item',
|
||||
snippet: '\\item ',
|
||||
meta: 'cmd',
|
||||
score: 3.800886892251021,
|
||||
},
|
||||
{
|
||||
caption: '\\item[]',
|
||||
snippet: '\\item[$1] ',
|
||||
meta: 'cmd',
|
||||
score: 3.800886892251021,
|
||||
},
|
||||
{
|
||||
caption: '\\section{}',
|
||||
snippet: '\\section{$1}',
|
||||
meta: 'cmd',
|
||||
score: 3.0952612541683835,
|
||||
},
|
||||
{
|
||||
caption: '\\textbf{}',
|
||||
snippet: '\\textbf{$1}',
|
||||
meta: 'cmd',
|
||||
score: 2.627755982816738,
|
||||
},
|
||||
{
|
||||
caption: '\\cite{}',
|
||||
snippet: '\\cite{$1}',
|
||||
meta: 'cmd',
|
||||
score: 2.341195220791228,
|
||||
},
|
||||
{
|
||||
caption: '\\label{}',
|
||||
snippet: '\\label{$1}',
|
||||
meta: 'cmd',
|
||||
score: 1.897791904799601,
|
||||
},
|
||||
{
|
||||
caption: '\\textit{}',
|
||||
snippet: '\\textit{$1}',
|
||||
meta: 'cmd',
|
||||
score: 1.6842996195493385,
|
||||
},
|
||||
{
|
||||
caption: '\\includegraphics[]{}',
|
||||
snippet: '\\includegraphics[$1]{$2}',
|
||||
meta: 'cmd',
|
||||
score: 1.4595731795525781,
|
||||
},
|
||||
{
|
||||
caption: '\\documentclass[]{}',
|
||||
snippet: '\\documentclass[$1]{$2}',
|
||||
meta: 'cmd',
|
||||
score: 1.4425339817971206,
|
||||
},
|
||||
{
|
||||
caption: '\\documentclass{}',
|
||||
snippet: '\\documentclass{$1}',
|
||||
meta: 'cmd',
|
||||
score: 1.4425339817971206,
|
||||
},
|
||||
{
|
||||
caption: '\\ref{}',
|
||||
snippet: '\\ref{$1}',
|
||||
meta: 'cross-reference',
|
||||
score: 0.014379554883991673,
|
||||
},
|
||||
{
|
||||
caption: '\\frac{}{}',
|
||||
snippet: '\\frac{$1}{$2}',
|
||||
meta: 'cmd',
|
||||
score: 1.4341091141105058,
|
||||
},
|
||||
{
|
||||
caption: '\\subsection{}',
|
||||
snippet: '\\subsection{$1}',
|
||||
meta: 'cmd',
|
||||
score: 1.3890912739512353,
|
||||
},
|
||||
{
|
||||
caption: '\\hline',
|
||||
snippet: '\\hline',
|
||||
meta: 'cmd',
|
||||
score: 1.3209538327406387,
|
||||
},
|
||||
{
|
||||
caption: '\\caption{}',
|
||||
snippet: '\\caption{$1}',
|
||||
meta: 'cmd',
|
||||
score: 1.2569477427490174,
|
||||
},
|
||||
{
|
||||
caption: '\\centering',
|
||||
snippet: '\\centering',
|
||||
meta: 'cmd',
|
||||
score: 1.1642881814937829,
|
||||
},
|
||||
{
|
||||
caption: '\\vspace{}',
|
||||
snippet: '\\vspace{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.9533807826673939,
|
||||
},
|
||||
{
|
||||
caption: '\\title{}',
|
||||
snippet: '\\title{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.9202908262245683,
|
||||
},
|
||||
{
|
||||
caption: '\\author{}',
|
||||
snippet: '\\author{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.8973590434087177,
|
||||
},
|
||||
{
|
||||
caption: '\\author[]{}',
|
||||
snippet: '\\author[$1]{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.8973590434087177,
|
||||
},
|
||||
{
|
||||
caption: '\\maketitle',
|
||||
snippet: '\\maketitle',
|
||||
meta: 'cmd',
|
||||
score: 0.7504160124360846,
|
||||
},
|
||||
{
|
||||
caption: '\\textwidth',
|
||||
snippet: '\\textwidth',
|
||||
meta: 'cmd',
|
||||
score: 0.7355328080889112,
|
||||
},
|
||||
{
|
||||
caption: '\\newcommand{}{}',
|
||||
snippet: '\\newcommand{$1}{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.7264891987129375,
|
||||
},
|
||||
{
|
||||
caption: '\\newcommand{}[]{}',
|
||||
snippet: '\\newcommand{$1}[$2]{$3}',
|
||||
meta: 'cmd',
|
||||
score: 0.7264891987129375,
|
||||
},
|
||||
{
|
||||
caption: '\\date{}',
|
||||
snippet: '\\date{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.7225518453076786,
|
||||
},
|
||||
{
|
||||
caption: '\\emph{}',
|
||||
snippet: '\\emph{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.7060308784832261,
|
||||
},
|
||||
{
|
||||
caption: '\\textsc{}',
|
||||
snippet: '\\textsc{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.6926466355384758,
|
||||
},
|
||||
{
|
||||
caption: '\\multicolumn{}{}{}',
|
||||
snippet: '\\multicolumn{$1}{$2}{$3}',
|
||||
meta: 'cmd',
|
||||
score: 0.5473606021405326,
|
||||
},
|
||||
{
|
||||
caption: '\\input{}',
|
||||
snippet: '\\input{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.4966021927742672,
|
||||
},
|
||||
{
|
||||
caption: '\\alpha',
|
||||
snippet: '\\alpha',
|
||||
meta: 'cmd',
|
||||
score: 0.49520006391384913,
|
||||
},
|
||||
{
|
||||
caption: '\\in',
|
||||
snippet: '\\in',
|
||||
meta: 'cmd',
|
||||
score: 0.4716039670146658,
|
||||
},
|
||||
{
|
||||
caption: '\\mathbf{}',
|
||||
snippet: '\\mathbf{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.4682018419466319,
|
||||
},
|
||||
{
|
||||
caption: '\\right',
|
||||
snippet: '\\right',
|
||||
meta: 'cmd',
|
||||
score: 0.4299239459457309,
|
||||
},
|
||||
{
|
||||
caption: '\\left',
|
||||
snippet: '\\left',
|
||||
meta: 'cmd',
|
||||
score: 0.42937815279867964,
|
||||
},
|
||||
{
|
||||
caption: '\\sum',
|
||||
snippet: '\\sum',
|
||||
meta: 'cmd',
|
||||
score: 0.42607994509619934,
|
||||
},
|
||||
{
|
||||
caption: '\\chapter{}',
|
||||
snippet: '\\chapter{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.422097569591803,
|
||||
},
|
||||
{
|
||||
caption: '\\par',
|
||||
snippet: '\\par',
|
||||
meta: 'cmd',
|
||||
score: 0.413853376001159,
|
||||
},
|
||||
{
|
||||
caption: '\\lambda',
|
||||
snippet: '\\lambda',
|
||||
meta: 'cmd',
|
||||
score: 0.39389600578684125,
|
||||
},
|
||||
{
|
||||
caption: '\\subsubsection{}',
|
||||
snippet: '\\subsubsection{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.3727781330132016,
|
||||
},
|
||||
{
|
||||
caption: '\\bibitem{}',
|
||||
snippet: '\\bibitem{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.3689547570562042,
|
||||
},
|
||||
{
|
||||
caption: '\\bibitem[]{}',
|
||||
snippet: '\\bibitem[$1]{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.3689547570562042,
|
||||
},
|
||||
{
|
||||
caption: '\\text{}',
|
||||
snippet: '\\text{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.3608680734736821,
|
||||
},
|
||||
{
|
||||
caption: '\\setlength{}{}',
|
||||
snippet: '\\setlength{$1}{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.354445763583904,
|
||||
},
|
||||
{
|
||||
caption: '\\mathcal{}',
|
||||
snippet: '\\mathcal{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.35084018920966636,
|
||||
},
|
||||
{
|
||||
caption: '\\newpage',
|
||||
snippet: '\\newpage',
|
||||
meta: 'cmd',
|
||||
score: 0.3277033727934986,
|
||||
},
|
||||
{
|
||||
caption: '\\renewcommand{}{}',
|
||||
snippet: '\\renewcommand{$1}{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.3267437011085663,
|
||||
},
|
||||
{
|
||||
caption: '\\theta',
|
||||
snippet: '\\theta',
|
||||
meta: 'cmd',
|
||||
score: 0.3210417159232142,
|
||||
},
|
||||
{
|
||||
caption: '\\hspace{}',
|
||||
snippet: '\\hspace{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.3147206476372336,
|
||||
},
|
||||
{
|
||||
caption: '\\beta',
|
||||
snippet: '\\beta',
|
||||
meta: 'cmd',
|
||||
score: 0.3061799530337638,
|
||||
},
|
||||
{
|
||||
caption: '\\texttt{}',
|
||||
snippet: '\\texttt{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.3019066753744355,
|
||||
},
|
||||
{
|
||||
caption: '\\times',
|
||||
snippet: '\\times',
|
||||
meta: 'cmd',
|
||||
score: 0.2957960629411553,
|
||||
},
|
||||
{
|
||||
caption: '\\color{}',
|
||||
snippet: '\\color{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.2864294797053033,
|
||||
},
|
||||
{
|
||||
caption: '\\mu',
|
||||
snippet: '\\mu',
|
||||
meta: 'cmd',
|
||||
score: 0.27635652476799255,
|
||||
},
|
||||
{
|
||||
caption: '\\bibliography{}',
|
||||
snippet: '\\bibliography{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.2659628337907604,
|
||||
},
|
||||
{
|
||||
caption: '\\linewidth',
|
||||
snippet: '\\linewidth',
|
||||
meta: 'cmd',
|
||||
score: 0.2639498312518439,
|
||||
},
|
||||
{
|
||||
caption: '\\delta',
|
||||
snippet: '\\delta',
|
||||
meta: 'cmd',
|
||||
score: 0.2620578600722735,
|
||||
},
|
||||
{
|
||||
caption: '\\sigma',
|
||||
snippet: '\\sigma',
|
||||
meta: 'cmd',
|
||||
score: 0.25940147926344487,
|
||||
},
|
||||
{
|
||||
caption: '\\pi',
|
||||
snippet: '\\pi',
|
||||
meta: 'cmd',
|
||||
score: 0.25920934567729714,
|
||||
},
|
||||
{
|
||||
caption: '\\hat{}',
|
||||
snippet: '\\hat{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.25264309033778715,
|
||||
},
|
||||
{
|
||||
caption: '\\bibliographystyle{}',
|
||||
snippet: '\\bibliographystyle{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.25122317941387773,
|
||||
},
|
||||
{
|
||||
caption: '\\small',
|
||||
snippet: '\\small',
|
||||
meta: 'cmd',
|
||||
score: 0.2447632045426295,
|
||||
},
|
||||
{
|
||||
caption: '\\LaTeX',
|
||||
snippet: '\\LaTeX',
|
||||
meta: 'cmd',
|
||||
score: 0.2334089308452787,
|
||||
},
|
||||
{
|
||||
caption: '\\cdot',
|
||||
snippet: '\\cdot',
|
||||
meta: 'cmd',
|
||||
score: 0.23029085545522762,
|
||||
},
|
||||
{
|
||||
caption: '\\footnote{}',
|
||||
snippet: '\\footnote{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.2253056071787701,
|
||||
},
|
||||
{
|
||||
caption: '\\newtheorem{}{}',
|
||||
snippet: '\\newtheorem{$1}{$2}',
|
||||
meta: 'cmd',
|
||||
score: 0.215689795055434,
|
||||
},
|
||||
{
|
||||
caption: '\\Delta',
|
||||
snippet: '\\Delta',
|
||||
meta: 'cmd',
|
||||
score: 0.21386475063892618,
|
||||
},
|
||||
{
|
||||
caption: '\\tau',
|
||||
snippet: '\\tau',
|
||||
meta: 'cmd',
|
||||
score: 0.21236188205859796,
|
||||
},
|
||||
{
|
||||
caption: '\\hfill',
|
||||
snippet: '\\hfill',
|
||||
meta: 'cmd',
|
||||
score: 0.2058248088519886,
|
||||
},
|
||||
{
|
||||
caption: '\\leq',
|
||||
snippet: '\\leq',
|
||||
meta: 'cmd',
|
||||
score: 0.20498894440637172,
|
||||
},
|
||||
{
|
||||
caption: '\\footnotesize',
|
||||
snippet: '\\footnotesize',
|
||||
meta: 'cmd',
|
||||
score: 0.2038592081252624,
|
||||
},
|
||||
{
|
||||
caption: '\\large',
|
||||
snippet: '\\large',
|
||||
meta: 'cmd',
|
||||
score: 0.20377416734108866,
|
||||
},
|
||||
{
|
||||
caption: '\\sqrt{}',
|
||||
snippet: '\\sqrt{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.20240160977404634,
|
||||
},
|
||||
{
|
||||
caption: '\\epsilon',
|
||||
snippet: '\\epsilon',
|
||||
meta: 'cmd',
|
||||
score: 0.2005136761359043,
|
||||
},
|
||||
{
|
||||
caption: '\\Large',
|
||||
snippet: '\\Large',
|
||||
meta: 'cmd',
|
||||
score: 0.1987771081149759,
|
||||
},
|
||||
{
|
||||
caption: '\\rho',
|
||||
snippet: '\\rho',
|
||||
meta: 'cmd',
|
||||
score: 0.1959287380541684,
|
||||
},
|
||||
{
|
||||
caption: '\\omega',
|
||||
snippet: '\\omega',
|
||||
meta: 'cmd',
|
||||
score: 0.19326783415115262,
|
||||
},
|
||||
{
|
||||
caption: '\\mathrm{}',
|
||||
snippet: '\\mathrm{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.19117752976172653,
|
||||
},
|
||||
{
|
||||
caption: '\\boldsymbol{}',
|
||||
snippet: '\\boldsymbol{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.18137737738638837,
|
||||
},
|
||||
{
|
||||
caption: '\\gamma',
|
||||
snippet: '\\gamma',
|
||||
meta: 'cmd',
|
||||
score: 0.17940276535431304,
|
||||
},
|
||||
{
|
||||
caption: '\\clearpage',
|
||||
snippet: '\\clearpage',
|
||||
meta: 'cmd',
|
||||
score: 0.1789117552185788,
|
||||
},
|
||||
{
|
||||
caption: '\\infty',
|
||||
snippet: '\\infty',
|
||||
meta: 'cmd',
|
||||
score: 0.17837290019711305,
|
||||
},
|
||||
{
|
||||
caption: '\\phi',
|
||||
snippet: '\\phi',
|
||||
meta: 'cmd',
|
||||
score: 0.17405809173097808,
|
||||
},
|
||||
{
|
||||
caption: '\\partial',
|
||||
snippet: '\\partial',
|
||||
meta: 'cmd',
|
||||
score: 0.17168102367966637,
|
||||
},
|
||||
{
|
||||
caption: '\\include{}',
|
||||
snippet: '\\include{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.1547080054979312,
|
||||
},
|
||||
{
|
||||
caption: '\\address{}',
|
||||
snippet: '\\address{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.1525055392611109,
|
||||
},
|
||||
{
|
||||
caption: '\\quad',
|
||||
snippet: '\\quad',
|
||||
meta: 'cmd',
|
||||
score: 0.15242755832392743,
|
||||
},
|
||||
{
|
||||
caption: '\\paragraph{}',
|
||||
snippet: '\\paragraph{$1}',
|
||||
meta: 'cmd',
|
||||
score: 0.152074250347974,
|
||||
},
|
||||
{
|
||||
caption: '\\varepsilon',
|
||||
snippet: '\\varepsilon',
|
||||
meta: 'cmd',
|
||||
score: 0.05411564201390573,
|
||||
},
|
||||
{
|
||||
caption: '\\zeta',
|
||||
snippet: '\\zeta',
|
||||
meta: 'cmd',
|
||||
score: 0.023330249803752954,
|
||||
},
|
||||
{
|
||||
caption: '\\eta',
|
||||
snippet: '\\eta',
|
||||
meta: 'cmd',
|
||||
score: 0.11088718379889091,
|
||||
},
|
||||
{
|
||||
caption: '\\vartheta',
|
||||
snippet: '\\vartheta',
|
||||
meta: 'cmd',
|
||||
score: 0.0025822992078068712,
|
||||
},
|
||||
{
|
||||
caption: '\\iota',
|
||||
snippet: '\\iota',
|
||||
meta: 'cmd',
|
||||
score: 0.0024774003791525486,
|
||||
},
|
||||
{
|
||||
caption: '\\kappa',
|
||||
snippet: '\\kappa',
|
||||
meta: 'cmd',
|
||||
score: 0.04887876299369008,
|
||||
},
|
||||
{
|
||||
caption: '\\nu',
|
||||
snippet: '\\nu',
|
||||
meta: 'cmd',
|
||||
score: 0.09206962821059342,
|
||||
},
|
||||
{
|
||||
caption: '\\xi',
|
||||
snippet: '\\xi',
|
||||
meta: 'cmd',
|
||||
score: 0.06496042899265699,
|
||||
},
|
||||
{
|
||||
caption: '\\varpi',
|
||||
snippet: '\\varpi',
|
||||
meta: 'cmd',
|
||||
score: 0.0007039358167790341,
|
||||
},
|
||||
{
|
||||
caption: '\\varrho',
|
||||
snippet: '\\varrho',
|
||||
meta: 'cmd',
|
||||
score: 0.0011279491613898612,
|
||||
},
|
||||
{
|
||||
caption: '\\varsigma',
|
||||
snippet: '\\varsigma',
|
||||
meta: 'cmd',
|
||||
score: 0.0010424880711234978,
|
||||
},
|
||||
{
|
||||
caption: '\\upsilon',
|
||||
snippet: '\\upsilon',
|
||||
meta: 'cmd',
|
||||
score: 0.00420715572598688,
|
||||
},
|
||||
{
|
||||
caption: '\\varphi',
|
||||
snippet: '\\varphi',
|
||||
meta: 'cmd',
|
||||
score: 0.03351251516668212,
|
||||
},
|
||||
{
|
||||
caption: '\\chi',
|
||||
snippet: '\\chi',
|
||||
meta: 'cmd',
|
||||
score: 0.043373492287805675,
|
||||
},
|
||||
{
|
||||
caption: '\\psi',
|
||||
snippet: '\\psi',
|
||||
meta: 'cmd',
|
||||
score: 0.09994508706163642,
|
||||
},
|
||||
{
|
||||
caption: '\\Gamma',
|
||||
snippet: '\\Gamma',
|
||||
meta: 'cmd',
|
||||
score: 0.04801549269801977,
|
||||
},
|
||||
{
|
||||
caption: '\\Theta',
|
||||
snippet: '\\Theta',
|
||||
meta: 'cmd',
|
||||
score: 0.038090902146599444,
|
||||
},
|
||||
{
|
||||
caption: '\\Lambda',
|
||||
snippet: '\\Lambda',
|
||||
meta: 'cmd',
|
||||
score: 0.032206594305977686,
|
||||
},
|
||||
{
|
||||
caption: '\\Xi',
|
||||
snippet: '\\Xi',
|
||||
meta: 'cmd',
|
||||
score: 0.01060997225400494,
|
||||
},
|
||||
{
|
||||
caption: '\\Pi',
|
||||
snippet: '\\Pi',
|
||||
meta: 'cmd',
|
||||
score: 0.021264671817473237,
|
||||
},
|
||||
{
|
||||
caption: '\\Sigma',
|
||||
snippet: '\\Sigma',
|
||||
meta: 'cmd',
|
||||
score: 0.05769642802079917,
|
||||
},
|
||||
{
|
||||
caption: '\\Upsilon',
|
||||
snippet: '\\Upsilon',
|
||||
meta: 'cmd',
|
||||
score: 0.00032875192955749566,
|
||||
},
|
||||
{
|
||||
caption: '\\Phi',
|
||||
snippet: '\\Phi',
|
||||
meta: 'cmd',
|
||||
score: 0.0538724950042562,
|
||||
},
|
||||
{
|
||||
caption: '\\Psi',
|
||||
snippet: '\\Psi',
|
||||
meta: 'cmd',
|
||||
score: 0.03056589143021648,
|
||||
},
|
||||
{
|
||||
caption: '\\Omega',
|
||||
snippet: '\\Omega',
|
||||
meta: 'cmd',
|
||||
score: 0.09490387997853639,
|
||||
},
|
||||
]
|
|
@ -1,57 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
||||
let CursorPositionAdapter
|
||||
|
||||
export default CursorPositionAdapter = class CursorPositionAdapter {
|
||||
constructor(editor) {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
getCursor() {
|
||||
return this.editor.getCursorPosition()
|
||||
}
|
||||
|
||||
getEditorScrollPosition() {
|
||||
return this.editor.getFirstVisibleRow()
|
||||
}
|
||||
|
||||
setCursor(pos) {
|
||||
pos = pos.cursorPosition || { row: 0, column: 0 }
|
||||
return this.editor.moveCursorToPosition(pos)
|
||||
}
|
||||
|
||||
setEditorScrollPosition(pos) {
|
||||
pos = pos.firstVisibleLine || 0
|
||||
return this.editor.scrollToLine(pos)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
return this.editor.selection.clearSelection()
|
||||
}
|
||||
|
||||
gotoLine(line, column) {
|
||||
this.editor.gotoLine(line, column)
|
||||
this.editor.scrollToLine(line, true, true) // centre and animate
|
||||
return this.editor.focus()
|
||||
}
|
||||
|
||||
gotoOffset(offset) {
|
||||
const lines = this.editor.getSession().getDocument().getAllLines()
|
||||
const position = EditorShareJsCodec.shareJsOffsetToRowColumn(offset, lines)
|
||||
return this.gotoLine(position.row + 1, position.column)
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.editor.focus()
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let CursorPositionManager
|
||||
|
||||
export default CursorPositionManager = class CursorPositionManager {
|
||||
constructor($scope, adapter, localStorage) {
|
||||
this.storePositionAndLine = this.storePositionAndLine.bind(this)
|
||||
this.jumpToPositionInNewDoc = this.jumpToPositionInNewDoc.bind(this)
|
||||
this.onUnload = this.onUnload.bind(this)
|
||||
this.onCursorChange = this.onCursorChange.bind(this)
|
||||
this.onSyncToPdf = this.onSyncToPdf.bind(this)
|
||||
this.$scope = $scope
|
||||
this.adapter = adapter
|
||||
this.localStorage = localStorage
|
||||
this.$scope.$on('editorInit', this.jumpToPositionInNewDoc)
|
||||
|
||||
this.$scope.$on('store-doc-position', this.storePositionAndLine)
|
||||
|
||||
this.$scope.$on('afterChangeDocument', this.jumpToPositionInNewDoc)
|
||||
|
||||
this.$scope.$on('changeEditor', this.storePositionAndLine)
|
||||
|
||||
this.$scope.$on(
|
||||
`${this.$scope.name}:gotoLine`,
|
||||
(e, line, column, syncToPdf) => {
|
||||
if (line != null) {
|
||||
return setTimeout(() => {
|
||||
this.adapter.gotoLine(line, column)
|
||||
if (syncToPdf) this.onSyncToPdf()
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
) // Hack: Must happen after @gotoStoredPosition
|
||||
|
||||
this.$scope.$on(`${this.$scope.name}:gotoOffset`, (e, offset) => {
|
||||
if (offset != null) {
|
||||
return setTimeout(() => {
|
||||
return this.adapter.gotoOffset(offset)
|
||||
}, 10)
|
||||
}
|
||||
}) // Hack: Must happen after @gotoStoredPosition
|
||||
|
||||
this.$scope.$on(`${this.$scope.name}:clearSelection`, e => {
|
||||
return this.adapter.clearSelection()
|
||||
})
|
||||
}
|
||||
|
||||
storePositionAndLine() {
|
||||
this.storeCursorPosition()
|
||||
return this.storeFirstVisibleLine()
|
||||
}
|
||||
|
||||
jumpToPositionInNewDoc() {
|
||||
this.doc_id =
|
||||
this.$scope.sharejsDoc != null ? this.$scope.sharejsDoc.doc_id : undefined
|
||||
return setTimeout(() => {
|
||||
return this.gotoStoredPosition()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
onUnload() {
|
||||
this.storeCursorPosition()
|
||||
return this.storeFirstVisibleLine()
|
||||
}
|
||||
|
||||
onCursorChange() {
|
||||
return this.emitCursorUpdateEvent()
|
||||
}
|
||||
|
||||
onSyncToPdf() {
|
||||
return this.$scope.$emit(`cursor:${this.$scope.name}:syncToPdf`)
|
||||
}
|
||||
|
||||
storeFirstVisibleLine() {
|
||||
if (this.doc_id != null) {
|
||||
const docPosition = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
||||
docPosition.firstVisibleLine = this.adapter.getEditorScrollPosition()
|
||||
return this.localStorage(`doc.position.${this.doc_id}`, docPosition)
|
||||
}
|
||||
}
|
||||
|
||||
storeCursorPosition() {
|
||||
if (this.doc_id != null) {
|
||||
const docPosition = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
||||
docPosition.cursorPosition = this.adapter.getCursor()
|
||||
return this.localStorage(`doc.position.${this.doc_id}`, docPosition)
|
||||
}
|
||||
}
|
||||
|
||||
emitCursorUpdateEvent() {
|
||||
const cursor = this.adapter.getCursor()
|
||||
this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`cursor:${this.$scope.name}:update`, {
|
||||
detail: cursor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
gotoStoredPosition() {
|
||||
if (this.doc_id == null) {
|
||||
return
|
||||
}
|
||||
const pos = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
||||
this.adapter.setCursor(pos)
|
||||
this.adapter.setEditorScrollPosition(pos)
|
||||
this.adapter.focus()
|
||||
}
|
||||
}
|
|
@ -1,396 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import 'ace/ace'
|
||||
import ColorManager from '../../../../colors/ColorManager'
|
||||
let HighlightsManager
|
||||
const { Range } = ace.require('ace/range')
|
||||
|
||||
export default HighlightsManager = class HighlightsManager {
|
||||
constructor($scope, editor, element) {
|
||||
this.$scope = $scope
|
||||
this.editor = editor
|
||||
this.element = element
|
||||
this.markerIds = []
|
||||
this.labels = []
|
||||
|
||||
this.$scope.annotationLabel = {
|
||||
show: false,
|
||||
right: 'auto',
|
||||
left: 'auto',
|
||||
top: 'auto',
|
||||
bottom: 'auto',
|
||||
backgroundColor: 'black',
|
||||
text: '',
|
||||
}
|
||||
|
||||
this.$scope.updateLabels = {
|
||||
updatesAbove: 0,
|
||||
updatesBelow: 0,
|
||||
}
|
||||
|
||||
this.$scope.$watch('highlights', value => {
|
||||
return this.redrawAnnotations()
|
||||
})
|
||||
|
||||
this.$scope.$watch('theme', value => {
|
||||
return this.redrawAnnotations()
|
||||
})
|
||||
|
||||
this.editor.on('mousemove', e => {
|
||||
const position = this.editor.renderer.screenToTextCoordinates(
|
||||
e.clientX,
|
||||
e.clientY
|
||||
)
|
||||
e.position = position
|
||||
return this.showAnnotationLabels(position)
|
||||
})
|
||||
|
||||
const onChangeScrollTop = () => {
|
||||
return this.updateShowMoreLabels()
|
||||
}
|
||||
|
||||
this.editor.getSession().on('changeScrollTop', onChangeScrollTop)
|
||||
|
||||
this.$scope.$watch('text', () => {
|
||||
if (this.$scope.navigateHighlights) {
|
||||
return setTimeout(() => {
|
||||
return this.scrollToFirstHighlight()
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
this.editor.on('changeSession', e => {
|
||||
if (e.oldSession != null) {
|
||||
e.oldSession.off('changeScrollTop', onChangeScrollTop)
|
||||
}
|
||||
e.session.on('changeScrollTop', onChangeScrollTop)
|
||||
return this.redrawAnnotations()
|
||||
})
|
||||
|
||||
this.$scope.gotoHighlightBelow = () => {
|
||||
if (this.firstHiddenHighlightAfter == null) {
|
||||
return
|
||||
}
|
||||
return this.editor.scrollToLine(
|
||||
this.firstHiddenHighlightAfter.end.row,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
this.$scope.gotoHighlightAbove = () => {
|
||||
if (this.lastHiddenHighlightBefore == null) {
|
||||
return
|
||||
}
|
||||
return this.editor.scrollToLine(
|
||||
this.lastHiddenHighlightBefore.start.row,
|
||||
true,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
redrawAnnotations() {
|
||||
this._clearMarkers()
|
||||
this._clearLabels()
|
||||
|
||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
||||
;(annotation => {
|
||||
const colorScheme = ColorManager.getColorScheme(
|
||||
annotation.hue,
|
||||
this.element
|
||||
)
|
||||
if (annotation.cursor != null) {
|
||||
this.labels.push({
|
||||
text: annotation.label,
|
||||
range: new Range(
|
||||
annotation.cursor.row,
|
||||
annotation.cursor.column,
|
||||
annotation.cursor.row,
|
||||
annotation.cursor.column + 1
|
||||
),
|
||||
colorScheme,
|
||||
snapToStartOfRange: true,
|
||||
})
|
||||
return this._drawCursor(annotation, colorScheme)
|
||||
} else if (annotation.highlight != null) {
|
||||
this.labels.push({
|
||||
text: annotation.label,
|
||||
range: new Range(
|
||||
annotation.highlight.start.row,
|
||||
annotation.highlight.start.column,
|
||||
annotation.highlight.end.row,
|
||||
annotation.highlight.end.column
|
||||
),
|
||||
colorScheme,
|
||||
})
|
||||
return this._drawHighlight(annotation, colorScheme)
|
||||
} else if (annotation.strikeThrough != null) {
|
||||
this.labels.push({
|
||||
text: annotation.label,
|
||||
range: new Range(
|
||||
annotation.strikeThrough.start.row,
|
||||
annotation.strikeThrough.start.column,
|
||||
annotation.strikeThrough.end.row,
|
||||
annotation.strikeThrough.end.column
|
||||
),
|
||||
colorScheme,
|
||||
})
|
||||
return this._drawStrikeThrough(annotation, colorScheme)
|
||||
}
|
||||
})(annotation)
|
||||
}
|
||||
|
||||
return this.updateShowMoreLabels()
|
||||
}
|
||||
|
||||
showAnnotationLabels(position) {
|
||||
let labelToShow = null
|
||||
for (const label of Array.from(this.labels || [])) {
|
||||
if (label.range.contains(position.row, position.column)) {
|
||||
labelToShow = label
|
||||
}
|
||||
}
|
||||
|
||||
if (labelToShow == null) {
|
||||
// this is the most common path, triggered on mousemove, so
|
||||
// for performance only apply setting when it changes
|
||||
if (
|
||||
__guard__(
|
||||
this.$scope != null ? this.$scope.annotationLabel : undefined,
|
||||
x => x.show
|
||||
) !== false
|
||||
) {
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.annotationLabel.show = false)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let bottom, coords, left, right, top
|
||||
const $ace = $(this.editor.renderer.container).find('.ace_scroller')
|
||||
// Move the label into the Ace content area so that offsets and positions are easy to calculate.
|
||||
$ace.append(this.element.find('.annotation-label'))
|
||||
|
||||
if (labelToShow.snapToStartOfRange) {
|
||||
coords = this.editor.renderer.textToScreenCoordinates(
|
||||
labelToShow.range.start.row,
|
||||
labelToShow.range.start.column
|
||||
)
|
||||
} else {
|
||||
coords = this.editor.renderer.textToScreenCoordinates(
|
||||
position.row,
|
||||
position.column
|
||||
)
|
||||
}
|
||||
|
||||
const offset = $ace.offset()
|
||||
const height = $ace.height()
|
||||
coords.pageX = coords.pageX - offset.left
|
||||
coords.pageY = coords.pageY - offset.top
|
||||
|
||||
if (coords.pageY > this.editor.renderer.lineHeight * 2) {
|
||||
top = 'auto'
|
||||
bottom = height - coords.pageY
|
||||
} else {
|
||||
top = coords.pageY + this.editor.renderer.lineHeight
|
||||
bottom = 'auto'
|
||||
}
|
||||
|
||||
// Apply this first that the label has the correct width when calculating below
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.annotationLabel.text = labelToShow.text
|
||||
return (this.$scope.annotationLabel.show = true)
|
||||
})
|
||||
|
||||
const $label = this.element.find('.annotation-label')
|
||||
|
||||
if (coords.pageX + $label.outerWidth() < $ace.width()) {
|
||||
left = coords.pageX
|
||||
right = 'auto'
|
||||
} else {
|
||||
right = 0
|
||||
left = 'auto'
|
||||
}
|
||||
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.annotationLabel = {
|
||||
show: true,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
top,
|
||||
backgroundColor: labelToShow.colorScheme.labelBackgroundColor,
|
||||
text: labelToShow.text,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateShowMoreLabels() {
|
||||
if (!this.$scope.navigateHighlights) {
|
||||
return
|
||||
}
|
||||
return setTimeout(() => {
|
||||
const firstRow = this.editor.getFirstVisibleRow()
|
||||
const lastRow = this.editor.getLastVisibleRow()
|
||||
let highlightsBefore = 0
|
||||
let highlightsAfter = 0
|
||||
this.lastHiddenHighlightBefore = null
|
||||
this.firstHiddenHighlightAfter = null
|
||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
||||
const range = annotation.highlight || annotation.strikeThrough
|
||||
if (range == null) {
|
||||
continue
|
||||
}
|
||||
if (range.start.row < firstRow) {
|
||||
highlightsBefore += 1
|
||||
this.lastHiddenHighlightBefore = range
|
||||
}
|
||||
if (range.end.row > lastRow) {
|
||||
highlightsAfter += 1
|
||||
if (!this.firstHiddenHighlightAfter) {
|
||||
this.firstHiddenHighlightAfter = range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.updateLabels = {
|
||||
highlightsBefore,
|
||||
highlightsAfter,
|
||||
})
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
|
||||
scrollToFirstHighlight() {
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
||||
const range = annotation.highlight || annotation.strikeThrough
|
||||
if (range == null) {
|
||||
continue
|
||||
}
|
||||
this.editor.scrollToLine(range.start.row, true, false)
|
||||
break
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
_clearMarkers() {
|
||||
for (const marker_id of Array.from(this.markerIds)) {
|
||||
this.editor.getSession().removeMarker(marker_id)
|
||||
}
|
||||
return (this.markerIds = [])
|
||||
}
|
||||
|
||||
_clearLabels() {
|
||||
return (this.labels = [])
|
||||
}
|
||||
|
||||
_drawCursor(annotation, colorScheme) {
|
||||
return this._addMarkerWithCustomStyle(
|
||||
new Range(
|
||||
annotation.cursor.row,
|
||||
annotation.cursor.column,
|
||||
annotation.cursor.row,
|
||||
annotation.cursor.column + 1
|
||||
),
|
||||
'annotation remote-cursor',
|
||||
false,
|
||||
`border-color: ${colorScheme.cursor};`
|
||||
)
|
||||
}
|
||||
|
||||
_drawHighlight(annotation, colorScheme) {
|
||||
return this._addMarkerWithCustomStyle(
|
||||
new Range(
|
||||
annotation.highlight.start.row,
|
||||
annotation.highlight.start.column,
|
||||
annotation.highlight.end.row,
|
||||
annotation.highlight.end.column
|
||||
),
|
||||
'annotation highlight',
|
||||
false,
|
||||
`background-color: ${colorScheme.highlightBackgroundColor}`
|
||||
)
|
||||
}
|
||||
|
||||
_drawStrikeThrough(annotation, colorScheme) {
|
||||
this._addMarkerWithCustomStyle(
|
||||
new Range(
|
||||
annotation.strikeThrough.start.row,
|
||||
annotation.strikeThrough.start.column,
|
||||
annotation.strikeThrough.end.row,
|
||||
annotation.strikeThrough.end.column
|
||||
),
|
||||
'annotation strike-through-background',
|
||||
false,
|
||||
`background-color: ${colorScheme.strikeThroughBackgroundColor}`
|
||||
)
|
||||
return this._addMarkerWithCustomStyle(
|
||||
new Range(
|
||||
annotation.strikeThrough.start.row,
|
||||
annotation.strikeThrough.start.column,
|
||||
annotation.strikeThrough.end.row,
|
||||
annotation.strikeThrough.end.column
|
||||
),
|
||||
'annotation strike-through-foreground',
|
||||
true,
|
||||
`color: ${colorScheme.strikeThroughForegroundColor};`
|
||||
)
|
||||
}
|
||||
|
||||
_addMarkerWithCustomStyle(range, klass, foreground, style) {
|
||||
let markerLayer
|
||||
if (!foreground) {
|
||||
markerLayer = this.editor.renderer.$markerBack
|
||||
} else {
|
||||
markerLayer = this.editor.renderer.$markerFront
|
||||
}
|
||||
|
||||
return this.markerIds.push(
|
||||
this.editor.getSession().addMarker(
|
||||
range,
|
||||
klass,
|
||||
function (html, range, left, top, config) {
|
||||
if (range.isMultiLine()) {
|
||||
return markerLayer.drawTextMarker(html, range, klass, config, style)
|
||||
} else {
|
||||
return markerLayer.drawSingleLineMarker(
|
||||
html,
|
||||
range,
|
||||
`${klass} ace_start`,
|
||||
config,
|
||||
0,
|
||||
style
|
||||
)
|
||||
}
|
||||
},
|
||||
foreground
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
no-cond-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import 'ace/ace'
|
||||
let MetadataManager
|
||||
const { Range } = ace.require('ace/range')
|
||||
|
||||
const getLastCommandFragment = function (lineUpToCursor) {
|
||||
let m
|
||||
if ((m = lineUpToCursor.match(/(\\[^\\]+)$/))) {
|
||||
return m[1]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataManager = class MetadataManager {
|
||||
constructor($scope, editor, element, Metadata) {
|
||||
this.$scope = $scope
|
||||
this.editor = editor
|
||||
this.element = element
|
||||
this.Metadata = Metadata
|
||||
this.debouncer = {} // DocId => Timeout
|
||||
|
||||
const onChange = change => {
|
||||
if (change.origin === 'remote') {
|
||||
return
|
||||
}
|
||||
if (!['remove', 'insert'].includes(change.action)) {
|
||||
return
|
||||
}
|
||||
const cursorPosition = this.editor.getCursorPosition()
|
||||
const { end } = change
|
||||
let range = new Range(end.row, 0, end.row, end.column)
|
||||
let lineUpToCursor = this.editor.getSession().getTextRange(range)
|
||||
if (
|
||||
lineUpToCursor.trim() === '%' ||
|
||||
lineUpToCursor.slice(0, 1) === '\\'
|
||||
) {
|
||||
range = new Range(end.row, 0, end.row, end.column + 80)
|
||||
lineUpToCursor = this.editor.getSession().getTextRange(range)
|
||||
}
|
||||
const commandFragment = getLastCommandFragment(lineUpToCursor)
|
||||
|
||||
const linesContainPackage = _.some(change.lines, line =>
|
||||
line.match(/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
|
||||
)
|
||||
const linesContainReqPackage = _.some(change.lines, line =>
|
||||
line.match(/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
|
||||
)
|
||||
const linesContainLabel = _.some(change.lines, line =>
|
||||
line.match(/\\label{(.{0,80}?)}/)
|
||||
)
|
||||
const linesContainMeta =
|
||||
linesContainPackage || linesContainLabel || linesContainReqPackage
|
||||
|
||||
const lastCommandFragmentIsLabel =
|
||||
(commandFragment != null ? commandFragment.slice(0, 7) : undefined) ===
|
||||
'\\label{'
|
||||
const lastCommandFragmentIsPackage =
|
||||
(commandFragment != null ? commandFragment.slice(0, 11) : undefined) ===
|
||||
'\\usepackage'
|
||||
const lastCommandFragmentIsReqPack =
|
||||
(commandFragment != null ? commandFragment.slice(0, 15) : undefined) ===
|
||||
'\\RequirePackage'
|
||||
const lastCommandFragmentIsMeta =
|
||||
lastCommandFragmentIsPackage ||
|
||||
lastCommandFragmentIsLabel ||
|
||||
lastCommandFragmentIsReqPack
|
||||
|
||||
if (linesContainMeta || lastCommandFragmentIsMeta) {
|
||||
return this.Metadata.scheduleLoadDocMetaFromServer(this.$scope.docId)
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.on('changeSession', e => {
|
||||
e.oldSession.off('change', onChange)
|
||||
return e.session.on('change', onChange)
|
||||
})
|
||||
}
|
||||
|
||||
getAllLabels() {
|
||||
return this.Metadata.getAllLabels()
|
||||
}
|
||||
|
||||
getAllPackages() {
|
||||
return this.Metadata.getAllPackages()
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import 'ace/ace'
|
||||
let HighlightedWordManager
|
||||
const { Range } = ace.require('ace/range')
|
||||
|
||||
class Highlight {
|
||||
constructor(markerId, range, options) {
|
||||
this.markerId = markerId
|
||||
this.range = range
|
||||
this.word = options.word
|
||||
this.suggestions = options.suggestions
|
||||
}
|
||||
}
|
||||
|
||||
export default HighlightedWordManager = class HighlightedWordManager {
|
||||
constructor(editor) {
|
||||
this.editor = editor
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.highlights != null) {
|
||||
this.highlights.forEach(highlight => {
|
||||
return this.editor.getSession().removeMarker(highlight.markerId)
|
||||
})
|
||||
}
|
||||
return (this.highlights = [])
|
||||
}
|
||||
|
||||
addHighlight(options) {
|
||||
const session = this.editor.getSession()
|
||||
const doc = session.getDocument()
|
||||
// Set up Range that will automatically update it's positions when the
|
||||
// document changes
|
||||
const range = new Range()
|
||||
range.start = doc.createAnchor({
|
||||
row: options.row,
|
||||
column: options.column,
|
||||
})
|
||||
range.end = doc.createAnchor({
|
||||
row: options.row,
|
||||
column: options.column + options.word.length,
|
||||
})
|
||||
// Prevent range from adding newly typed characters to the end of the word.
|
||||
// This makes it appear as if the spelling error continues to the next word
|
||||
// even after a space
|
||||
range.end.$insertRight = true
|
||||
|
||||
const markerId = session.addMarker(
|
||||
range,
|
||||
'spelling-highlight',
|
||||
'text',
|
||||
false
|
||||
)
|
||||
|
||||
return this.highlights.push(new Highlight(markerId, range, options))
|
||||
}
|
||||
|
||||
removeHighlight(highlight) {
|
||||
this.editor.getSession().removeMarker(highlight.markerId)
|
||||
return (this.highlights = this.highlights.filter(hl => hl !== highlight))
|
||||
}
|
||||
|
||||
removeWord(word) {
|
||||
return this.highlights
|
||||
.filter(highlight => highlight.word === word)
|
||||
.forEach(highlight => {
|
||||
return this.removeHighlight(highlight)
|
||||
})
|
||||
}
|
||||
|
||||
clearRow(row) {
|
||||
return this.highlights
|
||||
.filter(highlight => highlight.range.start.row === row)
|
||||
.forEach(highlight => {
|
||||
return this.removeHighlight(highlight)
|
||||
})
|
||||
}
|
||||
|
||||
findHighlightWithinRange(range) {
|
||||
return _.find(this.highlights, highlight => {
|
||||
return this._doesHighlightOverlapRange(highlight, range.start, range.end)
|
||||
})
|
||||
}
|
||||
|
||||
_doesHighlightOverlapRange(highlight, start, end) {
|
||||
const highlightRow = highlight.range.start.row
|
||||
const highlightStartColumn = highlight.range.start.column
|
||||
const highlightEndColumn = highlight.range.end.column
|
||||
|
||||
const highlightIsAllBeforeRange =
|
||||
highlightRow < start.row ||
|
||||
(highlightRow === start.row && highlightEndColumn <= start.column)
|
||||
const highlightIsAllAfterRange =
|
||||
highlightRow > end.row ||
|
||||
(highlightRow === end.row && highlightStartColumn >= end.column)
|
||||
return !(highlightIsAllBeforeRange || highlightIsAllAfterRange)
|
||||
}
|
||||
|
||||
clearHighlightTouchingRange(range) {
|
||||
const highlight = _.find(this.highlights, hl => {
|
||||
return this._doesHighlightTouchRange(hl, range.start, range.end)
|
||||
})
|
||||
if (highlight) {
|
||||
return this.removeHighlight(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
_doesHighlightTouchRange(highlight, start, end) {
|
||||
const highlightRow = highlight.range.start.row
|
||||
const highlightStartColumn = highlight.range.start.column
|
||||
const highlightEndColumn = highlight.range.end.column
|
||||
|
||||
const rangeStartIsWithinHighlight =
|
||||
highlightStartColumn <= start.column && highlightEndColumn >= start.column
|
||||
const rangeEndIsWithinHighlight =
|
||||
highlightStartColumn <= end.column && highlightEndColumn >= end.column
|
||||
|
||||
return (
|
||||
highlightRow === start.row &&
|
||||
(rangeStartIsWithinHighlight || rangeEndIsWithinHighlight)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
export const IGNORED_MISSPELLINGS = [
|
||||
'Overleaf',
|
||||
'overleaf',
|
||||
'ShareLaTeX',
|
||||
'sharelatex',
|
||||
'LaTeX',
|
||||
'TeX',
|
||||
'BibTeX',
|
||||
'BibLaTeX',
|
||||
'XeTeX',
|
||||
'XeLaTeX',
|
||||
'LuaTeX',
|
||||
'LuaLaTeX',
|
||||
'http',
|
||||
'https',
|
||||
'www',
|
||||
'COVID',
|
||||
'Lockdown',
|
||||
'lockdown',
|
||||
'Coronavirus',
|
||||
'coronavirus',
|
||||
]
|
|
@ -1,96 +0,0 @@
|
|||
import HighlightedWordManager from './HighlightedWordManager'
|
||||
import 'ace/ace'
|
||||
const { Range } = ace.require('ace/range')
|
||||
|
||||
class SpellCheckAdapter {
|
||||
constructor(editor) {
|
||||
this.replaceWord = this.replaceWord.bind(this)
|
||||
this.editor = editor
|
||||
this.highlightedWordManager = new HighlightedWordManager(this.editor)
|
||||
}
|
||||
|
||||
getLines() {
|
||||
return this.editor.getValue().split('\n')
|
||||
}
|
||||
|
||||
getLineCount() {
|
||||
return this.editor.session.getLength()
|
||||
}
|
||||
|
||||
getFirstVisibleRowNum() {
|
||||
return this.editor.renderer.layerConfig.firstRow
|
||||
}
|
||||
|
||||
getLastVisibleRowNum() {
|
||||
return this.editor.renderer.layerConfig.lastRow
|
||||
}
|
||||
|
||||
getLinesByRows(rows) {
|
||||
return rows.map(rowIdx => this.editor.session.doc.getLine(rowIdx))
|
||||
}
|
||||
|
||||
getSelectionContents() {
|
||||
return this.editor.getSelectedText()
|
||||
}
|
||||
|
||||
normalizeChangeEvent(e) {
|
||||
return e
|
||||
}
|
||||
|
||||
getCoordsFromContextMenuEvent(e) {
|
||||
e.domEvent.stopPropagation()
|
||||
return {
|
||||
x: e.domEvent.clientX,
|
||||
y: e.domEvent.clientY,
|
||||
}
|
||||
}
|
||||
|
||||
preventContextMenuEventDefault(e) {
|
||||
e.domEvent.preventDefault()
|
||||
}
|
||||
|
||||
getHighlightFromCoords(coords) {
|
||||
const position = this.editor.renderer.screenToTextCoordinates(
|
||||
coords.x,
|
||||
coords.y
|
||||
)
|
||||
return this.highlightedWordManager.findHighlightWithinRange({
|
||||
start: position,
|
||||
end: position,
|
||||
})
|
||||
}
|
||||
|
||||
isContextMenuEventOnBottomHalf(e) {
|
||||
const { clientY } = e.domEvent
|
||||
const editorBoundingRect = e.target.container.getBoundingClientRect()
|
||||
const relativeYPos =
|
||||
(clientY - editorBoundingRect.top) / editorBoundingRect.height
|
||||
return relativeYPos > 0.5
|
||||
}
|
||||
|
||||
selectHighlightedWord(highlight) {
|
||||
const { row } = highlight.range.start
|
||||
const startColumn = highlight.range.start.column
|
||||
const endColumn = highlight.range.end.column
|
||||
|
||||
this.editor
|
||||
.getSession()
|
||||
.getSelection()
|
||||
.setSelectionRange(new Range(row, startColumn, row, endColumn))
|
||||
}
|
||||
|
||||
replaceWord(highlight, newWord) {
|
||||
const { row } = highlight.range.start
|
||||
const startColumn = highlight.range.start.column
|
||||
const endColumn = highlight.range.end.column
|
||||
|
||||
this.editor
|
||||
.getSession()
|
||||
.replace(new Range(row, startColumn, row, endColumn), newWord)
|
||||
|
||||
// Bring editor back into focus after clicking on suggestion
|
||||
this.editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
export default SpellCheckAdapter
|
File diff suppressed because one or more lines are too long
|
@ -1,240 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase
|
||||
*/
|
||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
||||
import 'ace/ace'
|
||||
const { Range } = ace.require('ace/range')
|
||||
class TrackChangesAdapter {
|
||||
constructor(editor) {
|
||||
this.editor = editor
|
||||
this.changeIdToMarkerIdMap = {}
|
||||
}
|
||||
|
||||
tearDown() {
|
||||
this.changeIdToMarkerIdMap = {}
|
||||
}
|
||||
|
||||
clearAnnotations() {
|
||||
const session = this.editor.getSession()
|
||||
for (const change_id in this.changeIdToMarkerIdMap) {
|
||||
const markers = this.changeIdToMarkerIdMap[change_id]
|
||||
for (const marker_name in markers) {
|
||||
const marker_id = markers[marker_name]
|
||||
session.removeMarker(marker_id)
|
||||
}
|
||||
}
|
||||
this.changeIdToMarkerIdMap = {}
|
||||
}
|
||||
|
||||
onInsertAdded(change) {
|
||||
const start = this.shareJsOffsetToRowColumn(change.op.p)
|
||||
const end = this.shareJsOffsetToRowColumn(change.op.p + change.op.i.length)
|
||||
|
||||
const session = this.editor.getSession()
|
||||
const background_range = new Range(
|
||||
start.row,
|
||||
start.column,
|
||||
end.row,
|
||||
end.column
|
||||
)
|
||||
const background_marker_id = session.addMarker(
|
||||
background_range,
|
||||
'track-changes-marker track-changes-added-marker',
|
||||
'text'
|
||||
)
|
||||
const callout_marker_id = this.createCalloutMarker(
|
||||
start,
|
||||
'track-changes-added-marker-callout'
|
||||
)
|
||||
|
||||
this.changeIdToMarkerIdMap[change.id] = {
|
||||
background_marker_id,
|
||||
callout_marker_id,
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteAdded(change) {
|
||||
const position = this.shareJsOffsetToRowColumn(change.op.p)
|
||||
const session = this.editor.getSession()
|
||||
|
||||
const markerLayer = this.editor.renderer.$markerBack
|
||||
const klass = 'track-changes-marker track-changes-deleted-marker'
|
||||
const background_range = this.makeZeroWidthRange(position)
|
||||
const background_marker_id = session.addMarker(
|
||||
background_range,
|
||||
klass,
|
||||
(html, range, left, top, config) =>
|
||||
markerLayer.drawSingleLineMarker(
|
||||
html,
|
||||
range,
|
||||
`${klass} ace_start`,
|
||||
config,
|
||||
0,
|
||||
''
|
||||
)
|
||||
)
|
||||
|
||||
const callout_marker_id = this.createCalloutMarker(
|
||||
position,
|
||||
'track-changes-deleted-marker-callout'
|
||||
)
|
||||
|
||||
this.changeIdToMarkerIdMap[change.id] = {
|
||||
background_marker_id,
|
||||
callout_marker_id,
|
||||
}
|
||||
}
|
||||
|
||||
onInsertRemoved(change) {
|
||||
if (this.changeIdToMarkerIdMap[change.id] == null) {
|
||||
return
|
||||
}
|
||||
const { background_marker_id, callout_marker_id } =
|
||||
this.changeIdToMarkerIdMap[change.id]
|
||||
delete this.changeIdToMarkerIdMap[change.id]
|
||||
const session = this.editor.getSession()
|
||||
session.removeMarker(background_marker_id)
|
||||
session.removeMarker(callout_marker_id)
|
||||
}
|
||||
|
||||
onDeleteRemoved(change) {
|
||||
if (this.changeIdToMarkerIdMap[change.id] == null) {
|
||||
return
|
||||
}
|
||||
const { background_marker_id, callout_marker_id } =
|
||||
this.changeIdToMarkerIdMap[change.id]
|
||||
delete this.changeIdToMarkerIdMap[change.id]
|
||||
|
||||
const session = this.editor.getSession()
|
||||
session.removeMarker(background_marker_id)
|
||||
session.removeMarker(callout_marker_id)
|
||||
}
|
||||
|
||||
onChangeMoved(change) {
|
||||
let end
|
||||
const start = this.shareJsOffsetToRowColumn(change.op.p)
|
||||
if (change.op.i != null) {
|
||||
end = this.shareJsOffsetToRowColumn(change.op.p + change.op.i.length)
|
||||
} else {
|
||||
end = start
|
||||
}
|
||||
this.updateMarker(change.id, start, end)
|
||||
}
|
||||
|
||||
onCommentAdded(comment) {
|
||||
if (this.changeIdToMarkerIdMap[comment.id] == null) {
|
||||
// Only create new markers if they don't already exist
|
||||
const start = this.shareJsOffsetToRowColumn(comment.op.p)
|
||||
const end = this.shareJsOffsetToRowColumn(
|
||||
comment.op.p + comment.op.c.length
|
||||
)
|
||||
const session = this.editor.getSession()
|
||||
const background_range = new Range(
|
||||
start.row,
|
||||
start.column,
|
||||
end.row,
|
||||
end.column
|
||||
)
|
||||
const background_marker_id = session.addMarker(
|
||||
background_range,
|
||||
'track-changes-marker track-changes-comment-marker',
|
||||
'text'
|
||||
)
|
||||
const callout_marker_id = this.createCalloutMarker(
|
||||
start,
|
||||
'track-changes-comment-marker-callout'
|
||||
)
|
||||
this.changeIdToMarkerIdMap[comment.id] = {
|
||||
background_marker_id,
|
||||
callout_marker_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCommentMoved(comment) {
|
||||
const start = this.shareJsOffsetToRowColumn(comment.op.p)
|
||||
const end = this.shareJsOffsetToRowColumn(
|
||||
comment.op.p + comment.op.c.length
|
||||
)
|
||||
this.updateMarker(comment.id, start, end)
|
||||
}
|
||||
|
||||
onCommentRemoved(comment) {
|
||||
if (this.changeIdToMarkerIdMap[comment.id] != null) {
|
||||
// Resolved comments may not have marker ids
|
||||
const { background_marker_id, callout_marker_id } =
|
||||
this.changeIdToMarkerIdMap[comment.id]
|
||||
delete this.changeIdToMarkerIdMap[comment.id]
|
||||
const session = this.editor.getSession()
|
||||
session.removeMarker(background_marker_id)
|
||||
session.removeMarker(callout_marker_id)
|
||||
}
|
||||
}
|
||||
|
||||
updateMarker(change_id, start, end) {
|
||||
if (this.changeIdToMarkerIdMap[change_id] == null) {
|
||||
return
|
||||
}
|
||||
const session = this.editor.getSession()
|
||||
const markers = session.getMarkers()
|
||||
const { background_marker_id, callout_marker_id } =
|
||||
this.changeIdToMarkerIdMap[change_id]
|
||||
if (background_marker_id != null && markers[background_marker_id] != null) {
|
||||
const background_marker = markers[background_marker_id]
|
||||
background_marker.range.start = start
|
||||
background_marker.range.end = end
|
||||
}
|
||||
if (callout_marker_id != null && markers[callout_marker_id] != null) {
|
||||
const callout_marker = markers[callout_marker_id]
|
||||
callout_marker.range.start = start
|
||||
callout_marker.range.end = start
|
||||
}
|
||||
}
|
||||
|
||||
shareJsOffsetToRowColumn(offset) {
|
||||
const lines = this.editor.getSession().getDocument().getAllLines()
|
||||
return EditorShareJsCodec.shareJsOffsetToRowColumn(offset, lines)
|
||||
}
|
||||
|
||||
createCalloutMarker(position, klass) {
|
||||
const session = this.editor.getSession()
|
||||
const callout_range = this.makeZeroWidthRange(position)
|
||||
const markerLayer = this.editor.renderer.$markerBack
|
||||
return session.addMarker(
|
||||
callout_range,
|
||||
klass,
|
||||
(html, range, left, top, config) =>
|
||||
markerLayer.drawSingleLineMarker(
|
||||
html,
|
||||
range,
|
||||
`track-changes-marker-callout ${klass} ace_start`,
|
||||
config,
|
||||
0,
|
||||
'width: auto; right: 0;'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
makeZeroWidthRange(position) {
|
||||
const ace_range = new Range(
|
||||
position.row,
|
||||
position.column,
|
||||
position.row,
|
||||
position.column
|
||||
)
|
||||
// Our delete marker is zero characters wide, but Ace doesn't draw ranges
|
||||
// that are empty. So we monkey patch the range to tell Ace it's not empty
|
||||
// We do want to claim to be empty if we're off screen after clipping rows
|
||||
// though. This is the code we need to trick:
|
||||
// var range = marker.range.clipRows(config.firstRow, config.lastRow);
|
||||
// if (range.isEmpty()) continue;
|
||||
ace_range.clipRows = function (first_row, last_row) {
|
||||
this.isEmpty = function () {
|
||||
return first_row > this.end.row || last_row < this.start.row
|
||||
}
|
||||
return this
|
||||
}
|
||||
return ace_range
|
||||
}
|
||||
}
|
||||
export default TrackChangesAdapter
|
|
@ -1,664 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len
|
||||
*/
|
||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
||||
import 'ace/ace'
|
||||
import '../../../../../utils/EventEmitter'
|
||||
import '../../../../colors/ColorManager'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
const { Range } = ace.require('ace/range')
|
||||
class TrackChangesManager {
|
||||
constructor($scope, editor, element, adapter) {
|
||||
this._doneUpdateThisLoop = false
|
||||
this._pendingUpdates = false
|
||||
|
||||
this.onChangeSession = this.onChangeSession.bind(this)
|
||||
this.onChangeSelection = this.onChangeSelection.bind(this)
|
||||
this.onCut = this.onCut.bind(this)
|
||||
this.onPaste = this.onPaste.bind(this)
|
||||
this.onResize = this.onResize.bind(this)
|
||||
this.tearDown = this.tearDown.bind(this)
|
||||
|
||||
this.$scope = $scope
|
||||
this.editor = editor
|
||||
this.element = element
|
||||
this.adapter = adapter
|
||||
this._scrollTimeout = null
|
||||
this.changingSelection = false
|
||||
|
||||
if (window.trackChangesManager == null) {
|
||||
window.trackChangesManager = this
|
||||
}
|
||||
|
||||
this.$scope.$watch('trackChanges', track_changes => {
|
||||
if (track_changes == null) {
|
||||
return
|
||||
}
|
||||
this.setTrackChanges(track_changes)
|
||||
})
|
||||
|
||||
this.$scope.$watch('sharejsDoc', (doc, oldDoc) => {
|
||||
if (doc == null) {
|
||||
return
|
||||
}
|
||||
if (oldDoc != null) {
|
||||
this.disconnectFromDoc(oldDoc)
|
||||
}
|
||||
this.setTrackChanges(this.$scope.trackChanges)
|
||||
this.connectToDoc(doc)
|
||||
})
|
||||
|
||||
this.$scope.$on('comment:add', (e, thread_id, offset, length) => {
|
||||
this.addCommentToSelection(thread_id, offset, length)
|
||||
})
|
||||
|
||||
this.$scope.$on('comment:select_line', e => {
|
||||
this.selectLineIfNoSelection()
|
||||
})
|
||||
|
||||
this.$scope.$on('changes:accept', (e, change_ids) => {
|
||||
this.acceptChangeIds(change_ids)
|
||||
})
|
||||
|
||||
this.$scope.$on('changes:reject', (e, change_ids) => {
|
||||
this.rejectChangeIds(change_ids)
|
||||
})
|
||||
|
||||
this.$scope.$on('comment:remove', (e, comment_id) => {
|
||||
this.removeCommentId(comment_id)
|
||||
})
|
||||
|
||||
this.$scope.$on('comment:resolve_threads', (e, thread_ids) => {
|
||||
this.hideCommentsByThreadIds(thread_ids)
|
||||
})
|
||||
|
||||
this.$scope.$on('comment:unresolve_thread', (e, thread_id) => {
|
||||
this.showCommentByThreadId(thread_id)
|
||||
})
|
||||
|
||||
this.$scope.$on('review-panel:recalculate-screen-positions', () => {
|
||||
this.recalculateReviewEntriesScreenPositions()
|
||||
})
|
||||
|
||||
this._resetCutState()
|
||||
}
|
||||
|
||||
onChangeSession(e) {
|
||||
this.clearAnnotations()
|
||||
this.redrawAnnotations()
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.session.on('changeScrollTop', this.onChangeScroll.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
onChangeScroll() {
|
||||
if (this._scrollTimeout == null) {
|
||||
return (this._scrollTimeout = setTimeout(() => {
|
||||
this.recalculateVisibleEntries()
|
||||
this.$scope.$apply()
|
||||
return (this._scrollTimeout = null)
|
||||
}, 200))
|
||||
}
|
||||
}
|
||||
|
||||
onChangeSelection() {
|
||||
// Deletes can send about 5 changeSelection events, so
|
||||
// just act on the last one.
|
||||
if (!this.changingSelection) {
|
||||
this.changingSelection = true
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.changingSelection = false
|
||||
return this.updateFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onResize() {
|
||||
this.recalculateReviewEntriesScreenPositions()
|
||||
}
|
||||
|
||||
connectToDoc(doc) {
|
||||
this.rangesTracker = doc.ranges
|
||||
this.setTrackChanges(this.$scope.trackChanges)
|
||||
|
||||
doc.on('ranges:dirty', () => {
|
||||
this.updateAnnotations()
|
||||
})
|
||||
doc.on('ranges:clear', () => {
|
||||
this.clearAnnotations()
|
||||
})
|
||||
doc.on('ranges:redraw', () => {
|
||||
this.redrawAnnotations()
|
||||
})
|
||||
}
|
||||
|
||||
disconnectFromDoc(doc) {
|
||||
doc.off('ranges:clear')
|
||||
doc.off('ranges:redraw')
|
||||
doc.off('ranges:dirty')
|
||||
}
|
||||
|
||||
tearDown() {
|
||||
this.adapter.tearDown()
|
||||
}
|
||||
|
||||
setTrackChanges(value) {
|
||||
if (value) {
|
||||
if (this.$scope.sharejsDoc != null) {
|
||||
this.$scope.sharejsDoc.track_changes_as = window.user.id || 'anonymous'
|
||||
}
|
||||
} else {
|
||||
if (this.$scope.sharejsDoc != null) {
|
||||
this.$scope.sharejsDoc.track_changes_as = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearAnnotations() {
|
||||
this.adapter.clearAnnotations()
|
||||
}
|
||||
|
||||
redrawAnnotations() {
|
||||
for (const change of Array.from(this.rangesTracker.changes)) {
|
||||
if (change.op.i != null) {
|
||||
this.adapter.onInsertAdded(change)
|
||||
} else if (change.op.d != null) {
|
||||
this.adapter.onDeleteAdded(change)
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(this.rangesTracker.comments).forEach(comment => {
|
||||
if (!this.isCommentResolved(comment)) {
|
||||
this.adapter.onCommentAdded(comment)
|
||||
}
|
||||
})
|
||||
|
||||
this.broadcastChange()
|
||||
}
|
||||
|
||||
updateAnnotations() {
|
||||
// Doc updates with multiple ops, like search/replace or block comments
|
||||
// will call this with every individual op in a single event loop. So only
|
||||
// do the first this loop, then schedule an update for the next loop for
|
||||
// the rest.
|
||||
if (!this._doneUpdateThisLoop) {
|
||||
this._doUpdateAnnotations()
|
||||
this._doneUpdateThisLoop = true
|
||||
return setTimeout(() => {
|
||||
if (this._pendingUpdates) {
|
||||
this._doUpdateAnnotations()
|
||||
}
|
||||
this._doneUpdateThisLoop = false
|
||||
this._pendingUpdates = false
|
||||
})
|
||||
} else {
|
||||
this._pendingUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
_doUpdateAnnotations() {
|
||||
let change, comment
|
||||
const dirty = this.rangesTracker.getDirtyState()
|
||||
|
||||
let updateMarkers = false
|
||||
|
||||
for (const id in dirty.change.added) {
|
||||
change = dirty.change.added[id]
|
||||
if (change.op.i != null) {
|
||||
this.adapter.onInsertAdded(change)
|
||||
} else if (change.op.d != null) {
|
||||
this.adapter.onDeleteAdded(change)
|
||||
}
|
||||
}
|
||||
for (const id in dirty.change.removed) {
|
||||
change = dirty.change.removed[id]
|
||||
if (change.op.i != null) {
|
||||
this.adapter.onInsertRemoved(change)
|
||||
} else if (change.op.d != null) {
|
||||
this.adapter.onDeleteRemoved(change)
|
||||
}
|
||||
}
|
||||
for (const id in dirty.change.moved) {
|
||||
change = dirty.change.moved[id]
|
||||
updateMarkers = true
|
||||
this.adapter.onChangeMoved(change)
|
||||
}
|
||||
|
||||
for (const id in dirty.comment.added) {
|
||||
comment = dirty.comment.added[id]
|
||||
if (!this.isCommentResolved(comment)) {
|
||||
this.adapter.onCommentAdded(comment)
|
||||
}
|
||||
}
|
||||
for (const id in dirty.comment.removed) {
|
||||
comment = dirty.comment.removed[id]
|
||||
if (!this.isCommentResolved(comment)) {
|
||||
this.adapter.onCommentRemoved(comment)
|
||||
}
|
||||
}
|
||||
for (const id in dirty.comment.moved) {
|
||||
comment = dirty.comment.moved[id]
|
||||
if (this.adapter.onCommentMoved && !this.isCommentResolved(comment)) {
|
||||
updateMarkers = true
|
||||
this.adapter.onCommentMoved(comment)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For now, if not using ACE don't worry about the markers
|
||||
*/
|
||||
if (!this.editor) {
|
||||
updateMarkers = false
|
||||
}
|
||||
|
||||
this.rangesTracker.resetDirtyState()
|
||||
if (updateMarkers) {
|
||||
this.editor.renderer.updateBackMarkers()
|
||||
}
|
||||
this.broadcastChange()
|
||||
}
|
||||
|
||||
addComment(offset, content, thread_id) {
|
||||
const op = { c: content, p: offset, t: thread_id }
|
||||
// @rangesTracker.applyOp op # Will apply via sharejs
|
||||
this.$scope.sharejsDoc.submitOp(op)
|
||||
}
|
||||
|
||||
addCommentToSelection(thread_id, offset, length) {
|
||||
const start = this.adapter.shareJsOffsetToRowColumn(offset)
|
||||
const end = this.adapter.shareJsOffsetToRowColumn(offset + length)
|
||||
const range = new Range(start.row, start.column, end.row, end.column)
|
||||
const content = this.editor.session.getTextRange(range)
|
||||
this.addComment(offset, content, thread_id)
|
||||
}
|
||||
|
||||
isCommentResolved(comment) {
|
||||
return this.rangesTracker.resolvedThreadIds[comment.op.t]
|
||||
}
|
||||
|
||||
selectLineIfNoSelection() {
|
||||
if (this.editor.selection.isEmpty()) {
|
||||
this.editor.selection.selectLine()
|
||||
}
|
||||
}
|
||||
|
||||
acceptChangeIds(change_ids) {
|
||||
this.rangesTracker.removeChangeIds(change_ids)
|
||||
this.updateAnnotations()
|
||||
this.updateFocus()
|
||||
}
|
||||
|
||||
rejectChangeIds(change_ids) {
|
||||
const changes = this.rangesTracker.getChanges(change_ids)
|
||||
if (changes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// When doing bulk rejections, adjacent changes might interact with each other.
|
||||
// Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
|
||||
//
|
||||
// "foo bar baz" -> "foo quux baz"
|
||||
//
|
||||
// The change above will be modeled with two ops, with the insertion going first:
|
||||
//
|
||||
// foo quux baz
|
||||
// |--| -> insertion of "quux", op 1, at position 4
|
||||
// | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
|
||||
//
|
||||
// When rejecting these changes at once, if the insertion is rejected first, we get unexpected
|
||||
// results. What happens is:
|
||||
//
|
||||
// 1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
||||
// starting from position 4;
|
||||
//
|
||||
// "foo quux baz" -> "foo baz"
|
||||
// |--| -> 4 characters to be removed
|
||||
//
|
||||
// 2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
|
||||
// the word "quuux" was still present).
|
||||
//
|
||||
// "foo baz" -> "foo bazbar"
|
||||
// | -> deletion of "bar" is reverted by reinserting "bar" at position 8
|
||||
//
|
||||
// While the intended result would be "foo bar baz", what we get is:
|
||||
//
|
||||
// "foo bazbar" (note "bar" readded at position 8)
|
||||
//
|
||||
// The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
|
||||
// from position 4. This includes the position where the deletion exists; when that position is
|
||||
// cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
|
||||
// As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
|
||||
// does so at the outdated position (position 8, which was valid when "quux" was present).
|
||||
//
|
||||
// To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
|
||||
// subsequent operations that come after. Reverse sorting the operations based on position will
|
||||
// achieve it; in the case above, it makes sure that the the deletion is reverted first:
|
||||
//
|
||||
// 1) Rejecting the deletion adds the deleted word "bar" at position 8
|
||||
//
|
||||
// "foo quux baz" -> "foo quuxbar baz"
|
||||
// | -> deletion of "bar" is reverted by
|
||||
// reinserting "bar" at position 8
|
||||
//
|
||||
// 2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
||||
// starting from position 4 and achieves the expected result:
|
||||
//
|
||||
// "foo quuxbar baz" -> "foo bar baz"
|
||||
// |--| -> 4 characters to be removed
|
||||
|
||||
changes.sort((a, b) => b.op.p - a.op.p)
|
||||
|
||||
const session = this.editor.getSession()
|
||||
for (const change of Array.from(changes)) {
|
||||
if (change.op.d != null) {
|
||||
const content = change.op.d
|
||||
const position = this.adapter.shareJsOffsetToRowColumn(change.op.p)
|
||||
session.$fromReject = true // Tell track changes to cancel out delete
|
||||
session.insert(position, content)
|
||||
session.$fromReject = false
|
||||
} else if (change.op.i != null) {
|
||||
const start = this.adapter.shareJsOffsetToRowColumn(change.op.p)
|
||||
const end = this.adapter.shareJsOffsetToRowColumn(
|
||||
change.op.p + change.op.i.length
|
||||
)
|
||||
const editor_text = session.getDocument().getTextRange({ start, end })
|
||||
if (editor_text !== change.op.i) {
|
||||
throw new Error(
|
||||
`Op to be removed (${JSON.stringify(
|
||||
change.op
|
||||
)}), does not match editor text, '${editor_text}'`
|
||||
)
|
||||
}
|
||||
session.$fromReject = true
|
||||
session.remove({ start, end })
|
||||
session.$fromReject = false
|
||||
} else {
|
||||
throw new Error(`unknown change: ${JSON.stringify(change)}`)
|
||||
}
|
||||
}
|
||||
setTimeout(() => this.updateFocus())
|
||||
}
|
||||
|
||||
removeCommentId(comment_id) {
|
||||
this.rangesTracker.removeCommentId(comment_id)
|
||||
return this.updateAnnotations()
|
||||
}
|
||||
|
||||
hideCommentsByThreadIds(thread_ids) {
|
||||
const resolve_ids = {}
|
||||
const comments = this.rangesTracker.comments || []
|
||||
for (const id of Array.from(thread_ids)) {
|
||||
resolve_ids[id] = true
|
||||
}
|
||||
|
||||
for (const comment of comments) {
|
||||
if (resolve_ids[comment.op.t]) {
|
||||
this.adapter.onCommentRemoved(comment)
|
||||
}
|
||||
}
|
||||
return this.broadcastChange()
|
||||
}
|
||||
|
||||
showCommentByThreadId(thread_id) {
|
||||
const comments = this.rangesTracker.comments || []
|
||||
for (const comment of comments) {
|
||||
if (comment.op.t === thread_id && !this.isCommentResolved(comment)) {
|
||||
this.adapter.onCommentAdded(comment)
|
||||
}
|
||||
}
|
||||
return this.broadcastChange()
|
||||
}
|
||||
|
||||
_resetCutState() {
|
||||
return (this._cutState = {
|
||||
text: null,
|
||||
comments: [],
|
||||
docId: null,
|
||||
})
|
||||
}
|
||||
|
||||
onCut() {
|
||||
this._resetCutState()
|
||||
const selection = this.editor.getSelectionRange()
|
||||
const selection_start = this._rangeToShareJs(selection.start)
|
||||
const selection_end = this._rangeToShareJs(selection.end)
|
||||
this._cutState.text = this.editor.getSelectedText()
|
||||
this._cutState.docId = this.$scope.docId
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const comment of Array.from(this.rangesTracker.comments)) {
|
||||
const comment_start = comment.op.p
|
||||
const comment_end = comment_start + comment.op.c.length
|
||||
if (selection_start <= comment_start && comment_end <= selection_end) {
|
||||
result.push(
|
||||
this._cutState.comments.push({
|
||||
offset: comment.op.p - selection_start,
|
||||
text: comment.op.c,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
result.push(undefined)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
onPaste() {
|
||||
this.editor.once('change', change => {
|
||||
if (change.action !== 'insert') {
|
||||
return
|
||||
}
|
||||
const pasted_text = change.lines.join('\n')
|
||||
const paste_offset = this._rangeToShareJs(change.start)
|
||||
// We have to wait until the change has been processed by the range
|
||||
// tracker, since if we move the ops into place beforehand, they will be
|
||||
// moved again when the changes are processed by the range tracker. This
|
||||
// ranges:dirty event is fired after the doc has applied the changes to
|
||||
// the range tracker.
|
||||
this.$scope.sharejsDoc.on('ranges:dirty.paste', () => {
|
||||
// Doc event emitter uses namespaced events
|
||||
this.$scope.sharejsDoc.off('ranges:dirty.paste')
|
||||
if (
|
||||
pasted_text === this._cutState.text &&
|
||||
this.$scope.docId === this._cutState.docId
|
||||
) {
|
||||
for (const { comment, offset, text } of Array.from(
|
||||
this._cutState.comments
|
||||
)) {
|
||||
const op = { c: text, p: paste_offset + offset, t: comment.id }
|
||||
this.$scope.sharejsDoc.submitOp(op)
|
||||
} // Resubmitting an existing comment op (by thread id) will move it
|
||||
}
|
||||
this._resetCutState()
|
||||
// Check that comments still match text. Will throw error if not.
|
||||
this.rangesTracker.validate(this.editor.getValue())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
checkMapping() {
|
||||
// TODO: reintroduce this check
|
||||
let background_marker_id, callout_marker_id, end, marker, op, start
|
||||
const session = this.editor.getSession()
|
||||
|
||||
// Make a copy of session.getMarkers() so we can modify it
|
||||
const markers = {}
|
||||
const object = session.getMarkers()
|
||||
for (const marker_id in object) {
|
||||
marker = object[marker_id]
|
||||
markers[marker_id] = marker
|
||||
}
|
||||
|
||||
const expected_markers = []
|
||||
for (const change of Array.from(this.rangesTracker.changes)) {
|
||||
if (this.adapter.changeIdToMarkerIdMap[change.id] != null) {
|
||||
;({ op } = change)
|
||||
;({ background_marker_id, callout_marker_id } =
|
||||
this.adapter.changeIdToMarkerIdMap[change.id])
|
||||
start = this.adapter.shareJsOffsetToRowColumn(op.p)
|
||||
if (op.i != null) {
|
||||
end = this.adapter.shareJsOffsetToRowColumn(op.p + op.i.length)
|
||||
} else if (op.d != null) {
|
||||
end = start
|
||||
}
|
||||
expected_markers.push({
|
||||
marker_id: background_marker_id,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
expected_markers.push({
|
||||
marker_id: callout_marker_id,
|
||||
start,
|
||||
end: start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const comment of Array.from(this.rangesTracker.comments)) {
|
||||
if (this.adapter.changeIdToMarkerIdMap[comment.id] != null) {
|
||||
;({ background_marker_id, callout_marker_id } =
|
||||
this.adapter.changeIdToMarkerIdMap[comment.id])
|
||||
start = this.adapter.shareJsOffsetToRowColumn(comment.op.p)
|
||||
end = this.adapter.shareJsOffsetToRowColumn(
|
||||
comment.op.p + comment.op.c.length
|
||||
)
|
||||
expected_markers.push({
|
||||
marker_id: background_marker_id,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
expected_markers.push({
|
||||
marker_id: callout_marker_id,
|
||||
start,
|
||||
end: start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const { marker_id, start, end } of Array.from(expected_markers)) {
|
||||
marker = markers[marker_id]
|
||||
delete markers[marker_id]
|
||||
if (
|
||||
marker.range.start.row !== start.row ||
|
||||
marker.range.start.column !== start.column ||
|
||||
marker.range.end.row !== end.row ||
|
||||
marker.range.end.column !== end.column
|
||||
) {
|
||||
debugConsole.error("Change doesn't match marker anymore", {
|
||||
marker,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const marker_id in markers) {
|
||||
marker = markers[marker_id]
|
||||
if (/track-changes/.test(marker.clazz)) {
|
||||
result.push(debugConsole.error('Orphaned ace marker', marker))
|
||||
} else {
|
||||
result.push(undefined)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
broadcastChange() {
|
||||
this.$scope.$emit('editor:track-changes:changed', this.$scope.docId)
|
||||
}
|
||||
|
||||
recalculateReviewEntriesScreenPositions() {
|
||||
if (!this.editor) {
|
||||
return
|
||||
}
|
||||
const session = this.editor.getSession()
|
||||
const { renderer } = this.editor
|
||||
const entries = this._getCurrentDocEntries()
|
||||
const object = entries || {}
|
||||
for (const entry_id in object) {
|
||||
const entry = object[entry_id]
|
||||
const doc_position = this.adapter.shareJsOffsetToRowColumn(entry.offset)
|
||||
const screen_position = session.documentToScreenPosition(
|
||||
doc_position.row,
|
||||
doc_position.column
|
||||
)
|
||||
const y = screen_position.row * renderer.lineHeight
|
||||
if (entry.screenPos == null) {
|
||||
entry.screenPos = {}
|
||||
}
|
||||
entry.screenPos.y = y
|
||||
entry.docPos = doc_position
|
||||
}
|
||||
this.recalculateVisibleEntries()
|
||||
this.$scope.$apply()
|
||||
}
|
||||
|
||||
recalculateVisibleEntries() {
|
||||
const OFFSCREEN_ROWS = 20
|
||||
|
||||
// With less than this number of entries, don't bother culling to avoid
|
||||
// little UI jumps when scrolling.
|
||||
const CULL_AFTER = 100
|
||||
|
||||
const { firstRow, lastRow } = this.editor.renderer.layerConfig
|
||||
const entries = this._getCurrentDocEntries() || {}
|
||||
const entriesLength = Object.keys(entries).length
|
||||
let changed = false
|
||||
for (const entry_id in entries) {
|
||||
const entry = entries[entry_id]
|
||||
const old = entry.visible
|
||||
entry.visible =
|
||||
entriesLength < CULL_AFTER ||
|
||||
(firstRow - OFFSCREEN_ROWS <= entry.docPos.row &&
|
||||
entry.docPos.row <= lastRow + OFFSCREEN_ROWS)
|
||||
if (entry.visible !== old) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.$scope.$emit('editor:track-changes:visibility_changed')
|
||||
}
|
||||
}
|
||||
|
||||
_getCurrentDocEntries() {
|
||||
const doc_id = this.$scope.docId
|
||||
const entries = this.$scope.reviewPanel.entries[doc_id] || {}
|
||||
return entries
|
||||
}
|
||||
|
||||
updateFocus() {
|
||||
if (this.editor) {
|
||||
const selection = this.editor.getSelectionRange()
|
||||
const selection_start = this._rangeToShareJs(selection.start)
|
||||
const selection_end = this._rangeToShareJs(selection.end)
|
||||
const is_selection = selection_start !== selection_end
|
||||
this.$scope.$emit(
|
||||
'editor:focus:changed',
|
||||
selection_start,
|
||||
selection_end,
|
||||
is_selection
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_rangeToShareJs(range) {
|
||||
const lines = this.editor.getSession().getDocument().getLines(0, range.row)
|
||||
return EditorShareJsCodec.rangeToShareJs(range, lines)
|
||||
}
|
||||
|
||||
_changeToShareJs(delta) {
|
||||
const lines = this.editor
|
||||
.getSession()
|
||||
.getDocument()
|
||||
.getLines(0, delta.start.row)
|
||||
return EditorShareJsCodec.changeToShareJs(delta, lines)
|
||||
}
|
||||
}
|
||||
export default TrackChangesManager
|
|
@ -1,30 +0,0 @@
|
|||
import 'ace/ace'
|
||||
const BuiltInUndoManager = ace.require('ace/undomanager').UndoManager
|
||||
|
||||
class UndoManager {
|
||||
constructor(editor) {
|
||||
this.editor = editor
|
||||
|
||||
this.onChangeSession = this.onChangeSession.bind(this)
|
||||
this.onChange = this.onChange.bind(this)
|
||||
}
|
||||
|
||||
onChangeSession(session) {
|
||||
session.setUndoManager(new BuiltInUndoManager())
|
||||
}
|
||||
|
||||
onChange(change) {
|
||||
if (change.origin !== 'remote') return
|
||||
|
||||
// HACK: remote changes in Ace are added by the ShareJS/Ace adapter
|
||||
// asynchronously via a timeout (see attach_ace function). This makes it
|
||||
// impossible to clear to undo stack when remote changes are received.
|
||||
// To hack around this we queue the undo stack clear so that it applies
|
||||
// after the change is applied
|
||||
setTimeout(() => {
|
||||
this.editor.getSession().getUndoManager().reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default UndoManager
|
|
@ -1,25 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.factory('files', [
|
||||
'ide',
|
||||
function (ide) {
|
||||
const Files = {
|
||||
getTeXFiles() {
|
||||
const texFiles = []
|
||||
ide.fileTreeManager.forEachEntity(function (entity, _folder, path) {
|
||||
if (
|
||||
entity.type === 'doc' &&
|
||||
/.*\.(tex|md|txt|tikz)/.test(entity.name)
|
||||
) {
|
||||
const cloned = _.clone(entity)
|
||||
cloned.path = path
|
||||
texFiles.push(cloned)
|
||||
}
|
||||
})
|
||||
return texFiles
|
||||
},
|
||||
}
|
||||
return Files
|
||||
},
|
||||
])
|
|
@ -1,54 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.factory('graphics', [
|
||||
'ide',
|
||||
function (ide) {
|
||||
const Graphics = {
|
||||
getGraphicsFiles() {
|
||||
const graphicsFiles = []
|
||||
ide.fileTreeManager.forEachEntity(function (entity, folder, path) {
|
||||
if (
|
||||
entity.type === 'file' &&
|
||||
__guardMethod__(
|
||||
entity != null ? entity.name : undefined,
|
||||
'match',
|
||||
o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i)
|
||||
)
|
||||
) {
|
||||
const cloned = _.clone(entity)
|
||||
cloned.path = path
|
||||
return graphicsFiles.push(cloned)
|
||||
}
|
||||
})
|
||||
return graphicsFiles
|
||||
},
|
||||
}
|
||||
|
||||
return Graphics
|
||||
},
|
||||
])
|
||||
|
||||
function __guardMethod__(obj, methodName, transform) {
|
||||
if (
|
||||
typeof obj !== 'undefined' &&
|
||||
obj !== null &&
|
||||
typeof obj[methodName] === 'function'
|
||||
) {
|
||||
return transform(obj, methodName)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.factory('preamble', function (ide) {
|
||||
const Preamble = {
|
||||
getPreambleText() {
|
||||
const text = ide.editorManager.getCurrentDocValue().slice(0, 5000)
|
||||
const preamble =
|
||||
__guard__(text.match(/([^]*)^\\begin\{document\}/m), x => x[1]) || ''
|
||||
return preamble
|
||||
},
|
||||
|
||||
getGraphicsPaths() {
|
||||
let match
|
||||
const preamble = Preamble.getPreambleText()
|
||||
const graphicsPathsArgs =
|
||||
__guard__(preamble.match(/\\graphicspath\{(.*)\}/), x => x[1]) || ''
|
||||
const paths = []
|
||||
const re = /\{([^}]*)\}/g
|
||||
while ((match = re.exec(graphicsPathsArgs))) {
|
||||
paths.push(match[1])
|
||||
}
|
||||
return paths
|
||||
},
|
||||
}
|
||||
|
||||
return Preamble
|
||||
})
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -280,26 +280,6 @@ export default App.controller('ReviewPanelController', [
|
|||
return rangesTrackers[doc_id]
|
||||
}
|
||||
|
||||
let scrollbar = {}
|
||||
ide.$scope.reviewPanelEventsBridge.on(
|
||||
'aceScrollbarVisibilityChanged',
|
||||
function (isVisible, scrollbarWidth) {
|
||||
scrollbar = { isVisible, scrollbarWidth }
|
||||
return updateScrollbar()
|
||||
}
|
||||
)
|
||||
|
||||
function updateScrollbar() {
|
||||
if (
|
||||
scrollbar.isVisible &&
|
||||
ide.$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE
|
||||
) {
|
||||
return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`)
|
||||
} else {
|
||||
return $reviewPanelEl.css('right', '0')
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch(
|
||||
'!ui.reviewPanelOpen && reviewPanel.hasEntries',
|
||||
function (open, prevVal) {
|
||||
|
@ -335,7 +315,6 @@ export default App.controller('ReviewPanelController', [
|
|||
if (view == null) {
|
||||
return
|
||||
}
|
||||
updateScrollbar()
|
||||
if (view === $scope.SubViews.OVERVIEW) {
|
||||
return refreshOverviewPanel()
|
||||
} else if (oldView === $scope.SubViews.OVERVIEW) {
|
||||
|
|
|
@ -249,13 +249,11 @@ export default App.directive('reviewPanelSorted', function () {
|
|||
// mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into
|
||||
// scroll events ourselves, then it makes the review panel slightly less smooth (barely)
|
||||
// noticeable, but keeps it perfectly in step with Ace.
|
||||
ace
|
||||
.require('ace/lib/event')
|
||||
.addMouseWheelListener(scroller[0], function (e) {
|
||||
scroller[0].addEventListener('wheel', e => {
|
||||
// FIXME (or remove this): https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
|
||||
const deltaY = e.wheelY
|
||||
const old_top = parseInt(list.css('top'))
|
||||
const top = old_top - deltaY * 4
|
||||
scrollAce(-top)
|
||||
dispatchScrollEvent(deltaY * 4)
|
||||
return e.preventDefault()
|
||||
})
|
||||
|
@ -274,14 +272,6 @@ export default App.directive('reviewPanelSorted', function () {
|
|||
}
|
||||
}
|
||||
|
||||
const scrollAce = scrollTop =>
|
||||
scope.reviewPanelEventsBridge.emit('externalScroll', scrollTop)
|
||||
|
||||
scope.reviewPanelEventsBridge.on('aceScroll', scrollPanel)
|
||||
scope.$on('$destroy', () =>
|
||||
scope.reviewPanelEventsBridge.off('aceScroll')
|
||||
)
|
||||
|
||||
// receive the scroll position from the CodeMirror 6 track changes extension
|
||||
window.addEventListener('editor:scroll', event => {
|
||||
const { scrollTop, height, paddingTop } = event.detail
|
||||
|
|
|
@ -10,12 +10,5 @@ App.controller('EditorLoaderController', [
|
|||
val === true ? 'rich-text' : 'source'
|
||||
)
|
||||
})
|
||||
|
||||
$scope.$watch('editor.newSourceEditor', function (val) {
|
||||
localStorage(
|
||||
`editor.source_editor.${$scope.project_id}`,
|
||||
val === true ? 'cm6' : 'ace'
|
||||
)
|
||||
})
|
||||
},
|
||||
])
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import App from '../../base'
|
||||
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
|
||||
|
||||
const eModules = importOverleafModules('editorToolbarButtons')
|
||||
const editorToolbarButtons = eModules.map(item => item.import.default)
|
||||
|
||||
export default App.controller('EditorToolbarController', [
|
||||
'$scope',
|
||||
'ide',
|
||||
function ($scope, ide) {
|
||||
const editorButtons = []
|
||||
|
||||
for (const editorToolbarButton of editorToolbarButtons) {
|
||||
const button = editorToolbarButton.button($scope, ide)
|
||||
|
||||
if (editorToolbarButton.source) {
|
||||
editorButtons.push(button)
|
||||
}
|
||||
}
|
||||
|
||||
$scope.editorButtons = editorButtons
|
||||
},
|
||||
])
|
|
@ -1,2 +1 @@
|
|||
import './EditorLoaderController'
|
||||
import './EditorToolbarController'
|
||||
|
|
173
services/web/frontend/js/vendor/libs/sharejs.js
vendored
173
services/web/frontend/js/vendor/libs/sharejs.js
vendored
|
@ -26,7 +26,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
|
|||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
define(['ace/ace','crypto-js/sha1', '@/utils/debugging'], function (_ignore, CryptoJSSHA1, { debugging, debugConsole }) {
|
||||
define(['crypto-js/sha1', '@/utils/debugging'], function (CryptoJSSHA1, { debugging, debugConsole }) {
|
||||
var append = void 0,
|
||||
bootstrapTransform = void 0,
|
||||
exports = void 0,
|
||||
|
@ -1410,177 +1410,6 @@ define(['ace/ace','crypto-js/sha1', '@/utils/debugging'], function (_ignore, Cry
|
|||
MicroEvent.mixin(Doc);
|
||||
|
||||
exports.Doc = Doc;
|
||||
// This is some utility code to connect an ace editor to a sharejs document.
|
||||
|
||||
var _ace$require = ace.require('ace/range'),
|
||||
Range = _ace$require.Range;
|
||||
|
||||
// Convert an ace delta into an op understood by share.js
|
||||
|
||||
|
||||
var applyAceToShareJS = function applyAceToShareJS(editorDoc, delta, doc, fromUndo) {
|
||||
// Get the start position of the range, in no. of characters
|
||||
var getStartOffsetPosition = function getStartOffsetPosition(start) {
|
||||
// This is quite inefficient - getLines makes a copy of the entire
|
||||
// lines array in the document. It would be nice if we could just
|
||||
// access them directly.
|
||||
var lines = editorDoc.getLines(0, start.row);
|
||||
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
offset += i < start.row ? line.length : start.column;
|
||||
}
|
||||
|
||||
// Add the row number to include newlines.
|
||||
return offset + start.row;
|
||||
};
|
||||
|
||||
var pos = getStartOffsetPosition(delta.start);
|
||||
|
||||
// NOTE: Keep in sync with EditorWatchdogManager.
|
||||
switch (delta.action) {
|
||||
case 'insert':
|
||||
text = delta.lines.join('\n');
|
||||
doc.insert(pos, text, fromUndo);
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
text = delta.lines.join('\n');
|
||||
doc.del(pos, text.length, fromUndo);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('unknown action: ' + delta.action);
|
||||
}
|
||||
};
|
||||
|
||||
// Attach an ace editor to the document. The editor's contents are replaced
|
||||
// with the document's contents.
|
||||
window.sharejs.extendDoc('attach_ace', function (editor, maxDocLength) {
|
||||
if (!this.provides['text']) {
|
||||
throw new Error('Only text documents can be attached to ace');
|
||||
}
|
||||
|
||||
var doc = this;
|
||||
var editorDoc = editor.getSession().getDocument();
|
||||
editorDoc.setNewLineMode('unix');
|
||||
|
||||
function check() {
|
||||
return window.setTimeout(function () {
|
||||
var editorText = editorDoc.getValue();
|
||||
var otText = doc.getText();
|
||||
|
||||
if (editorText !== otText) {
|
||||
doc.emit('error','Text does not match in ace')
|
||||
debugConsole.error('Text does not match!');
|
||||
debugConsole.error('editor: ' + editorText);
|
||||
debugConsole.error('ot: ' + otText);
|
||||
}
|
||||
}
|
||||
// Should probably also replace the editor text with the doc snapshot.
|
||||
, 0);
|
||||
};
|
||||
|
||||
onDelete(0, editorDoc.getValue());
|
||||
onInsert(0, doc.getText());
|
||||
|
||||
check();
|
||||
|
||||
// Listen for edits in ace
|
||||
function editorListener(change) {
|
||||
if (change.origin === 'remote') {
|
||||
// this change has been injected via sharejs
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxDocLength != null && editorDoc.getValue().length >= maxDocLength) {
|
||||
doc.emit('error', new Error('document length is greater than maxDocLength'));
|
||||
return;
|
||||
}
|
||||
|
||||
var fromUndo = !!(editor.getSession().$fromUndo || editor.getSession().$fromReject);
|
||||
|
||||
applyAceToShareJS(editorDoc, change, doc, fromUndo);
|
||||
|
||||
return check();
|
||||
};
|
||||
|
||||
editorDoc.on('change', editorListener);
|
||||
|
||||
// Horribly inefficient.
|
||||
function offsetToPos(offset) {
|
||||
// Again, very inefficient.
|
||||
var lines = editorDoc.getAllLines();
|
||||
|
||||
var row = 0;
|
||||
for (row = 0; row < lines.length; row++) {
|
||||
var line = lines[row];
|
||||
if (offset <= line.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// +1 for the newline.
|
||||
offset -= lines[row].length + 1;
|
||||
}
|
||||
|
||||
return { row: row, column: offset };
|
||||
};
|
||||
|
||||
// We want to insert the flag `origin: 'remote'` into the delta if the op
|
||||
// is the initial document write or comes from the underlying sharejs doc
|
||||
// (which means it is from a remote op), so we have to do the work of
|
||||
// editorDoc.insert and editorDoc.remove manually.
|
||||
// These methods are copied from ace.js doc#insert and #remove, and then
|
||||
// inject the `origin: 'remote'` flag into the delta.
|
||||
function onInsert(pos, text) {
|
||||
if (editorDoc.getLength() <= 1) {
|
||||
editorDoc.$detectNewLine(text);
|
||||
}
|
||||
|
||||
var lines = editorDoc.$split(text);
|
||||
var position = offsetToPos(pos);
|
||||
var start = editorDoc.clippedPos(position.row, position.column);
|
||||
var end = {
|
||||
row: start.row + lines.length - 1,
|
||||
column: (lines.length === 1 ? start.column : 0) + lines[lines.length - 1].length
|
||||
};
|
||||
|
||||
editorDoc.applyDelta({
|
||||
start: start,
|
||||
end: end,
|
||||
action: 'insert',
|
||||
lines: lines,
|
||||
origin: 'remote'
|
||||
});
|
||||
return check();
|
||||
};
|
||||
|
||||
function onDelete(pos, text) {
|
||||
var range = Range.fromPoints(offsetToPos(pos), offsetToPos(pos + text.length));
|
||||
var start = editorDoc.clippedPos(range.start.row, range.start.column);
|
||||
var end = editorDoc.clippedPos(range.end.row, range.end.column);
|
||||
editorDoc.applyDelta({
|
||||
start: start,
|
||||
end: end,
|
||||
action: 'remove',
|
||||
lines: editorDoc.getLinesForRange({ start: start, end: end }),
|
||||
origin: 'remote'
|
||||
});
|
||||
return check();
|
||||
};
|
||||
|
||||
doc.on('insert', onInsert);
|
||||
doc.on('delete', onDelete);
|
||||
|
||||
doc.detach_ace = function () {
|
||||
doc.removeListener('insert', onInsert);
|
||||
doc.removeListener('delete', onDelete);
|
||||
editorDoc.removeListener('change', editorListener);
|
||||
return delete doc.detach_ace;
|
||||
};
|
||||
});
|
||||
|
||||
return window.sharejs;
|
||||
});
|
||||
|
|
|
@ -92,7 +92,6 @@ const initialize = () => {
|
|||
toggleHistory: () => {},
|
||||
editor: {
|
||||
richText: false,
|
||||
newSourceEditor: false,
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import EditorSwitch from '../js/features/source-editor/components/editor-switch-legacy'
|
||||
import EditorSwitch from '../js/features/source-editor/components/editor-switch'
|
||||
import { ScopeDecorator } from './decorators/scope'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -93,13 +93,6 @@
|
|||
.full-size;
|
||||
}
|
||||
|
||||
.editor-container #editor {
|
||||
top: @editor-toolbar-height;
|
||||
}
|
||||
.editor-container.has-source-toolbar #editor {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.pdf-empty,
|
||||
.no-history-available,
|
||||
.no-file-selection,
|
||||
|
@ -276,104 +269,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**************************************
|
||||
Ace
|
||||
***************************************/
|
||||
|
||||
// The internal components of the aceEditor directive
|
||||
.ace-editor-wrapper {
|
||||
.full-size;
|
||||
.undo-conflict-warning {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.ace-editor-body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.spelling-highlight {
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
background-image: url(../../../public/img/spellcheck-underline.png);
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
background-image: url(../../../public/img/spellcheck-underline@2x.png);
|
||||
background-size: 5px 4px;
|
||||
}
|
||||
background-repeat: repeat-x;
|
||||
background-position: bottom left;
|
||||
}
|
||||
.remote-cursor {
|
||||
position: absolute;
|
||||
border-left: 2px solid transparent;
|
||||
// Adds "nubbin" top right of cursor, which inherits the injected color
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: -5px;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-top-width: 3px;
|
||||
border-right-width: 3px;
|
||||
border-bottom-width: 2px;
|
||||
border-left-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: inherit;
|
||||
}
|
||||
}
|
||||
.annotation-label {
|
||||
padding: (@line-height-computed / 4) (@line-height-computed / 2);
|
||||
font-size: 0.8rem;
|
||||
z-index: 100;
|
||||
font-family: @font-family-sans-serif;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.annotation {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
.highlights-before-label,
|
||||
.highlights-after-label {
|
||||
position: absolute;
|
||||
right: @line-height-computed;
|
||||
z-index: 1;
|
||||
}
|
||||
.highlights-before-label {
|
||||
top: @line-height-computed / 2;
|
||||
}
|
||||
.highlights-after-label {
|
||||
bottom: @line-height-computed / 2;
|
||||
}
|
||||
}
|
||||
|
||||
.strike-through-foreground::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
margin-top: -1px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
// Hack to solve an issue where scrollbars aren't visible in Safari.
|
||||
// Safari seems to clip part of the scrollbar element. By giving the
|
||||
// element a background, we're telling Safari that it *really* needs to
|
||||
// paint the whole area. See https://github.com/ajaxorg/ace/issues/2872
|
||||
.ace_scrollbar-inner {
|
||||
background-color: #fff;
|
||||
opacity: 0.01;
|
||||
|
||||
.ace_dark & {
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************
|
||||
CodeMirror
|
||||
***************************************/
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
background-color: @rp-bg-blue;
|
||||
|
@ -90,10 +90,6 @@
|
|||
font-size: @rp-base-font-size;
|
||||
color: @rp-type-blue;
|
||||
z-index: 6;
|
||||
|
||||
.has-source-toolbar & {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-panel {
|
||||
|
@ -1128,10 +1124,6 @@ button when (@is-overleaf-light = true) {
|
|||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.has-source-toolbar & {
|
||||
top: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.rp-track-changes-indicator {
|
||||
|
@ -1376,8 +1368,4 @@ button when (@is-overleaf-light = true) {
|
|||
.rp-track-changes-indicator {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.has-source-toolbar & {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,7 +239,6 @@
|
|||
"@uppy/utils": "^4.0.7",
|
||||
"@uppy/xhr-upload": "^1.6.8",
|
||||
"abort-controller": "^3.0.0",
|
||||
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
|
||||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"algoliasearch": "^3.35.1",
|
||||
|
|
|
@ -10,7 +10,6 @@ type Scope = {
|
|||
doc_id?: string
|
||||
getSnapshot?: () => string
|
||||
}
|
||||
newSourceEditor?: boolean
|
||||
}
|
||||
hasLintingError?: boolean
|
||||
ui?: {
|
||||
|
@ -45,7 +44,6 @@ export const mockScope = (scope?: Scope) => ({
|
|||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
},
|
||||
newSourceEditor: true,
|
||||
},
|
||||
hasLintingError: false,
|
||||
ui: {
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('<HelpShowHotkeys />', function () {
|
|||
expect(screen.queryByRole('dialog')).to.equal(null)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show Hotkeys' }))
|
||||
const modal = screen.getAllByRole('dialog')[0]
|
||||
within(modal).getByText('Hotkeys (Legacy source editor)')
|
||||
within(modal).getByText('Common')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -11,25 +11,13 @@ const modalProps = {
|
|||
|
||||
describe('<HotkeysModal />', function () {
|
||||
it('renders the translated modal title on cm6', async function () {
|
||||
const { baseElement } = render(
|
||||
<HotkeysModal {...modalProps} newSourceEditor />
|
||||
)
|
||||
const { baseElement } = render(<HotkeysModal {...modalProps} />)
|
||||
|
||||
expect(baseElement.querySelector('.modal-title').textContent).to.equal(
|
||||
'Hotkeys (Source editor)'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the translated modal title on ace', async function () {
|
||||
const { baseElement } = render(
|
||||
<HotkeysModal {...modalProps} newSourceEditor={false} />
|
||||
)
|
||||
|
||||
expect(baseElement.querySelector('.modal-title').textContent).to.equal(
|
||||
'Hotkeys (Legacy source editor)'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders translated heading with embedded code', function () {
|
||||
const { baseElement } = render(<HotkeysModal {...modalProps} />)
|
||||
|
||||
|
|
|
@ -1,204 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
|
||||
import {
|
||||
matchOutline,
|
||||
nestOutline,
|
||||
} from '../../../../frontend/js/features/outline/outline-parser'
|
||||
|
||||
describe('OutlineParser', function () {
|
||||
describe('matchOutline', function () {
|
||||
it('matches all levels', function () {
|
||||
const content = `
|
||||
\\book{Book}
|
||||
\\part{Part}
|
||||
\\addpart{Part 2}
|
||||
\\chapter{Chapter}
|
||||
\\addchap{Chapter 2}
|
||||
\\section{Section 1}
|
||||
\\addsec{Section 1b}
|
||||
\\subsection{Subsection}
|
||||
\\subsubsection{Subsubsection}
|
||||
\\section{Section 2}
|
||||
\\subsubsection{Subsubsection without subsection}
|
||||
\\paragraph{a paragraph} Here is some text.
|
||||
\\subparagraph{a subparagraph} Here is some more text.
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 2, title: 'Book', level: 10 },
|
||||
{ line: 3, title: 'Part', level: 20 },
|
||||
{ line: 4, title: 'Part 2', level: 20 },
|
||||
{ line: 5, title: 'Chapter', level: 30 },
|
||||
{ line: 6, title: 'Chapter 2', level: 30 },
|
||||
{ line: 7, title: 'Section 1', level: 40 },
|
||||
{ line: 8, title: 'Section 1b', level: 40 },
|
||||
{ line: 9, title: 'Subsection', level: 50 },
|
||||
{ line: 10, title: 'Subsubsection', level: 60 },
|
||||
{ line: 11, title: 'Section 2', level: 40 },
|
||||
{ line: 12, title: 'Subsubsection without subsection', level: 60 },
|
||||
{ line: 13, title: 'a paragraph', level: 70 },
|
||||
{ line: 14, title: 'a subparagraph', level: 80 },
|
||||
])
|
||||
})
|
||||
|
||||
it('matches display titles', function () {
|
||||
const content = `
|
||||
\\section{\\label{foo} Label before}
|
||||
\\section{Label after \\label{foo}}
|
||||
\\section{Label \\label{foo} between}
|
||||
\\section{TT \\texttt{Bar}}
|
||||
\\section{plain title}
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 2, title: ' Label before', level: 40 },
|
||||
{ line: 3, title: 'Label after ', level: 40 },
|
||||
{ line: 4, title: 'Label between', level: 40 },
|
||||
{ line: 5, title: 'TT Bar', level: 40 },
|
||||
{ line: 6, title: 'plain title', level: 40 },
|
||||
])
|
||||
})
|
||||
|
||||
it('removes spurious commands after title definition', function () {
|
||||
const content = `
|
||||
\\section{Plain title} more text \\href{link}{link}
|
||||
\\section{\\label{foo} Label before} more text \\href{link}{link}
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 2, title: 'Plain title', level: 40 },
|
||||
{ line: 3, title: ' Label before', level: 40 },
|
||||
])
|
||||
})
|
||||
|
||||
it('matches empty sections', function () {
|
||||
const outline = matchOutline('\\section{}')
|
||||
expect(outline).to.deep.equal([{ line: 1, title: '', level: 40 }])
|
||||
})
|
||||
|
||||
it('matches indented sections', function () {
|
||||
const outline = matchOutline('\t\\section{Indented}')
|
||||
expect(outline).to.deep.equal([{ line: 1, title: 'Indented', level: 40 }])
|
||||
})
|
||||
|
||||
it('matches unnumbered sections', function () {
|
||||
const outline = matchOutline('\\section*{Unnumbered}')
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 1, title: 'Unnumbered', level: 40 },
|
||||
])
|
||||
})
|
||||
|
||||
it('matches short titles', function () {
|
||||
const outline = matchOutline(
|
||||
'\\chapter[Short Title For TOC]{Very Long Title for Text}'
|
||||
)
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 1, title: 'Short Title For TOC', level: 30 },
|
||||
])
|
||||
})
|
||||
|
||||
it('handles spacing', function () {
|
||||
const content = `
|
||||
\\section {Weird Spacing}
|
||||
\\section * {Weird Spacing Unnumbered}
|
||||
\\section [Weird Spacing for TOC] {Weird Spacing}
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([
|
||||
{ line: 2, title: 'Weird Spacing', level: 40 },
|
||||
{ line: 3, title: 'Weird Spacing Unnumbered', level: 40 },
|
||||
{ line: 4, title: 'Weird Spacing for TOC', level: 40 },
|
||||
])
|
||||
})
|
||||
|
||||
it("doesn't match commented lines", function () {
|
||||
const content = `
|
||||
% \\section{I should not appear in the outline}
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([])
|
||||
})
|
||||
|
||||
it("doesn't match inline sections", function () {
|
||||
const content = `
|
||||
I like to write \\section{inline} on one line.
|
||||
`
|
||||
const outline = matchOutline(content)
|
||||
expect(outline).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nestOutline', function () {
|
||||
it('matches all levels', function () {
|
||||
const flatOutline = [
|
||||
{ line: 10, title: 'Book', level: 10 },
|
||||
{ line: 20, title: 'Part A', level: 20 },
|
||||
{ line: 30, title: 'Section A 1', level: 40 },
|
||||
{ line: 40, title: 'Subsection A 1 1', level: 50 },
|
||||
{ line: 50, title: 'Subsection A 1 2', level: 50 },
|
||||
{ line: 60, title: 'Section A 2', level: 40 },
|
||||
{ line: 70, title: 'Section A 3', level: 40 },
|
||||
{ line: 80, title: 'Subsection A 3 1', level: 50 },
|
||||
{ line: 90, title: 'Chapter', level: 30 },
|
||||
{ line: 100, title: 'Part B', level: 20 },
|
||||
{ line: 110, title: 'Section 2', level: 40 },
|
||||
{ line: 120, title: 'Subsubsection without subsection', level: 60 },
|
||||
]
|
||||
const nestedOutline = nestOutline(flatOutline)
|
||||
expect(nestedOutline).to.deep.equal([
|
||||
{
|
||||
line: 10,
|
||||
title: 'Book',
|
||||
level: 10,
|
||||
children: [
|
||||
{
|
||||
line: 20,
|
||||
title: 'Part A',
|
||||
level: 20,
|
||||
children: [
|
||||
{
|
||||
line: 30,
|
||||
title: 'Section A 1',
|
||||
level: 40,
|
||||
children: [
|
||||
{ line: 40, title: 'Subsection A 1 1', level: 50 },
|
||||
{ line: 50, title: 'Subsection A 1 2', level: 50 },
|
||||
],
|
||||
},
|
||||
{ line: 60, title: 'Section A 2', level: 40 },
|
||||
{
|
||||
line: 70,
|
||||
title: 'Section A 3',
|
||||
level: 40,
|
||||
children: [
|
||||
{ line: 80, title: 'Subsection A 3 1', level: 50 },
|
||||
],
|
||||
},
|
||||
{ line: 90, title: 'Chapter', level: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
line: 100,
|
||||
title: 'Part B',
|
||||
level: 20,
|
||||
children: [
|
||||
{
|
||||
line: 110,
|
||||
title: 'Section 2',
|
||||
level: 40,
|
||||
children: [
|
||||
{
|
||||
line: 120,
|
||||
title: 'Subsubsection without subsection',
|
||||
level: 60,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -61,18 +61,11 @@ function getModuleDirectory(moduleName) {
|
|||
|
||||
const mathjaxDir = getModuleDirectory('mathjax')
|
||||
const mathjax3Dir = getModuleDirectory('mathjax-3')
|
||||
const aceDir = getModuleDirectory('ace-builds')
|
||||
|
||||
const pdfjsVersions = ['pdfjs-dist213', 'pdfjs-dist401']
|
||||
|
||||
const vendorDir = path.join(__dirname, 'frontend/js/vendor')
|
||||
|
||||
const ACE_VERSION = require('ace-builds/version')
|
||||
if (ACE_VERSION !== PackageVersions.version.ace) {
|
||||
throw new Error(
|
||||
'"ace-builds" version de-synced, update services/web/app/src/infrastructure/PackageVersions.js'
|
||||
)
|
||||
}
|
||||
const MATHJAX_VERSION = require('mathjax/package.json').version
|
||||
if (MATHJAX_VERSION !== PackageVersions.version.mathjax) {
|
||||
throw new Error(
|
||||
|
@ -236,11 +229,6 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// Aliases for AMD modules
|
||||
|
||||
// Enables ace/ace shortcut
|
||||
ace: 'ace-builds/src-noconflict',
|
||||
|
||||
// custom prefixes for import paths
|
||||
'@': path.resolve(__dirname, './frontend/js/'),
|
||||
},
|
||||
|
@ -335,11 +323,6 @@ module.exports = {
|
|||
to: 'js/libs/mathjax',
|
||||
context: mathjaxDir,
|
||||
},
|
||||
{
|
||||
from: 'src-min-noconflict',
|
||||
to: `js/ace-${PackageVersions.version.ace}/`,
|
||||
context: aceDir,
|
||||
},
|
||||
...pdfjsVersions.flatMap(version => {
|
||||
const dir = getModuleDirectory(version)
|
||||
|
||||
|
|
Loading…
Reference in a new issue