Remove Ace (#14299)

GitOrigin-RevId: ec8788fdbc8aea73ca33ec2810f4e588fe9476b5
This commit is contained in:
Alf Eaton 2023-11-28 11:20:24 +00:00 committed by Copybot
parent 4636f40f03
commit 9875e55a27
72 changed files with 59 additions and 7299 deletions

13
package-lock.json generated
View file

@ -14497,12 +14497,6 @@
"node": ">= 0.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": { "node_modules/acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -43692,7 +43686,6 @@
"@uppy/utils": "^4.0.7", "@uppy/utils": "^4.0.7",
"@uppy/xhr-upload": "^1.6.8", "@uppy/xhr-upload": "^1.6.8",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
"acorn": "^7.1.1", "acorn": "^7.1.1",
"acorn-walk": "^7.1.1", "acorn-walk": "^7.1.1",
"algoliasearch": "^3.35.1", "algoliasearch": "^3.35.1",
@ -52208,7 +52201,6 @@
"@xmldom/xmldom": "^0.7.13", "@xmldom/xmldom": "^0.7.13",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"accepts": "^1.3.7", "accepts": "^1.3.7",
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
"acorn": "^7.1.1", "acorn": "^7.1.1",
"acorn-walk": "^7.1.1", "acorn-walk": "^7.1.1",
"algoliasearch": "^3.35.1", "algoliasearch": "^3.35.1",
@ -57685,11 +57677,6 @@
"negotiator": "0.6.3" "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": { "acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",

View file

@ -166,7 +166,6 @@
"__webpack_public_path__": true, "__webpack_public_path__": true,
"$": true, "$": true,
"angular": true, "angular": true,
"ace": true,
"ga": true, "ga": true,
// Injected in layout.pug // Injected in layout.pug
"user_id": true, "user_id": true,

View file

@ -799,11 +799,6 @@ const ProjectController = {
const detachRole = req.params.detachRole const detachRole = req.params.detachRole
// Allow override via legacy_source_editor=true in query string
const showLegacySourceEditor = shouldDisplayFeature(
'legacy_source_editor'
)
const showSymbolPalette = const showSymbolPalette =
!Features.hasFeature('saas') || !Features.hasFeature('saas') ||
(user.features && user.features.symbolPalette) (user.features && user.features.symbolPalette)
@ -896,8 +891,6 @@ const ProjectController = {
showTemplatesServerPro, showTemplatesServerPro,
pdfjsVariant: pdfjsAssignment.variant, pdfjsVariant: pdfjsAssignment.variant,
debugPdfDetach, debugPdfDetach,
showLegacySourceEditor,
showSourceToolbar: !showLegacySourceEditor,
showSymbolPalette, showSymbolPalette,
symbolPaletteAvailable: Features.hasFeature('symbol-palette'), symbolPaletteAvailable: Features.hasFeature('symbol-palette'),
detachRole, detachRole,
@ -905,7 +898,6 @@ const ProjectController = {
showUpgradePrompt, showUpgradePrompt,
fixedSizeDocument: true, fixedSizeDocument: true,
useOpenTelemetry: Settings.useOpenTelemetryClient, useOpenTelemetry: Settings.useOpenTelemetryClient,
showCM6SwitchAwaySurvey: Settings.showCM6SwitchAwaySurvey,
isReviewPanelReact: reviewPanelAssignment.variant === 'react', isReviewPanelReact: reviewPanelAssignment.variant === 'react',
idePageReact, idePageReact,
showPersonalAccessToken, showPersonalAccessToken,

View file

@ -1,6 +1,5 @@
const version = { const version = {
// Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace // Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace
ace: '1.4.12',
mathjax: '2.7.9', mathjax: '2.7.9',
'mathjax-3': '3.2.2', 'mathjax-3': '3.2.2',
} }

View file

@ -21,7 +21,6 @@
include ./file-view include ./file-view
.editor-container.full-size( .editor-container.full-size(
class={"has-source-toolbar" : showSourceToolbar},
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0" ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
vertical-resizable-panes="south-pane-resizer" vertical-resizable-panes="south-pane-resizer"
vertical-resizable-panes-hidden-externally-on="south-pane-toggled" vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
@ -44,14 +43,10 @@
|   #{translate("open_a_file_on_the_left")} |   #{translate("open_a_file_on_the_left")}
div(ng-controller="EditorLoaderController") div(ng-controller="EditorLoaderController")
if (!showSourceToolbar)
include ./toolbar
div(ng-if="editor.newSourceEditor")
include ../../source-editor/source-editor include ../../source-editor/source-editor
div(ng-if="!editor.newSourceEditor")
include ./source-editor div(ng-if="!reviewPanel.isReact")
div(ng-if="!editor.newSourceEditor || !reviewPanel.isReact")
if !isRestrictedTokenMember if !isRestrictedTokenMember
include ./review-panel include ./review-panel

View file

@ -12,9 +12,6 @@ div.full-size(
custom-toggler-msg-when-open=translate("tooltip_hide_pdf") custom-toggler-msg-when-open=translate("tooltip_hide_pdf")
custom-toggler-msg-when-closed=translate("tooltip_show_pdf") custom-toggler-msg-when-closed=translate("tooltip_show_pdf")
) )
if (settings.showCM6SwitchAwaySurvey)
cm6-switch-away-survey()
include ./editor-pane include ./editor-pane
.ui-layout-east .ui-layout-east

View file

@ -24,15 +24,4 @@ aside.editor-sidebar.full-size(
.outline-container( .outline-container(
vertical-resizable-bottom 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"
) )

View file

@ -13,16 +13,12 @@ meta(name="ol-wikiEnabled" data-type="boolean" content=settings.proxyLearn)
meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl) meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl)
meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled)
meta(name="ol-compilesUserContentDomain" content=settings.compilesUserContentDomain) 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 //- enable doc hash checking for all projects
//- used in public/js/libs/sharejs.js //- used in public/js/libs/sharejs.js
meta(name="ol-useShareJsHash" data-type="boolean" content=true) meta(name="ol-useShareJsHash" data-type="boolean" content=true)
meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandshake) meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandshake)
meta(name="ol-pdfjsVariant" content=pdfjsVariant) meta(name="ol-pdfjsVariant" content=pdfjsVariant)
meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach) 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-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable) meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
meta(name="ol-detachRole" data-type="string" content=detachRole) 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-useOpenTelemetry" data-type="boolean" content=useOpenTelemetry)
meta(name="ol-showSupport", data-type="boolean" content=showSupport) meta(name="ol-showSupport", data-type="boolean" content=showSupport)
meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro) 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-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken) meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact) meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact)

View file

@ -1,5 +1,5 @@
#review-panel( #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 .rp-in-editor-widgets
a.rp-track-changes-indicator( a.rp-track-changes-indicator(

View file

@ -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"
)

View file

@ -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}}

View file

@ -1,5 +1,27 @@
import getMeta from '../../utils/meta' 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 { export class IgnoredWords {
public learnedWords!: Set<string> public learnedWords!: Set<string>

View file

@ -3,12 +3,10 @@ import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { useProjectContext } from '../../../shared/context/project-context' import { useProjectContext } from '../../../shared/context/project-context'
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal' import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import LeftMenuButton from './left-menu-button' import LeftMenuButton from './left-menu-button'
export default function HelpShowHotkeys() { export default function HelpShowHotkeys() {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
const { t } = useTranslation() const { t } = useTranslation()
const { features } = useProjectContext() const { features } = useProjectContext()
const isMac = /Mac/.test(window.navigator?.platform) const isMac = /Mac/.test(window.navigator?.platform)
@ -34,7 +32,6 @@ export default function HelpShowHotkeys() {
handleHide={() => setShowModal(false)} handleHide={() => setShowModal(false)}
isMac={isMac} isMac={isMac}
trackChangesVisible={features?.trackChangesVisible} trackChangesVisible={features?.trackChangesVisible}
newSourceEditor={newSourceEditor}
/> />
</> </>
) )

View file

@ -10,17 +10,11 @@ export default function HotkeysModal({
show, show,
isMac = false, isMac = false,
trackChangesVisible = false, trackChangesVisible = false,
newSourceEditor = false,
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const goToLineSuffix = newSourceEditor ? 'Shift + L' : 'L'
const ctrl = isMac ? 'Cmd' : 'Ctrl' const ctrl = isMac ? 'Cmd' : 'Ctrl'
const modalTitle = newSourceEditor
? `${t('hotkeys')} (Source editor)`
: `${t('hotkeys')} (Legacy source editor)`
return ( return (
<AccessibleModal <AccessibleModal
bsSize="large" bsSize="large"
@ -29,7 +23,7 @@ export default function HotkeysModal({
animation={animation} animation={animation}
> >
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>{modalTitle}</Modal.Title> <Modal.Title>{t('hotkeys')} (Source editor)</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body className="hotkeys-modal"> <Modal.Body className="hotkeys-modal">
@ -77,7 +71,7 @@ export default function HotkeysModal({
</Col> </Col>
<Col xs={4}> <Col xs={4}>
<Hotkey <Hotkey
combination={`${ctrl} + ${goToLineSuffix}`} combination={`${ctrl} + Shift + L`}
description={t('hotkey_go_to_line')} description={t('hotkey_go_to_line')}
/> />
</Col> </Col>
@ -211,7 +205,6 @@ HotkeysModal.propTypes = {
show: PropTypes.bool.isRequired, show: PropTypes.bool.isRequired,
handleHide: PropTypes.func.isRequired, handleHide: PropTypes.func.isRequired,
trackChangesVisible: PropTypes.bool, trackChangesVisible: PropTypes.bool,
newSourceEditor: PropTypes.bool,
} }
function Hotkey({ combination, description }) { function Hotkey({ combination, description }) {

View file

@ -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',
])
)

View file

@ -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

View file

@ -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 }

View file

@ -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">&times;</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>
)
}

View file

@ -17,7 +17,6 @@ import EditorSwitch from './editor-switch'
import SwitchToPDFButton from './switch-to-pdf-button' import SwitchToPDFButton from './switch-to-pdf-button'
import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control' import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper' import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
import getMeta from '../../../utils/meta'
import { isVisual } from '../extensions/visual/visual' import { isVisual } from '../extensions/visual/visual'
import { language } from '@codemirror/language' import { language } from '@codemirror/language'
import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors' import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
@ -34,8 +33,6 @@ export const CodeMirrorToolbar = () => {
} }
const Toolbar = memo(function Toolbar() { const Toolbar = memo(function Toolbar() {
const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar')
const state = useCodeMirrorStateContext() const state = useCodeMirrorStateContext()
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
@ -117,7 +114,7 @@ const Toolbar = memo(function Toolbar() {
return ( return (
<div className="ol-cm-toolbar toolbar-editor" ref={elementRef}> <div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
{showSourceToolbar && <EditorSwitch />} <EditorSwitch />
{showActions && ( {showActions && (
<ToolbarItems <ToolbarItems
state={state} state={state}
@ -126,6 +123,7 @@ const Toolbar = memo(function Toolbar() {
listDepth={listDepth} listDepth={listDepth}
/> />
)} )}
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch"> <div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
{showActions && ( {showActions && (
<ToolbarOverflow <ToolbarOverflow
@ -143,8 +141,10 @@ const Toolbar = memo(function Toolbar() {
/> />
</ToolbarOverflow> </ToolbarOverflow>
)} )}
<div className="formatting-buttons-wrapper" /> <div className="formatting-buttons-wrapper" />
</div> </div>
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end"> <div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
<ToolbarButton <ToolbarButton
id="toolbar-toggle-search" id="toolbar-toggle-search"
@ -153,13 +153,10 @@ const Toolbar = memo(function Toolbar() {
active={searchPanelOpen(state)} active={searchPanelOpen(state)}
icon="search" icon="search"
/> />
{showSourceToolbar && (
<>
<SwitchToPDFButton /> <SwitchToPDFButton />
<DetacherSynctexControl /> <DetacherSynctexControl />
<DetachCompileButtonWrapper /> <DetachCompileButtonWrapper />
</>
)}
</div> </div>
<div className="ol-cm-toolbar-button-group hidden"> <div className="ol-cm-toolbar-button-group hidden">
<ToolbarButton <ToolbarButton

View file

@ -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)

View file

@ -9,40 +9,28 @@ import { FeedbackBadge } from '@/shared/components/feedback-badge'
function EditorSwitch() { function EditorSwitch() {
const { t } = useTranslation() const { t } = useTranslation()
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
'editor.newSourceEditor'
)
const [visual, setVisual] = useScopeValue('editor.showVisual') const [visual, setVisual] = useScopeValue('editor.showVisual')
const [docName] = useScopeValue('editor.open_doc_name') const [docName] = useScopeValue('editor.open_doc_name')
const richTextAvailable = isValidTeXFile(docName) const richTextAvailable = isValidTeXFile(docName)
// TODO: rename this after legacy & toolbar split tests are complete
const richTextOrVisual = richTextAvailable && visual
const handleChange = useCallback( const handleChange = useCallback(
event => { event => {
const editorType = event.target.value const editorType = event.target.value
switch (editorType) { switch (editorType) {
case 'ace':
setVisual(false)
setNewSourceEditor(false)
break
case 'cm6': case 'cm6':
setVisual(false) setVisual(false)
setNewSourceEditor(true)
break break
case 'rich-text': case 'rich-text':
setVisual(true) setVisual(true)
setNewSourceEditor(true)
break break
} }
sendMB('editor-switch-change', { editorType }) sendMB('editor-switch-change', { editorType })
}, },
[setVisual, setNewSourceEditor] [setVisual]
) )
return ( return (
@ -56,7 +44,7 @@ function EditorSwitch() {
value="cm6" value="cm6"
id="editor-switch-cm6" id="editor-switch-cm6"
className="toggle-switch-input" className="toggle-switch-input"
checked={!richTextOrVisual && !!newSourceEditor} checked={!richTextAvailable || !visual}
onChange={handleChange} onChange={handleChange}
/> />
<label htmlFor="editor-switch-cm6" className="toggle-switch-label"> <label htmlFor="editor-switch-cm6" className="toggle-switch-label">
@ -64,13 +52,13 @@ function EditorSwitch() {
</label> </label>
<RichTextToggle <RichTextToggle
checked={!!richTextOrVisual} checked={richTextAvailable && visual}
disabled={!richTextAvailable} disabled={!richTextAvailable}
handleChange={handleChange} handleChange={handleChange}
/> />
</fieldset> </fieldset>
{!!richTextOrVisual && ( {richTextAvailable && visual && (
<FeedbackBadge <FeedbackBadge
id="visual-editor-feedback" id="visual-editor-feedback"
url="https://forms.gle/AUqHmKNiEH3DRniPA" url="https://forms.gle/AUqHmKNiEH3DRniPA"

View file

@ -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">&times;</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>
)
})

View file

@ -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))
)

View file

@ -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)))

View file

@ -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'])
)

View file

@ -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. // 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. // Once the review panel is always inside the editor, this offset can be removed.
const offsetTop = const offsetTop = getMeta('ol-isReviewPanelReact') ? 0 : 32
getMeta('ol-showSourceToolbar') && !getMeta('ol-isReviewPanelReact') ? 32 : 0
// With less than this number of entries, don't bother culling to avoid // With less than this number of entries, don't bother culling to avoid
// little UI jumps when scrolling. // little UI jumps when scrolling.

View file

@ -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)
}

View file

@ -28,8 +28,6 @@ import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
import ReferencesManager from './ide/references/ReferencesManager' import ReferencesManager from './ide/references/ReferencesManager'
import MetadataManager from './ide/metadata/MetadataManager' import MetadataManager from './ide/metadata/MetadataManager'
import './ide/review-panel/ReviewPanelManager' import './ide/review-panel/ReviewPanelManager'
import OutlineManager from './features/outline/outline-manager'
import SafariScrollPatcher from './ide/SafariScrollPatcher'
import './ide/cobranding/CobrandingDataService' import './ide/cobranding/CobrandingDataService'
import './ide/chat/index' import './ide/chat/index'
import './ide/file-view/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/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
import './features/pdf-preview/controllers/pdf-preview-controller' import './features/pdf-preview/controllers/pdf-preview-controller'
import './features/share-project-modal/controllers/react-share-project-modal-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/history/controllers/history-controller'
import './features/editor-left-menu/controllers/editor-left-menu-controller' import './features/editor-left-menu/controllers/editor-left-menu-controller'
import { cleanupServiceWorker } from './utils/service-worker-cleanup' import { cleanupServiceWorker } from './utils/service-worker-cleanup'
import { reportCM6Perf } from './infrastructure/cm6-performance' import { reportCM6Perf } from './infrastructure/cm6-performance'
import { reportAcePerf } from './ide/editor/ace-performance'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
App.controller('IdeController', [ App.controller('IdeController', [
@ -215,7 +209,6 @@ App.controller('IdeController', [
ide.permissionsManager = new PermissionsManager(ide, $scope) ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope) ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
ide.metadataManager = new MetadataManager(ide, $scope, metadata) ide.metadataManager = new MetadataManager(ide, $scope, metadata)
ide.outlineManager = new OutlineManager(ide, $scope)
let inited = false let inited = false
$scope.$on('project:joined', function () { $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 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.localStorage = localStorage
ide.browserIsSafari = false
$scope.switchToFlatLayout = function (view) { $scope.switchToFlatLayout = function (view) {
$scope.ui.pdfLayout = 'flat' $scope.ui.pdfLayout = 'flat'
$scope.ui.view = view $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? // 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 // User can append ?ft=somefeature to url to activate a feature toggle
ide.featureToggle = __guard__( ide.featureToggle = __guard__(
__guard__( __guard__(

View file

@ -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()
})
}
}

View file

@ -85,46 +85,19 @@ export default Document = (function () {
this.connected = this.ide.socket.socket.connected this.connected = this.ide.socket.socket.connected
this.joined = false this.joined = false
this.wantToBeJoined = false this.wantToBeJoined = false
this._checkAceConsistency = () => this._checkConsistency(this.ace)
this._checkCM6Consistency = () => this._checkConsistency(this.cm6) this._checkCM6Consistency = () => this._checkConsistency(this.cm6)
this._bindToEditorEvents() this._bindToEditorEvents()
this._bindToSocketEvents() this._bindToSocketEvents()
} }
editorType() { editorType() {
if (this.ace) { if (this.cm6) {
return 'ace'
} else if (this.cm6) {
return 'cm6' return 'cm6'
} else { } else {
return null 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) { attachToCM6(cm6) {
this.cm6 = cm6 this.cm6 = cm6
if (this.doc != null) { if (this.doc != null) {
@ -328,9 +301,7 @@ export default Document = (function () {
} }
char = copy[0] char = copy[0]
copy = copy.slice(1) copy = copy.slice(1)
if (this.ace) { if (this.cm6) {
this.ace.session.insert({ row: line, column: pos }, char)
} else if (this.cm6) {
this.cm6.view.dispatch({ this.cm6.view.dispatch({
changes: { changes: {
from: Math.min(pos, this.cm6.view.state.doc.length), from: Math.min(pos, this.cm6.view.state.doc.length),
@ -757,7 +728,7 @@ export default Document = (function () {
this.ranges.setIdSeed(old_id_seed) this.ranges.setIdSeed(old_id_seed)
} }
if (remote_op) { 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 // so defer updating track changes until it has
return setTimeout(() => this.emit('ranges:dirty')) return setTimeout(() => this.emit('ranges:dirty'))
} else { } else {

View file

@ -15,15 +15,12 @@ import _ from 'lodash'
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
import Document from './Document' import Document from './Document'
import './components/spellMenu'
import './directives/aceEditor'
import './directives/formattingButtons' import './directives/formattingButtons'
import './directives/toggleSwitch' import './directives/toggleSwitch'
import './controllers/SavingNotificationController' import './controllers/SavingNotificationController'
import './controllers/CompileButton' import './controllers/CompileButton'
import './controllers/SwitchToPDFButton' import './controllers/SwitchToPDFButton'
import getMeta from '../../utils/meta' import '../metadata/services/metadata'
import { hasSeenCM6SwitchAwaySurvey } from '../../features/source-editor/utils/switch-away-survey'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
let EditorManager let EditorManager
@ -48,7 +45,6 @@ export default EditorManager = (function () {
wantTrackChanges: false, wantTrackChanges: false,
docTooLongErrorShown: false, docTooLongErrorShown: false,
showVisual: this.showVisual(), showVisual: this.showVisual(),
newSourceEditor: this.newSourceEditor(),
showSymbolPalette: false, showSymbolPalette: false,
toggleSymbolPalette: () => { toggleSymbolPalette: () => {
const newValue = !this.$scope.editor.showSymbolPalette 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() { autoOpenDoc() {
const open_doc_id = const open_doc_id =
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) || this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||

View file

@ -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) { attachToCM6(cm6) {
this._attachToEditor('CM6', cm6, () => { this._attachToEditor('CM6', cm6, () => {
cm6.attachShareJs(this._doc, window.maxDocLength) cm6.attachShareJs(this._doc, window.maxDocLength)

View file

@ -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())
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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,
}

View file

@ -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,
},
]

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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)
)
}
}

View file

@ -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',
]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
},
])

View file

@ -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
}
}

View file

@ -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
}

View file

@ -280,26 +280,6 @@ export default App.controller('ReviewPanelController', [
return rangesTrackers[doc_id] 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( $scope.$watch(
'!ui.reviewPanelOpen && reviewPanel.hasEntries', '!ui.reviewPanelOpen && reviewPanel.hasEntries',
function (open, prevVal) { function (open, prevVal) {
@ -335,7 +315,6 @@ export default App.controller('ReviewPanelController', [
if (view == null) { if (view == null) {
return return
} }
updateScrollbar()
if (view === $scope.SubViews.OVERVIEW) { if (view === $scope.SubViews.OVERVIEW) {
return refreshOverviewPanel() return refreshOverviewPanel()
} else if (oldView === $scope.SubViews.OVERVIEW) { } else if (oldView === $scope.SubViews.OVERVIEW) {

View file

@ -249,13 +249,11 @@ export default App.directive('reviewPanelSorted', function () {
// mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into // mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into
// scroll events ourselves, then it makes the review panel slightly less smooth (barely) // scroll events ourselves, then it makes the review panel slightly less smooth (barely)
// noticeable, but keeps it perfectly in step with Ace. // noticeable, but keeps it perfectly in step with Ace.
ace scroller[0].addEventListener('wheel', e => {
.require('ace/lib/event') // FIXME (or remove this): https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
.addMouseWheelListener(scroller[0], function (e) {
const deltaY = e.wheelY const deltaY = e.wheelY
const old_top = parseInt(list.css('top')) const old_top = parseInt(list.css('top'))
const top = old_top - deltaY * 4 const top = old_top - deltaY * 4
scrollAce(-top)
dispatchScrollEvent(deltaY * 4) dispatchScrollEvent(deltaY * 4)
return e.preventDefault() 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 // receive the scroll position from the CodeMirror 6 track changes extension
window.addEventListener('editor:scroll', event => { window.addEventListener('editor:scroll', event => {
const { scrollTop, height, paddingTop } = event.detail const { scrollTop, height, paddingTop } = event.detail

View file

@ -10,12 +10,5 @@ App.controller('EditorLoaderController', [
val === true ? 'rich-text' : 'source' val === true ? 'rich-text' : 'source'
) )
}) })
$scope.$watch('editor.newSourceEditor', function (val) {
localStorage(
`editor.source_editor.${$scope.project_id}`,
val === true ? 'cm6' : 'ace'
)
})
}, },
]) ])

View file

@ -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
},
])

View file

@ -1,2 +1 @@
import './EditorLoaderController' import './EditorLoaderController'
import './EditorToolbarController'

View file

@ -26,7 +26,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * 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, var append = void 0,
bootstrapTransform = void 0, bootstrapTransform = void 0,
exports = void 0, exports = void 0,
@ -1410,177 +1410,6 @@ define(['ace/ace','crypto-js/sha1', '@/utils/debugging'], function (_ignore, Cry
MicroEvent.mixin(Doc); MicroEvent.mixin(Doc);
exports.Doc = 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; return window.sharejs;
}); });

View file

@ -92,7 +92,6 @@ const initialize = () => {
toggleHistory: () => {}, toggleHistory: () => {},
editor: { editor: {
richText: false, richText: false,
newSourceEditor: false,
sharejs_doc: { sharejs_doc: {
doc_id: 'test-doc', doc_id: 'test-doc',
getSnapshot: () => 'some doc content', getSnapshot: () => 'some doc content',

View file

@ -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' import { ScopeDecorator } from './decorators/scope'
export default { export default {

View file

@ -93,13 +93,6 @@
.full-size; .full-size;
} }
.editor-container #editor {
top: @editor-toolbar-height;
}
.editor-container.has-source-toolbar #editor {
top: 0;
}
.pdf-empty, .pdf-empty,
.no-history-available, .no-history-available,
.no-file-selection, .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 CodeMirror
***************************************/ ***************************************/

View file

@ -82,7 +82,7 @@
} }
position: absolute; position: absolute;
top: 32px; top: 0px;
bottom: 0px; bottom: 0px;
right: 0px; right: 0px;
background-color: @rp-bg-blue; background-color: @rp-bg-blue;
@ -90,10 +90,6 @@
font-size: @rp-base-font-size; font-size: @rp-base-font-size;
color: @rp-type-blue; color: @rp-type-blue;
z-index: 6; z-index: 6;
.has-source-toolbar & {
top: 0;
}
} }
.loading-panel { .loading-panel {
@ -1128,10 +1124,6 @@ button when (@is-overleaf-light = true) {
.rp-unsupported & { .rp-unsupported & {
display: none; display: none;
} }
.has-source-toolbar & {
top: 32px;
}
} }
.rp-track-changes-indicator { .rp-track-changes-indicator {
@ -1376,8 +1368,4 @@ button when (@is-overleaf-light = true) {
.rp-track-changes-indicator { .rp-track-changes-indicator {
border: 0; border: 0;
} }
.has-source-toolbar & {
top: 0;
}
} }

View file

@ -239,7 +239,6 @@
"@uppy/utils": "^4.0.7", "@uppy/utils": "^4.0.7",
"@uppy/xhr-upload": "^1.6.8", "@uppy/xhr-upload": "^1.6.8",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9",
"acorn": "^7.1.1", "acorn": "^7.1.1",
"acorn-walk": "^7.1.1", "acorn-walk": "^7.1.1",
"algoliasearch": "^3.35.1", "algoliasearch": "^3.35.1",

View file

@ -10,7 +10,6 @@ type Scope = {
doc_id?: string doc_id?: string
getSnapshot?: () => string getSnapshot?: () => string
} }
newSourceEditor?: boolean
} }
hasLintingError?: boolean hasLintingError?: boolean
ui?: { ui?: {
@ -45,7 +44,6 @@ export const mockScope = (scope?: Scope) => ({
doc_id: 'test-doc', doc_id: 'test-doc',
getSnapshot: () => 'some doc content', getSnapshot: () => 'some doc content',
}, },
newSourceEditor: true,
}, },
hasLintingError: false, hasLintingError: false,
ui: { ui: {

View file

@ -15,7 +15,6 @@ describe('<HelpShowHotkeys />', function () {
expect(screen.queryByRole('dialog')).to.equal(null) expect(screen.queryByRole('dialog')).to.equal(null)
fireEvent.click(screen.getByRole('button', { name: 'Show Hotkeys' })) fireEvent.click(screen.getByRole('button', { name: 'Show Hotkeys' }))
const modal = screen.getAllByRole('dialog')[0] const modal = screen.getAllByRole('dialog')[0]
within(modal).getByText('Hotkeys (Legacy source editor)')
within(modal).getByText('Common') within(modal).getByText('Common')
}) })
}) })

View file

@ -11,25 +11,13 @@ const modalProps = {
describe('<HotkeysModal />', function () { describe('<HotkeysModal />', function () {
it('renders the translated modal title on cm6', async function () { it('renders the translated modal title on cm6', async function () {
const { baseElement } = render( const { baseElement } = render(<HotkeysModal {...modalProps} />)
<HotkeysModal {...modalProps} newSourceEditor />
)
expect(baseElement.querySelector('.modal-title').textContent).to.equal( expect(baseElement.querySelector('.modal-title').textContent).to.equal(
'Hotkeys (Source editor)' '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 () { it('renders translated heading with embedded code', function () {
const { baseElement } = render(<HotkeysModal {...modalProps} />) const { baseElement } = render(<HotkeysModal {...modalProps} />)

View file

@ -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,
},
],
},
],
},
],
},
])
})
})
})

View file

@ -61,18 +61,11 @@ function getModuleDirectory(moduleName) {
const mathjaxDir = getModuleDirectory('mathjax') const mathjaxDir = getModuleDirectory('mathjax')
const mathjax3Dir = getModuleDirectory('mathjax-3') const mathjax3Dir = getModuleDirectory('mathjax-3')
const aceDir = getModuleDirectory('ace-builds')
const pdfjsVersions = ['pdfjs-dist213', 'pdfjs-dist401'] const pdfjsVersions = ['pdfjs-dist213', 'pdfjs-dist401']
const vendorDir = path.join(__dirname, 'frontend/js/vendor') 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 const MATHJAX_VERSION = require('mathjax/package.json').version
if (MATHJAX_VERSION !== PackageVersions.version.mathjax) { if (MATHJAX_VERSION !== PackageVersions.version.mathjax) {
throw new Error( throw new Error(
@ -236,11 +229,6 @@ module.exports = {
}, },
resolve: { resolve: {
alias: { alias: {
// Aliases for AMD modules
// Enables ace/ace shortcut
ace: 'ace-builds/src-noconflict',
// custom prefixes for import paths // custom prefixes for import paths
'@': path.resolve(__dirname, './frontend/js/'), '@': path.resolve(__dirname, './frontend/js/'),
}, },
@ -335,11 +323,6 @@ module.exports = {
to: 'js/libs/mathjax', to: 'js/libs/mathjax',
context: mathjaxDir, context: mathjaxDir,
}, },
{
from: 'src-min-noconflict',
to: `js/ace-${PackageVersions.version.ace}/`,
context: aceDir,
},
...pdfjsVersions.flatMap(version => { ...pdfjsVersions.flatMap(version => {
const dir = getModuleDirectory(version) const dir = getModuleDirectory(version)