mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Remove Ace (#14299)
GitOrigin-RevId: ec8788fdbc8aea73ca33ec2810f4e588fe9476b5
This commit is contained in:
parent
4636f40f03
commit
9875e55a27
72 changed files with 59 additions and 7299 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -14497,12 +14497,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
div(ng-if="!reviewPanel.isReact")
|
||||||
include ./source-editor
|
|
||||||
div(ng-if="!editor.newSourceEditor || !reviewPanel.isReact")
|
|
||||||
if !isRestrictedTokenMember
|
if !isRestrictedTokenMember
|
||||||
include ./review-panel
|
include ./review-panel
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
)
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
#editor(
|
|
||||||
ace-editor="editor",
|
|
||||||
ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state",
|
|
||||||
theme="settings.editorTheme",
|
|
||||||
keybindings="settings.mode",
|
|
||||||
font-size="settings.fontSize",
|
|
||||||
auto-complete="settings.autoComplete",
|
|
||||||
auto-pair-delimiters="settings.autoPairDelimiters",
|
|
||||||
spell-check="!anonymous",
|
|
||||||
spell-check-language="project.spellCheckLanguage"
|
|
||||||
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
|
|
||||||
show-print-margin="false",
|
|
||||||
sharejs-doc="editor.sharejs_doc",
|
|
||||||
last-updated="editor.last_updated",
|
|
||||||
cursor-position="editor.cursorPosition",
|
|
||||||
goto-line="editor.gotoLine",
|
|
||||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle,layout:flat-screen:toggle",
|
|
||||||
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
|
|
||||||
read-only="!permissions.write",
|
|
||||||
file-name="editor.open_doc_name",
|
|
||||||
on-ctrl-j="toggleReviewPanel",
|
|
||||||
on-ctrl-shift-c="addNewCommentFromKbdShortcut",
|
|
||||||
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
|
|
||||||
syntax-validation="settings.syntaxValidation",
|
|
||||||
review-panel="reviewPanel",
|
|
||||||
events-bridge="reviewPanelEventsBridge"
|
|
||||||
track-changes= "editor.trackChanges",
|
|
||||||
doc-id="editor.open_doc_id"
|
|
||||||
renderer-data="reviewPanel.rendererData"
|
|
||||||
font-family="settings.fontFamily"
|
|
||||||
line-height="settings.lineHeight"
|
|
||||||
)
|
|
|
@ -1,43 +0,0 @@
|
||||||
.toolbar.toolbar-editor(ng-controller="EditorToolbarController")
|
|
||||||
.toggle-wrapper
|
|
||||||
editor-switch
|
|
||||||
|
|
||||||
div(
|
|
||||||
formatting-buttons
|
|
||||||
ng-cloak
|
|
||||||
ng-if="!editor.showVisual"
|
|
||||||
buttons="editorButtons"
|
|
||||||
opening="editor.opening"
|
|
||||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle"
|
|
||||||
is-fullscreen-editor="ui.view == 'editor' && ui.pdfLayout == 'flat'"
|
|
||||||
class="formatting-buttons"
|
|
||||||
)
|
|
||||||
div(
|
|
||||||
formatting-buttons
|
|
||||||
ng-cloak
|
|
||||||
ng-if="editor.showVisual"
|
|
||||||
buttons="[]"
|
|
||||||
opening="editor.opening"
|
|
||||||
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle"
|
|
||||||
is-fullscreen-editor="ui.view == 'editor' && ui.pdfLayout == 'flat'"
|
|
||||||
class="formatting-buttons"
|
|
||||||
)
|
|
||||||
|
|
||||||
.toolbar-pdf-right
|
|
||||||
switch-to-pdf-button()
|
|
||||||
detacher-synctex-control()
|
|
||||||
editor-compile-button()
|
|
||||||
|
|
||||||
script(type="text/ng-template", id="formattingButtonsTpl")
|
|
||||||
.formatting-buttons-wrapper
|
|
||||||
|
|
|
||||||
button.btn.formatting-btn.formatting-btn--icon(
|
|
||||||
ng-repeat="button in shownButtons"
|
|
||||||
ng-click="button.handleClick()"
|
|
||||||
ng-class="{ active: button.active }",
|
|
||||||
aria-label="{{button.title}}"
|
|
||||||
tooltip="{{button.title}}"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
)
|
|
||||||
i(class="{{button.iconClass}}") {{button.iconText}}
|
|
|
@ -1,5 +1,27 @@
|
||||||
import getMeta from '../../utils/meta'
|
import 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>
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 }) {
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import App from '../../../base'
|
|
||||||
import OutlinePane from '../components/outline-pane'
|
|
||||||
import { react2angular } from 'react2angular'
|
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
|
||||||
|
|
||||||
App.controller('OutlineController', [
|
|
||||||
'$scope',
|
|
||||||
'ide',
|
|
||||||
'eventTracking',
|
|
||||||
function ($scope, ide, eventTracking) {
|
|
||||||
$scope.isTexFile = false
|
|
||||||
$scope.outline = []
|
|
||||||
$scope.eventTracking = eventTracking
|
|
||||||
|
|
||||||
function shouldShowOutline() {
|
|
||||||
return !$scope.editor.newSourceEditor
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.show = shouldShowOutline()
|
|
||||||
|
|
||||||
$scope.$watch('editor.newSourceEditor', function () {
|
|
||||||
$scope.show = shouldShowOutline()
|
|
||||||
})
|
|
||||||
|
|
||||||
$scope.$on('outline-manager:outline-changed', onOutlineChange)
|
|
||||||
|
|
||||||
function onOutlineChange(e, outlineInfo) {
|
|
||||||
$scope.$applyAsync(() => {
|
|
||||||
$scope.isTexFile = outlineInfo.isTexFile
|
|
||||||
$scope.outline = outlineInfo.outline
|
|
||||||
$scope.highlightedLine = outlineInfo.highlightedLine
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.jumpToLine = (lineNo, syncToPdf) => {
|
|
||||||
ide.outlineManager.jumpToLine(lineNo, syncToPdf)
|
|
||||||
eventTracking.sendMB('outline-jump-to-line')
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.onToggle = isOpen => {
|
|
||||||
$scope.$applyAsync(() => {
|
|
||||||
$scope.$emit('outline-toggled', isOpen)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// Wrap React component as Angular component. Only needed for "top-level" component
|
|
||||||
App.component(
|
|
||||||
'outlinePane',
|
|
||||||
react2angular(rootContext.use(OutlinePane), [
|
|
||||||
'outline',
|
|
||||||
'jumpToLine',
|
|
||||||
'highlightedLine',
|
|
||||||
'eventTracking',
|
|
||||||
'onToggle',
|
|
||||||
'isTexFile',
|
|
||||||
'show',
|
|
||||||
])
|
|
||||||
)
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { isEqual, cloneDeep } from 'lodash'
|
|
||||||
import './controllers/outline-controller'
|
|
||||||
import { matchOutline, nestOutline } from './outline-parser'
|
|
||||||
import isValidTeXFile from '../../main/is-valid-tex-file'
|
|
||||||
|
|
||||||
class OutlineManager {
|
|
||||||
constructor(ide, scope) {
|
|
||||||
this.ide = ide
|
|
||||||
this.scope = scope
|
|
||||||
this.shareJsDoc = null
|
|
||||||
this.isTexFile = false
|
|
||||||
this.flatOutline = []
|
|
||||||
this.previousFlatOutline = []
|
|
||||||
this.outline = []
|
|
||||||
this.highlightedLine = null
|
|
||||||
this.ignoreNextScroll = false
|
|
||||||
this.ignoreNextCursorUpdate = false
|
|
||||||
|
|
||||||
scope.$watch('editor.newSourceEditor', (now, before) => {
|
|
||||||
if (before && !now) {
|
|
||||||
this.updateOutline()
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.$on('doc:after-opened', (ev, { isNewDoc }) => {
|
|
||||||
if (isNewDoc) {
|
|
||||||
// if a new doc is opened a cursor updates will be triggered before the
|
|
||||||
// content is loaded. We have to ignore it or the outline highlight
|
|
||||||
// will be incorrect. This doesn't happen when `doc:after-opened` is
|
|
||||||
// fired without a new doc opened.
|
|
||||||
this.ignoreNextCursorUpdate = true
|
|
||||||
}
|
|
||||||
// always ignore the next scroll update so the cursor update takes
|
|
||||||
// precedence
|
|
||||||
this.ignoreNextScroll = true
|
|
||||||
this.shareJsDoc = scope.editor.sharejs_doc
|
|
||||||
this.isTexFile = isValidTeXFile(scope.editor.open_doc_name)
|
|
||||||
this.updateOutline()
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.$on('doc:changed', () => {
|
|
||||||
this.updateOutline()
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.$on('file-view:file-opened', () => {
|
|
||||||
this.isTexFile = false
|
|
||||||
this.updateOutline()
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.$on('cursor:editor:update', (event, cursorPosition) => {
|
|
||||||
if (this.ignoreNextCursorUpdate) {
|
|
||||||
this.ignoreNextCursorUpdate = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.updateHighlightedLine(cursorPosition.row + 1)
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
})
|
|
||||||
|
|
||||||
scope.$on('scroll:editor:update', (event, middleVisibleRow) => {
|
|
||||||
if (this.ignoreNextScroll) {
|
|
||||||
this.ignoreNextScroll = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateHighlightedLine(middleVisibleRow + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowOutline() {
|
|
||||||
return !this.scope.editor.newSourceEditor
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOutline() {
|
|
||||||
// Disable if using CM6
|
|
||||||
if (!this.shouldShowOutline()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.isTexFile) {
|
|
||||||
const content = this.ide.editorManager.getCurrentDocValue()
|
|
||||||
if (content) {
|
|
||||||
this.flatOutline = matchOutline(content)
|
|
||||||
if (!isEqual(this.flatOutline, this.previousFlatOutline)) {
|
|
||||||
this.previousFlatOutline = cloneDeep(this.flatOutline)
|
|
||||||
this.outline = nestOutline(this.flatOutline)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.previousFlatOutline = []
|
|
||||||
this.outline = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// set highlightedLine to the closest outline line above the editorLine
|
|
||||||
updateHighlightedLine(editorLine) {
|
|
||||||
let closestOutlineLine = null
|
|
||||||
for (let lineId = 0; lineId < this.flatOutline.length; lineId++) {
|
|
||||||
const outline = this.flatOutline[lineId]
|
|
||||||
if (editorLine < outline.line) break // editorLine is above
|
|
||||||
closestOutlineLine = outline.line
|
|
||||||
}
|
|
||||||
if (closestOutlineLine === this.highlightedLine) return
|
|
||||||
this.highlightedLine = closestOutlineLine
|
|
||||||
this.broadcastChangeEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpToLine(line, syncToPdf) {
|
|
||||||
this.ignoreNextScroll = true
|
|
||||||
this.ide.editorManager.jumpToLine({ gotoLine: line, syncToPdf })
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastChangeEvent() {
|
|
||||||
// Disable if using CM6
|
|
||||||
if (!this.shouldShowOutline()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.scope.$broadcast('outline-manager:outline-changed', {
|
|
||||||
isTexFile: this.isTexFile,
|
|
||||||
outline: this.outline,
|
|
||||||
highlightedLine: this.highlightedLine,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OutlineManager
|
|
|
@ -1,133 +0,0 @@
|
||||||
const COMMAND_LEVELS = {
|
|
||||||
book: 10,
|
|
||||||
part: 20,
|
|
||||||
addpart: 20,
|
|
||||||
chapter: 30,
|
|
||||||
addchap: 30,
|
|
||||||
section: 40,
|
|
||||||
addsec: 40,
|
|
||||||
subsection: 50,
|
|
||||||
subsubsection: 60,
|
|
||||||
paragraph: 70,
|
|
||||||
subparagraph: 80,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* RegExp matcher parts:
|
|
||||||
*
|
|
||||||
* REGEX_START: begining of line, any number of spaces, double \ (required)
|
|
||||||
* REGEX_COMMAND: any of the listed commands (required)
|
|
||||||
* REGEX_SPACING: spaces and * between groups (optional)
|
|
||||||
* REGEX_SHORT_TITLE: a text between square brackets (optional)
|
|
||||||
* REGEX_TITLE: a text between curly brackets (required)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const REGEX_START = '^\\s*\\\\'
|
|
||||||
const REGEX_COMMAND = `(${Object.keys(COMMAND_LEVELS).join('|')})`
|
|
||||||
const REGEX_SPACING = '\\s?\\*?\\s?'
|
|
||||||
const REGEX_SHORT_TITLE = '(\\[([^\\]]+)\\])?'
|
|
||||||
const REGEX_TITLE = '{(.*)}'
|
|
||||||
const MATCHER = new RegExp(
|
|
||||||
`${REGEX_START}${REGEX_COMMAND}${REGEX_SPACING}${REGEX_SHORT_TITLE}${REGEX_SPACING}${REGEX_TITLE}`
|
|
||||||
)
|
|
||||||
|
|
||||||
function matchOutline(content) {
|
|
||||||
const lines = content.split('\n')
|
|
||||||
const flatOutline = matchOutlineFromLines(lines)
|
|
||||||
return flatOutline
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchOutlineFromLines(lines) {
|
|
||||||
const flatOutline = []
|
|
||||||
lines.forEach((line, lineId) => {
|
|
||||||
const match = line.match(MATCHER)
|
|
||||||
if (!match) return
|
|
||||||
const [, command, , shortTitle, title] = match
|
|
||||||
|
|
||||||
flatOutline.push({
|
|
||||||
line: lineId + 1,
|
|
||||||
title: matchDisplayTitle(shortTitle || title),
|
|
||||||
level: COMMAND_LEVELS[command],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return flatOutline
|
|
||||||
}
|
|
||||||
|
|
||||||
const DISPLAY_TITLE_REGEX = /([^\\]*)\\([^{]+){([^}]+)}(.*)/
|
|
||||||
const END_OF_TITLE_REGEX = /^([^{}]*?({[^{}]*?}[^{}]*?)*)}/
|
|
||||||
/*
|
|
||||||
* Attempt to improve the display of the outline title for titles with commands.
|
|
||||||
* Either skip the command (for labels) or display the command's content instead
|
|
||||||
* of the entire command.
|
|
||||||
*
|
|
||||||
* e.g. "Label \\label{foo} between" => "Label between"
|
|
||||||
* e.g. "TT \\texttt{Bar}" => "TT Bar"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function matchDisplayTitle(title) {
|
|
||||||
const closingBracketPosition = title.indexOf('}')
|
|
||||||
|
|
||||||
if (closingBracketPosition < 0) {
|
|
||||||
// simple title (no commands)
|
|
||||||
return title
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is anything outside the title def on the line, remove it
|
|
||||||
// before proceeding
|
|
||||||
const titleOnlyMatch = title.match(END_OF_TITLE_REGEX)
|
|
||||||
if (titleOnlyMatch) {
|
|
||||||
title = titleOnlyMatch[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleMatch = title.match(DISPLAY_TITLE_REGEX)
|
|
||||||
if (!titleMatch) {
|
|
||||||
// no contained commands; strip everything after the first closing bracket
|
|
||||||
return title.substring(0, closingBracketPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, textBefore, command, commandContent, textAfter] = titleMatch
|
|
||||||
if (command === 'label') {
|
|
||||||
// label: don't display them at all
|
|
||||||
title = `${textBefore}${textAfter}`
|
|
||||||
} else {
|
|
||||||
// display the content of the command. Works well for formatting commands
|
|
||||||
title = `${textBefore}${commandContent}${textAfter}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return title
|
|
||||||
}
|
|
||||||
|
|
||||||
function nestOutline(flatOutline) {
|
|
||||||
const parentOutlines = {}
|
|
||||||
const nestedOutlines = []
|
|
||||||
flatOutline.forEach(outline => {
|
|
||||||
const parentOutlineLevels = Object.keys(parentOutlines)
|
|
||||||
|
|
||||||
// find the nearest parent outline
|
|
||||||
const nearestParentLevel = parentOutlineLevels
|
|
||||||
.reverse()
|
|
||||||
.find(level => level < outline.level)
|
|
||||||
const parentOutline = parentOutlines[nearestParentLevel]
|
|
||||||
if (!parentOutline) {
|
|
||||||
// top level
|
|
||||||
nestedOutlines.push(outline)
|
|
||||||
} else if (!parentOutline.children) {
|
|
||||||
// first outline in this node
|
|
||||||
parentOutline.children = [outline]
|
|
||||||
} else {
|
|
||||||
// push outline to node
|
|
||||||
parentOutline.children.push(outline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the outline as new parent at its level and forget lower-level
|
|
||||||
// outlines (if any) as they shouldn't get any children anymore
|
|
||||||
parentOutlines[outline.level] = outline
|
|
||||||
parentOutlineLevels
|
|
||||||
.filter(level => level > outline.level)
|
|
||||||
.forEach(level => delete parentOutlines[level])
|
|
||||||
})
|
|
||||||
return nestedOutlines
|
|
||||||
}
|
|
||||||
|
|
||||||
export { matchOutline, matchOutlineFromLines, nestOutline }
|
|
|
@ -1,93 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
|
||||||
import {
|
|
||||||
hasSeenCM6SwitchAwaySurvey,
|
|
||||||
setHasSeenCM6SwitchAwaySurvey,
|
|
||||||
} from '../utils/switch-away-survey'
|
|
||||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
|
||||||
|
|
||||||
type CM6SwitchAwaySurveyState = 'disabled' | 'enabled' | 'shown'
|
|
||||||
|
|
||||||
export default function CM6SwitchAwaySurvey() {
|
|
||||||
const [state, setState] = useState<CM6SwitchAwaySurveyState>('disabled')
|
|
||||||
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If the user has previously seen any switch-away survey, then don't show
|
|
||||||
// the current one
|
|
||||||
if (hasSeenCM6SwitchAwaySurvey()) return
|
|
||||||
|
|
||||||
if (!newSourceEditor) {
|
|
||||||
setState('enabled')
|
|
||||||
} else {
|
|
||||||
setState('disabled')
|
|
||||||
}
|
|
||||||
}, [newSourceEditor])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = () => {
|
|
||||||
const TIME_FOR_SURVEY_TO_APPEAR = 3000
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (state === 'enabled') {
|
|
||||||
setState('shown')
|
|
||||||
setHasSeenCM6SwitchAwaySurvey()
|
|
||||||
}
|
|
||||||
}, TIME_FOR_SURVEY_TO_APPEAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
// can't access the ace editor directly, so add the keydown event
|
|
||||||
// to window
|
|
||||||
window?.addEventListener('keydown', handleKeyDown, { once: true })
|
|
||||||
|
|
||||||
return () => window?.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setState('disabled')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleFollowLink = useCallback(() => {
|
|
||||||
sendMB('cm6-switch-away-survey')
|
|
||||||
setState('disabled')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (state !== 'shown') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="alert alert-success cm6-switch-away-survey" role="alert">
|
|
||||||
<Button
|
|
||||||
className="close"
|
|
||||||
data-dismiss="alert"
|
|
||||||
aria-label="Close"
|
|
||||||
onClick={handleClose}
|
|
||||||
bsStyle={null}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</Button>
|
|
||||||
<div className="warning-content">
|
|
||||||
<div>
|
|
||||||
<div className="warning-text">
|
|
||||||
We noticed that you're still using the{' '}
|
|
||||||
<strong>Source (legacy)</strong> editor.
|
|
||||||
</div>
|
|
||||||
<div className="warning-text">Could you let us know why?</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'inline-flex' }}>
|
|
||||||
<a
|
|
||||||
href="https://forms.gle/Ygv8gLZ4N8LepQj56"
|
|
||||||
className="btn btn-sm btn-info"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
onClick={handleFollowLink}
|
|
||||||
>
|
|
||||||
Take survey
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ import EditorSwitch from './editor-switch'
|
||||||
import SwitchToPDFButton from './switch-to-pdf-button'
|
import 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
|
||||||
|
|
|
@ -1,172 +0,0 @@
|
||||||
import { ChangeEvent, FC, memo, useCallback } from 'react'
|
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
|
||||||
import Tooltip from '../../../shared/components/tooltip'
|
|
||||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
|
||||||
import getMeta from '../../../utils/meta'
|
|
||||||
import isValidTeXFile from '../../../main/is-valid-tex-file'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
function Badge() {
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
Overleaf has upgraded the source editor.
|
|
||||||
<br />
|
|
||||||
You can still use the old editor by selecting "Source (legacy)".
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Click to learn more and give feedback
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
id="editor-switch"
|
|
||||||
description={content}
|
|
||||||
overlayProps={{
|
|
||||||
placement: 'bottom',
|
|
||||||
delayHide: 100,
|
|
||||||
}}
|
|
||||||
tooltipProps={{ className: 'tooltip-wide' }}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://forms.gle/GmSs6odZRKRp3VX98"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="info-badge"
|
|
||||||
>
|
|
||||||
<span className="sr-only">{content}</span>
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showLegacySourceEditor: boolean = getMeta('ol-showLegacySourceEditor')
|
|
||||||
|
|
||||||
function EditorSwitch() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [newSourceEditor, setNewSourceEditor] = useScopeValue(
|
|
||||||
'editor.newSourceEditor'
|
|
||||||
)
|
|
||||||
const [visual, setVisual] = useScopeValue('editor.showVisual')
|
|
||||||
|
|
||||||
const [docName] = useScopeValue('editor.open_doc_name')
|
|
||||||
const richTextAvailable = isValidTeXFile(docName)
|
|
||||||
// TODO: rename this after legacy & toolbar split tests are complete
|
|
||||||
const richTextOrVisual = richTextAvailable && visual
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
event => {
|
|
||||||
const editorType = event.target.value
|
|
||||||
|
|
||||||
switch (editorType) {
|
|
||||||
case 'ace':
|
|
||||||
setVisual(false)
|
|
||||||
setNewSourceEditor(false)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'cm6':
|
|
||||||
setVisual(false)
|
|
||||||
setNewSourceEditor(true)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'rich-text':
|
|
||||||
setVisual(true)
|
|
||||||
setNewSourceEditor(true)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMB('editor-switch-change', { editorType })
|
|
||||||
},
|
|
||||||
[setVisual, setNewSourceEditor]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="editor-toggle-switch">
|
|
||||||
{showLegacySourceEditor ? <Badge /> : null}
|
|
||||||
|
|
||||||
<fieldset className="toggle-switch">
|
|
||||||
<legend className="sr-only">Editor mode.</legend>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="editor"
|
|
||||||
value="cm6"
|
|
||||||
id="editor-switch-cm6"
|
|
||||||
className="toggle-switch-input"
|
|
||||||
checked={!richTextOrVisual && !!newSourceEditor}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label htmlFor="editor-switch-cm6" className="toggle-switch-label">
|
|
||||||
<span>{t('code_editor')}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{showLegacySourceEditor ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="editor"
|
|
||||||
value="ace"
|
|
||||||
id="editor-switch-ace"
|
|
||||||
className="toggle-switch-input"
|
|
||||||
checked={!richTextOrVisual && !newSourceEditor}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label htmlFor="editor-switch-ace" className="toggle-switch-label">
|
|
||||||
<span>Source (legacy)</span>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<RichTextToggle
|
|
||||||
checked={!!richTextOrVisual}
|
|
||||||
disabled={!richTextAvailable}
|
|
||||||
handleChange={handleChange}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const RichTextToggle: FC<{
|
|
||||||
checked: boolean
|
|
||||||
disabled: boolean
|
|
||||||
handleChange: (event: ChangeEvent<HTMLInputElement>) => void
|
|
||||||
}> = ({ checked, disabled, handleChange }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const toggle = (
|
|
||||||
<span>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="editor"
|
|
||||||
value="rich-text"
|
|
||||||
id="editor-switch-rich-text"
|
|
||||||
className="toggle-switch-input"
|
|
||||||
checked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<label htmlFor="editor-switch-rich-text" className="toggle-switch-label">
|
|
||||||
<span>{t('visual_editor')}</span>
|
|
||||||
</label>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
description={t('visual_editor_is_only_available_for_tex_files')}
|
|
||||||
id="rich-text-toggle-tooltip"
|
|
||||||
overlayProps={{ placement: 'bottom' }}
|
|
||||||
tooltipProps={{ className: 'tooltip-wide' }}
|
|
||||||
>
|
|
||||||
{toggle}
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return toggle
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(EditorSwitch)
|
|
|
@ -9,40 +9,28 @@ import { FeedbackBadge } from '@/shared/components/feedback-badge'
|
||||||
|
|
||||||
function EditorSwitch() {
|
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"
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { memo, useCallback, useEffect, useState } from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import customLocalStorage from '../../../infrastructure/local-storage'
|
|
||||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
|
||||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
|
||||||
|
|
||||||
export const LegacyEditorWarning = memo(function LegacyEditorWarning({
|
|
||||||
delay,
|
|
||||||
}: {
|
|
||||||
delay: number
|
|
||||||
}) {
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
|
|
||||||
const hasDismissedLegacyEditor = customLocalStorage.getItem(
|
|
||||||
'editor.has_dismissed_legacy_editor_warning'
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const showLegacyEditor =
|
|
||||||
!hasDismissedLegacyEditor && newSourceEditor === false
|
|
||||||
|
|
||||||
let timeoutId: number | undefined
|
|
||||||
|
|
||||||
if (showLegacyEditor) {
|
|
||||||
timeoutId = window.setTimeout(() => {
|
|
||||||
eventTracking.sendMB('legacy-editor-warning-prompt')
|
|
||||||
setShow(true)
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}, [hasDismissedLegacyEditor, newSourceEditor, delay])
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setShow(false)
|
|
||||||
customLocalStorage.setItem(
|
|
||||||
'editor.has_dismissed_legacy_editor_warning',
|
|
||||||
true
|
|
||||||
)
|
|
||||||
eventTracking.sendMB('legacy-editor-warning-dismiss')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
eventTracking.sendMB('legacy-editor-warning-link-click')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="alert alert-info legacy-editor-warning" role="alert">
|
|
||||||
<Button
|
|
||||||
className="close"
|
|
||||||
data-dismiss="alert"
|
|
||||||
aria-label="Close"
|
|
||||||
onClick={handleClose}
|
|
||||||
bsStyle={null}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</Button>
|
|
||||||
<div className="warning-content">
|
|
||||||
<div>We're retiring our Source (legacy) editor in late May 2023.</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="warning-link"
|
|
||||||
href="https://www.overleaf.com/blog/were-retiring-our-legacy-source-editor"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Read the blog post
|
|
||||||
</a>{' '}
|
|
||||||
to learn more and find out how to report any problems.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,9 +0,0 @@
|
||||||
import App from '../../../base'
|
|
||||||
import { react2angular } from 'react2angular'
|
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
|
||||||
import CM6SwitchAwaySurvey from '../components/cm6-switch-away-survey'
|
|
||||||
|
|
||||||
App.component(
|
|
||||||
'cm6SwitchAwaySurvey',
|
|
||||||
react2angular(rootContext.use(CM6SwitchAwaySurvey))
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
import App from '../../../base'
|
|
||||||
import { react2angular } from 'react2angular'
|
|
||||||
import EditorSwitch from '../components/editor-switch-legacy'
|
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
|
||||||
|
|
||||||
App.component('editorSwitch', react2angular(rootContext.use(EditorSwitch)))
|
|
|
@ -1,9 +0,0 @@
|
||||||
import App from '../../../base'
|
|
||||||
import { react2angular } from 'react2angular'
|
|
||||||
import { rootContext } from '../../../shared/context/root-context'
|
|
||||||
import { LegacyEditorWarning } from '../components/legacy-editor-warning'
|
|
||||||
|
|
||||||
App.component(
|
|
||||||
'legacyEditorWarning',
|
|
||||||
react2angular(rootContext.use(LegacyEditorWarning), ['delay'])
|
|
||||||
)
|
|
|
@ -15,8 +15,7 @@ import getMeta from '../../../../utils/meta'
|
||||||
|
|
||||||
// If a toolbar row sits alongside the review panel, the review panel entries need to be shifted down by 32px.
|
// 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.
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import localStorage from '../../../infrastructure/local-storage'
|
|
||||||
|
|
||||||
const surveyOneKey = 'editor.has_seen_cm6_switch_away_survey'
|
|
||||||
|
|
||||||
export function setHasSeenCM6SwitchAwaySurvey() {
|
|
||||||
localStorage.setItem(surveyOneKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasSeenCM6SwitchAwaySurvey() {
|
|
||||||
return !!localStorage.getItem(surveyOneKey)
|
|
||||||
}
|
|
|
@ -28,8 +28,6 @@ import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
|
||||||
import ReferencesManager from './ide/references/ReferencesManager'
|
import 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__(
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
let SafariScrollPatcher
|
|
||||||
|
|
||||||
export default SafariScrollPatcher = class SafariScrollPatcher {
|
|
||||||
constructor($scope) {
|
|
||||||
this.isOverAce = false // Flag to control if the pointer is over Ace.
|
|
||||||
this.pdfDiv = null
|
|
||||||
this.aceDiv = null
|
|
||||||
|
|
||||||
// Start listening to PDF wheel events when the pointer leaves the PDF region.
|
|
||||||
// P.S. This is the problem in a nutshell: although the pointer is elsewhere,
|
|
||||||
// wheel events keep being dispatched to the PDF.
|
|
||||||
this.handlePdfDivMouseLeave = () => {
|
|
||||||
return this.pdfDiv.addEventListener('wheel', this.dispatchToAce)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop listening to wheel events when the pointer enters the PDF region. If
|
|
||||||
// the pointer is over the PDF, native behaviour is adequate.
|
|
||||||
this.handlePdfDivMouseEnter = () => {
|
|
||||||
return this.pdfDiv.removeEventListener('wheel', this.dispatchToAce)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the "pointer over Ace" flag as false, when the mouse leaves its area.
|
|
||||||
this.handleAceDivMouseLeave = () => {
|
|
||||||
return (this.isOverAce = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the "pointer over Ace" flag as true, when the mouse enters its area.
|
|
||||||
this.handleAceDivMouseEnter = () => {
|
|
||||||
return (this.isOverAce = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
|
|
||||||
// If elements are already defined, clear existing event listeners and do
|
|
||||||
// the process again (grab elements, set listeners).
|
|
||||||
this.setListeners = () => {
|
|
||||||
this.isOverAce = false
|
|
||||||
|
|
||||||
// If elements aren't null, remove existing listeners.
|
|
||||||
if (this.pdfDiv != null) {
|
|
||||||
this.pdfDiv.removeEventListener(
|
|
||||||
'mouseleave',
|
|
||||||
this.handlePdfDivMouseLeave
|
|
||||||
)
|
|
||||||
this.pdfDiv.removeEventListener(
|
|
||||||
'mouseenter',
|
|
||||||
this.handlePdfDivMouseEnter
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.aceDiv != null) {
|
|
||||||
this.aceDiv.removeEventListener(
|
|
||||||
'mouseleave',
|
|
||||||
this.handleAceDivMouseLeave
|
|
||||||
)
|
|
||||||
this.aceDiv.removeEventListener(
|
|
||||||
'mouseenter',
|
|
||||||
this.handleAceDivMouseEnter
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab elements.
|
|
||||||
this.pdfDiv = document.querySelector('.pdfjs-viewer') // Grab the PDF div.
|
|
||||||
this.aceDiv = document.querySelector('.ace_content') // Also the editor.
|
|
||||||
|
|
||||||
// Set hover-related listeners.
|
|
||||||
if (this.pdfDiv != null) {
|
|
||||||
this.pdfDiv.addEventListener('mouseleave', this.handlePdfDivMouseLeave)
|
|
||||||
this.pdfDiv.addEventListener('mouseenter', this.handlePdfDivMouseEnter)
|
|
||||||
}
|
|
||||||
if (this.aceDiv != null) {
|
|
||||||
this.aceDiv.addEventListener('mouseleave', this.handleAceDivMouseLeave)
|
|
||||||
this.aceDiv.addEventListener('mouseenter', this.handleAceDivMouseEnter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for wheel events on the PDF.
|
|
||||||
// If the pointer is over Ace, grab the event, prevent default behaviour
|
|
||||||
// and dispatch it to Ace.
|
|
||||||
this.dispatchToAce = e => {
|
|
||||||
if (this.isOverAce) {
|
|
||||||
// If this is logged, the problem just happened: the event arrived
|
|
||||||
// here (the PDF wheel handler), but it should've gone to Ace.
|
|
||||||
|
|
||||||
// Small timeout - if we dispatch immediately, an exception is thrown.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Dispatch the exact same event to Ace (this will keep values
|
|
||||||
// values e.g. `wheelDelta` consistent with user interaction).
|
|
||||||
return this.aceDiv.dispatchEvent(e)
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// Avoid scrolling the PDF, as we assume this was intended to the
|
|
||||||
// editor.
|
|
||||||
return e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// "loaded" event is emitted from the pdfViewer controller $scope. This means
|
|
||||||
// that the previous PDF DOM element was destroyed and a new one is available,
|
|
||||||
// so we need to grab the elements and set the listeners again.
|
|
||||||
$scope.$on('loaded', () => {
|
|
||||||
return this.setListeners()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -85,46 +85,19 @@ export default Document = (function () {
|
||||||
this.connected = this.ide.socket.socket.connected
|
this.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 {
|
||||||
|
|
|
@ -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}`) ||
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
import { round } from 'lodash'
|
|
||||||
import grammarlyExtensionPresent from '../../shared/utils/grammarly'
|
|
||||||
import getMeta from '../../utils/meta'
|
|
||||||
import { debugConsole } from '@/utils/debugging'
|
|
||||||
|
|
||||||
const TIMER_DOM_UPDATE_NAME = 'Ace-DomUpdate'
|
|
||||||
const TIMER_MEASURE_NAME = 'Ace-Keypress-Measure'
|
|
||||||
|
|
||||||
const sessionStart = Date.now()
|
|
||||||
|
|
||||||
let performanceOptionsSupport = false
|
|
||||||
|
|
||||||
// Check that performance.mark and performance.measure accept an options object
|
|
||||||
try {
|
|
||||||
const testMarkName = 'featureTestMark'
|
|
||||||
performance.mark(testMarkName, {
|
|
||||||
startTime: performance.now(),
|
|
||||||
detail: { test: 1 },
|
|
||||||
})
|
|
||||||
performance.clearMarks(testMarkName)
|
|
||||||
|
|
||||||
const testMeasureName = 'featureTestMeasure'
|
|
||||||
performance.measure(testMeasureName, {
|
|
||||||
start: performance.now(),
|
|
||||||
detail: { test: 1 },
|
|
||||||
})
|
|
||||||
performance.clearMeasures(testMeasureName)
|
|
||||||
|
|
||||||
performanceOptionsSupport = true
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
let performanceMemorySupport = false
|
|
||||||
|
|
||||||
function measureMemoryUsage() {
|
|
||||||
// @ts-ignore
|
|
||||||
return performance.memory.usedJSHeapSize
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ('memory' in window.performance) {
|
|
||||||
measureMemoryUsage()
|
|
||||||
performanceMemorySupport = true
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
let keypressesSinceDomUpdateCount = 0
|
|
||||||
const unpaintedKeypressStartTimes: number[] = []
|
|
||||||
let animationFrameRequest: number | null = null
|
|
||||||
|
|
||||||
function timeInputToRender() {
|
|
||||||
if (!performanceOptionsSupport) return
|
|
||||||
|
|
||||||
++keypressesSinceDomUpdateCount
|
|
||||||
|
|
||||||
unpaintedKeypressStartTimes.push(performance.now())
|
|
||||||
|
|
||||||
if (!animationFrameRequest) {
|
|
||||||
animationFrameRequest = window.requestAnimationFrame(() => {
|
|
||||||
animationFrameRequest = null
|
|
||||||
|
|
||||||
performance.mark(TIMER_DOM_UPDATE_NAME, {
|
|
||||||
detail: { keypressesSinceDomUpdateCount },
|
|
||||||
})
|
|
||||||
keypressesSinceDomUpdateCount = 0
|
|
||||||
|
|
||||||
const keypressEnd = performance.now()
|
|
||||||
|
|
||||||
for (const keypressStart of unpaintedKeypressStartTimes) {
|
|
||||||
performance.measure(TIMER_MEASURE_NAME, {
|
|
||||||
start: keypressStart,
|
|
||||||
end: keypressEnd,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
unpaintedKeypressStartTimes.length = 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initAcePerfListener(textareaEl: HTMLTextAreaElement) {
|
|
||||||
textareaEl?.addEventListener('beforeinput', timeInputToRender)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tearDownAcePerfListener(textareaEl: HTMLTextAreaElement) {
|
|
||||||
textareaEl?.removeEventListener('beforeinput', timeInputToRender)
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateMean(durations: number[]) {
|
|
||||||
if (durations.length === 0) return 0
|
|
||||||
|
|
||||||
const sum = durations.reduce((acc, entry) => acc + entry, 0)
|
|
||||||
return sum / durations.length
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reportAcePerf() {
|
|
||||||
const durations = performance
|
|
||||||
.getEntriesByName(TIMER_MEASURE_NAME, 'measure')
|
|
||||||
.map(({ duration }) => duration)
|
|
||||||
|
|
||||||
performance.clearMeasures(TIMER_MEASURE_NAME)
|
|
||||||
|
|
||||||
const meanKeypressPaint = round(calculateMean(durations), 2)
|
|
||||||
|
|
||||||
const grammarly = grammarlyExtensionPresent()
|
|
||||||
const sessionLength = Math.floor((Date.now() - sessionStart) / 1000) // In seconds
|
|
||||||
|
|
||||||
const memory = performanceMemorySupport ? measureMemoryUsage() : null
|
|
||||||
|
|
||||||
// Get entries for keypress counts between DOM updates
|
|
||||||
const domUpdateEntries = performance.getEntriesByName(
|
|
||||||
TIMER_DOM_UPDATE_NAME,
|
|
||||||
'mark'
|
|
||||||
) as PerformanceMark[]
|
|
||||||
|
|
||||||
performance.clearMarks(TIMER_DOM_UPDATE_NAME)
|
|
||||||
|
|
||||||
let lags = 0
|
|
||||||
let nonLags = 0
|
|
||||||
let longestLag = 0
|
|
||||||
let totalKeypressCount = 0
|
|
||||||
|
|
||||||
for (const entry of domUpdateEntries) {
|
|
||||||
const keypressCount = entry.detail.keypressesSinceDomUpdateCount
|
|
||||||
if (keypressCount === 1) {
|
|
||||||
++nonLags
|
|
||||||
} else if (keypressCount > 1) {
|
|
||||||
++lags
|
|
||||||
}
|
|
||||||
if (keypressCount > longestLag) {
|
|
||||||
longestLag = keypressCount
|
|
||||||
}
|
|
||||||
totalKeypressCount += keypressCount
|
|
||||||
}
|
|
||||||
|
|
||||||
const meanLagsPerMeasure = round(lags / (lags + nonLags), 4)
|
|
||||||
const meanKeypressesPerMeasure = round(
|
|
||||||
totalKeypressCount / (lags + nonLags),
|
|
||||||
4
|
|
||||||
)
|
|
||||||
|
|
||||||
const release = getMeta('ol-ExposedSettings')?.sentryRelease || null
|
|
||||||
|
|
||||||
return {
|
|
||||||
numberOfEntries: durations.length,
|
|
||||||
meanKeypressPaint,
|
|
||||||
grammarly,
|
|
||||||
sessionLength,
|
|
||||||
memory,
|
|
||||||
lags,
|
|
||||||
nonLags,
|
|
||||||
longestLag,
|
|
||||||
meanLagsPerMeasure,
|
|
||||||
meanKeypressesPerMeasure,
|
|
||||||
release,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window._reportAcePerf = () => {
|
|
||||||
debugConsole.warn(reportAcePerf())
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import App from '../../../base'
|
|
||||||
|
|
||||||
export default App.component('spellMenu', {
|
|
||||||
bindings: {
|
|
||||||
open: '<',
|
|
||||||
top: '<',
|
|
||||||
left: '<',
|
|
||||||
layoutFromBottom: '<',
|
|
||||||
highlight: '<',
|
|
||||||
replaceWord: '&',
|
|
||||||
learnWord: '&',
|
|
||||||
},
|
|
||||||
template: `\
|
|
||||||
<div
|
|
||||||
class="dropdown context-menu spell-check-menu"
|
|
||||||
ng-show="$ctrl.open"
|
|
||||||
ng-style="{top: $ctrl.top, left: $ctrl.left}"
|
|
||||||
ng-class="{open: $ctrl.open, 'spell-check-menu-from-bottom': $ctrl.layoutFromBottom}"
|
|
||||||
>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li ng-repeat="suggestion in $ctrl.highlight.suggestions | limitTo:8">
|
|
||||||
<button
|
|
||||||
class="btn-link text-left dropdown-menu-button"
|
|
||||||
ng-click="$ctrl.replaceWord({ highlight: $ctrl.highlight, suggestion: suggestion })"
|
|
||||||
>
|
|
||||||
{{ suggestion }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="divider"></li>
|
|
||||||
<li>
|
|
||||||
<a href ng-click="$ctrl.learnWord({ highlight: $ctrl.highlight })">Add to Dictionary</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>\
|
|
||||||
`,
|
|
||||||
})
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,473 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
import CommandManager from './CommandManager'
|
|
||||||
import EnvironmentManager from './EnvironmentManager'
|
|
||||||
import PackageManager from './PackageManager'
|
|
||||||
import Helpers from './Helpers'
|
|
||||||
import 'ace/ace'
|
|
||||||
import 'ace/ext-language_tools'
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
const aceSnippetManager = ace.require('ace/snippets').snippetManager
|
|
||||||
|
|
||||||
class AutoCompleteManager {
|
|
||||||
constructor(
|
|
||||||
$scope,
|
|
||||||
editor,
|
|
||||||
element,
|
|
||||||
metadataManager,
|
|
||||||
graphics,
|
|
||||||
preamble,
|
|
||||||
files
|
|
||||||
) {
|
|
||||||
this.$scope = $scope
|
|
||||||
this.editor = editor
|
|
||||||
this.element = element
|
|
||||||
this.metadataManager = metadataManager
|
|
||||||
this.graphics = graphics
|
|
||||||
this.preamble = preamble
|
|
||||||
this.files = files
|
|
||||||
this.monkeyPatchAutocomplete()
|
|
||||||
|
|
||||||
this.$scope.$watch('autoComplete', autocomplete => {
|
|
||||||
if (autocomplete) {
|
|
||||||
this.enable()
|
|
||||||
} else {
|
|
||||||
this.disable()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onChange = change => {
|
|
||||||
this.onChange(change)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor.on('changeSession', e => {
|
|
||||||
e.oldSession.off('change', onChange)
|
|
||||||
e.session.on('change', onChange)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
enable() {
|
|
||||||
this.editor.setOptions({
|
|
||||||
enableBasicAutocompletion: true,
|
|
||||||
enableSnippets: true,
|
|
||||||
enableLiveAutocompletion: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const CommandCompleter = new CommandManager(this.metadataManager)
|
|
||||||
const SnippetCompleter = new EnvironmentManager()
|
|
||||||
const PackageCompleter = new PackageManager(this.metadataManager, Helpers)
|
|
||||||
|
|
||||||
const Graphics = this.graphics
|
|
||||||
const Preamble = this.preamble
|
|
||||||
const Files = this.files
|
|
||||||
|
|
||||||
const GraphicsCompleter = {
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
|
||||||
if (commandFragment) {
|
|
||||||
const match = commandFragment.match(
|
|
||||||
/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/
|
|
||||||
)
|
|
||||||
if (match) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const commandName = match[1]
|
|
||||||
const graphicsPaths = Preamble.getGraphicsPaths()
|
|
||||||
const result = []
|
|
||||||
for (const graphic of Graphics.getGraphicsFiles()) {
|
|
||||||
let { path } = graphic
|
|
||||||
for (const graphicsPath of graphicsPaths) {
|
|
||||||
if (path.indexOf(graphicsPath) === 0) {
|
|
||||||
path = path.slice(graphicsPath.length)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{${path}}`,
|
|
||||||
value: `\\${commandName}{${path}}`,
|
|
||||||
meta: 'graphic',
|
|
||||||
score: 50,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
callback(null, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const { metadataManager } = this
|
|
||||||
const FilesCompleter = {
|
|
||||||
getCompletions: (editor, session, pos, prefix, callback) => {
|
|
||||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
|
||||||
if (commandFragment) {
|
|
||||||
const match = commandFragment.match(/^\\(input|include){(\w*)/)
|
|
||||||
if (match) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const commandName = match[1]
|
|
||||||
const result = []
|
|
||||||
for (const file of Files.getTeXFiles()) {
|
|
||||||
if (file.id !== this.$scope.docId && !file.deleted && file.path) {
|
|
||||||
const { path } = file
|
|
||||||
const cleanPath = path.replace(/(.+)\.tex$/i, '$1')
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{${path}}`,
|
|
||||||
value: `\\${commandName}{${cleanPath}}`,
|
|
||||||
meta: 'file',
|
|
||||||
score: 50,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback(null, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const LabelsCompleter = {
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
|
||||||
if (commandFragment) {
|
|
||||||
const refMatch = commandFragment.match(
|
|
||||||
/^~?\\([a-zA-Z]*ref){([^}]*, *)?(\w*)/
|
|
||||||
)
|
|
||||||
if (refMatch) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const commandName = refMatch[1]
|
|
||||||
const result = []
|
|
||||||
if (commandName !== 'ref') {
|
|
||||||
// ref is in top 100 commands
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{}`,
|
|
||||||
snippet: `\\${commandName}{}`,
|
|
||||||
meta: 'cross-reference',
|
|
||||||
score: 60,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (const label of metadataManager.getAllLabels()) {
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{${label}}`,
|
|
||||||
value: `\\${commandName}{${label}}`,
|
|
||||||
meta: 'cross-reference',
|
|
||||||
score: 50,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
callback(null, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const references = this.$scope.$root._references
|
|
||||||
const ReferencesCompleter = {
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
const { commandFragment } = Helpers.getContext(editor, pos)
|
|
||||||
if (commandFragment) {
|
|
||||||
const citeMatch = commandFragment.match(
|
|
||||||
/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*, *)?(\w*)/
|
|
||||||
)
|
|
||||||
if (citeMatch) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
let [_ignore, commandName, previousArgs] = citeMatch
|
|
||||||
if (previousArgs == null) {
|
|
||||||
previousArgs = ''
|
|
||||||
}
|
|
||||||
const previousArgsCaption =
|
|
||||||
previousArgs.length > 8 ? '…,' : previousArgs
|
|
||||||
const result = []
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{}`,
|
|
||||||
snippet: `\\${commandName}{}`,
|
|
||||||
meta: 'reference',
|
|
||||||
score: 60,
|
|
||||||
})
|
|
||||||
if (references.keys && references.keys.length > 0) {
|
|
||||||
references.keys.forEach(function (key) {
|
|
||||||
if (key != null) {
|
|
||||||
result.push({
|
|
||||||
caption: `\\${commandName}{${previousArgsCaption}${key}}`,
|
|
||||||
value: `\\${commandName}{${previousArgs}${key}}`,
|
|
||||||
meta: 'reference',
|
|
||||||
score: 50,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
callback(null, result)
|
|
||||||
} else {
|
|
||||||
callback(null, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor.completers = [
|
|
||||||
CommandCompleter,
|
|
||||||
SnippetCompleter,
|
|
||||||
PackageCompleter,
|
|
||||||
ReferencesCompleter,
|
|
||||||
LabelsCompleter,
|
|
||||||
GraphicsCompleter,
|
|
||||||
FilesCompleter,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
return this.editor.setOptions({
|
|
||||||
enableBasicAutocompletion: false,
|
|
||||||
enableSnippets: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(change) {
|
|
||||||
let i
|
|
||||||
const cursorPosition = this.editor.getCursorPosition()
|
|
||||||
const { end } = change
|
|
||||||
const { lineUpToCursor, commandFragment } = Helpers.getContext(
|
|
||||||
this.editor,
|
|
||||||
end
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
(i = lineUpToCursor.indexOf('%')) > -1 &&
|
|
||||||
lineUpToCursor[i - 1] !== '\\'
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const lastCharIsBackslash = lineUpToCursor.slice(-1) === '\\'
|
|
||||||
const lastTwoChars = lineUpToCursor.slice(-2)
|
|
||||||
// Don't offer autocomplete on double-backslash, backslash-colon, etc
|
|
||||||
if (/^\\[^a-zA-Z]$/.test(lastTwoChars)) {
|
|
||||||
if (this.editor.completer) {
|
|
||||||
this.editor.completer.detach()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check that this change was made by us, not a collaborator
|
|
||||||
// (Cursor is still one place behind)
|
|
||||||
// NOTE: this is also the case when a user backspaces over a highlighted
|
|
||||||
// region
|
|
||||||
if (
|
|
||||||
change.origin !== 'remote' &&
|
|
||||||
change.action === 'insert' &&
|
|
||||||
end.row === cursorPosition.row &&
|
|
||||||
end.column === cursorPosition.column + 1
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
(commandFragment != null ? commandFragment.length : undefined) > 2 ||
|
|
||||||
lastCharIsBackslash
|
|
||||||
) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.editor.execCommand('startAutocomplete')
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const match = change.lines[0].match(/\\(\w+){}/)
|
|
||||||
if (
|
|
||||||
change.action === 'insert' &&
|
|
||||||
match &&
|
|
||||||
match[1] &&
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
/(begin|end|[a-zA-Z]*ref|usepackage|[a-z]*cite[a-z]*|input|include)/.test(
|
|
||||||
match[1]
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return setTimeout(() => {
|
|
||||||
this.editor.execCommand('startAutocomplete')
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeyPatchAutocomplete() {
|
|
||||||
const { Autocomplete } = ace.require('ace/autocomplete')
|
|
||||||
const Util = ace.require('ace/autocomplete/util')
|
|
||||||
|
|
||||||
if (Autocomplete.prototype._insertMatch == null) {
|
|
||||||
// Only override this once since it's global but we may create multiple
|
|
||||||
// autocomplete handlers
|
|
||||||
Autocomplete.prototype._insertMatch = Autocomplete.prototype.insertMatch
|
|
||||||
Autocomplete.prototype.insertMatch = function (data) {
|
|
||||||
const { editor } = this
|
|
||||||
|
|
||||||
const pos = editor.getCursorPosition()
|
|
||||||
let range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
|
|
||||||
const nextChar = editor.session.getTextRange(range)
|
|
||||||
// If we are in \begin{it|}, then we need to remove the trailing }
|
|
||||||
// since it will be adding in with the autocomplete of \begin{item}...
|
|
||||||
if (
|
|
||||||
/^\\\w+(\[[\w\\,=. ]*\])?{/.test(this.completions.filterText) &&
|
|
||||||
nextChar === '}'
|
|
||||||
) {
|
|
||||||
editor.session.remove(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide our own `insertMatch` implementation.
|
|
||||||
// See the `insertMatch` method of Autocomplete in
|
|
||||||
// `ext-language_tools.js`.
|
|
||||||
// We need this to account for editing existing commands, particularly
|
|
||||||
// when adding a prefix.
|
|
||||||
// We fix this by detecting when the cursor is in the middle of an
|
|
||||||
// existing command, and adjusting the insertions/deletions
|
|
||||||
// accordingly.
|
|
||||||
// Example:
|
|
||||||
// when changing `\ref{}` to `\href{}`, ace default behaviour
|
|
||||||
// is likely to end up with `\href{}ref{}`
|
|
||||||
if (data == null) {
|
|
||||||
const { completions } = this
|
|
||||||
const { popup } = this
|
|
||||||
data = popup.getData(popup.getRow())
|
|
||||||
data.completer = {
|
|
||||||
insertMatch(editor, matchData) {
|
|
||||||
for (range of editor.selection.getAllRanges()) {
|
|
||||||
const leftRange = _.clone(range)
|
|
||||||
const rightRange = _.clone(range)
|
|
||||||
// trim to left of cursor
|
|
||||||
const lineUpToCursor = editor
|
|
||||||
.getSession()
|
|
||||||
.getTextRange(
|
|
||||||
new Range(
|
|
||||||
range.start.row,
|
|
||||||
0,
|
|
||||||
range.start.row,
|
|
||||||
range.start.column
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// Delete back to command start, as appropriate
|
|
||||||
const commandStartIndex =
|
|
||||||
Helpers.getLastCommandFragmentIndex(lineUpToCursor)
|
|
||||||
if (commandStartIndex !== -1) {
|
|
||||||
leftRange.start.column = commandStartIndex
|
|
||||||
} else {
|
|
||||||
leftRange.start.column -= completions.filterText.length
|
|
||||||
}
|
|
||||||
editor.session.remove(leftRange)
|
|
||||||
// look at text after cursor
|
|
||||||
const lineBeyondCursor = editor
|
|
||||||
.getSession()
|
|
||||||
.getTextRange(
|
|
||||||
new Range(
|
|
||||||
rightRange.start.row,
|
|
||||||
rightRange.start.column,
|
|
||||||
rightRange.end.row,
|
|
||||||
99999
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (lineBeyondCursor) {
|
|
||||||
const partialCommandMatch =
|
|
||||||
lineBeyondCursor.match(/^([a-zA-Z0-9]+)\{/)
|
|
||||||
if (partialCommandMatch) {
|
|
||||||
// We've got a partial command after the cursor
|
|
||||||
const commandTail = partialCommandMatch[1]
|
|
||||||
// remove rest of the partial command, right of cursor
|
|
||||||
rightRange.end.column +=
|
|
||||||
commandTail.length - completions.filterText.length
|
|
||||||
editor.session.remove(rightRange)
|
|
||||||
// trim the completion text to just the command, without
|
|
||||||
// braces or brackets
|
|
||||||
// example: '\cite{}' -> '\cite'
|
|
||||||
if (matchData.snippet != null) {
|
|
||||||
matchData.snippet = matchData.snippet.replace(
|
|
||||||
/[{[].*[}\]]/,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (matchData.caption != null) {
|
|
||||||
matchData.caption = matchData.caption.replace(
|
|
||||||
/[{[].*[}\]]/,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (matchData.value != null) {
|
|
||||||
matchData.value = matchData.value.replace(
|
|
||||||
/[{[].*[}\]]/,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const inArgument = lineBeyondCursor.match(/^([\w._-]+)\}(.*)/)
|
|
||||||
if (inArgument) {
|
|
||||||
const argumentRightOfCursor = inArgument[1]
|
|
||||||
const afterArgument = inArgument[2]
|
|
||||||
if (afterArgument) {
|
|
||||||
rightRange.end.column =
|
|
||||||
rightRange.start.column +
|
|
||||||
argumentRightOfCursor.length +
|
|
||||||
1
|
|
||||||
}
|
|
||||||
editor.session.remove(rightRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// finally, insert the match
|
|
||||||
if (matchData.snippet) {
|
|
||||||
aceSnippetManager.insertSnippet(editor, matchData.snippet)
|
|
||||||
} else {
|
|
||||||
editor.execCommand('insertstring', matchData.value || matchData)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Autocomplete.prototype._insertMatch.call(this, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite this to set autoInsert = false and set font size
|
|
||||||
Autocomplete.startCommand = {
|
|
||||||
name: 'startAutocomplete',
|
|
||||||
exec: editor => {
|
|
||||||
if (!editor.completer) {
|
|
||||||
editor.completer = new Autocomplete()
|
|
||||||
}
|
|
||||||
editor.completer.autoInsert = false
|
|
||||||
editor.completer.autoSelect = true
|
|
||||||
editor.completer.showPopup(editor)
|
|
||||||
editor.completer.cancelContextMenu()
|
|
||||||
const container = $(
|
|
||||||
editor.completer.popup != null
|
|
||||||
? editor.completer.popup.container
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
container.css({ 'font-size': this.$scope.fontSize + 'px' })
|
|
||||||
// Dynamically set width of autocomplete popup
|
|
||||||
const filtered =
|
|
||||||
editor.completer.completions &&
|
|
||||||
editor.completer.completions.filtered
|
|
||||||
if (filtered) {
|
|
||||||
const longestCaption = _.max(filtered.map(c => c.caption.length))
|
|
||||||
const longestMeta = _.max(filtered.map(c => c.meta.length))
|
|
||||||
const charWidth = editor.renderer.characterWidth
|
|
||||||
// between 280 and 700 px
|
|
||||||
const width = Math.max(
|
|
||||||
Math.min(
|
|
||||||
Math.round(
|
|
||||||
longestCaption * charWidth +
|
|
||||||
longestMeta * charWidth +
|
|
||||||
5 * charWidth
|
|
||||||
),
|
|
||||||
700
|
|
||||||
),
|
|
||||||
280
|
|
||||||
)
|
|
||||||
container.css({ width: `${width}px` })
|
|
||||||
}
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
editor.completer.detach()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bindKey: 'Ctrl-Space|Ctrl-Shift-Space|Alt-Space',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Util.retrievePrecedingIdentifier = function (text, pos, regex) {
|
|
||||||
let currentLineOffset = 0
|
|
||||||
for (let i = pos - 1; i <= 0; i++) {
|
|
||||||
if (text[i] === '\n') {
|
|
||||||
currentLineOffset = i + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const currentLine = text.slice(currentLineOffset, pos)
|
|
||||||
const fragment = Helpers.getLastCommandFragment(currentLine) || ''
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoCompleteManager
|
|
|
@ -1,241 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* DS206: Consider reworking classes to avoid initClass
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import topHundred from './snippets/TopHundredSnippets'
|
|
||||||
let CommandManager
|
|
||||||
class Parser {
|
|
||||||
static initClass() {
|
|
||||||
// Ignore single letter commands since auto complete is moot then.
|
|
||||||
this.prototype.commandRegex = /\\([a-zA-Z]{2,})/
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(doc, prefix) {
|
|
||||||
this.doc = doc
|
|
||||||
this.prefix = prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
parse() {
|
|
||||||
// Safari regex is super slow, freezes browser for minutes on end,
|
|
||||||
// hacky solution: limit iterations
|
|
||||||
let command
|
|
||||||
let limit = null
|
|
||||||
if (
|
|
||||||
__guard__(
|
|
||||||
typeof window !== 'undefined' && window !== null
|
|
||||||
? window._ide
|
|
||||||
: undefined,
|
|
||||||
x => x.browserIsSafari
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
limit = 5000
|
|
||||||
}
|
|
||||||
|
|
||||||
// fully formed commands
|
|
||||||
const realCommands = []
|
|
||||||
// commands which match the prefix exactly,
|
|
||||||
// and could be partially typed or malformed
|
|
||||||
const incidentalCommands = []
|
|
||||||
const seen = {}
|
|
||||||
let iterations = 0
|
|
||||||
while ((command = this.nextCommand())) {
|
|
||||||
iterations += 1
|
|
||||||
if (limit && iterations > limit) {
|
|
||||||
return realCommands
|
|
||||||
}
|
|
||||||
|
|
||||||
const docState = this.doc
|
|
||||||
|
|
||||||
let optionalArgs = 0
|
|
||||||
while (this.consumeArgument('[', ']')) {
|
|
||||||
optionalArgs++
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = 0
|
|
||||||
while (this.consumeArgument('{', '}')) {
|
|
||||||
args++
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandHash = `${command}\\${optionalArgs}\\${args}`
|
|
||||||
|
|
||||||
if (this.prefix != null && `\\${command}` === this.prefix) {
|
|
||||||
incidentalCommands.push([command, optionalArgs, args])
|
|
||||||
} else {
|
|
||||||
if (seen[commandHash] == null) {
|
|
||||||
seen[commandHash] = true
|
|
||||||
realCommands.push([command, optionalArgs, args])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to before argument to handle nested commands
|
|
||||||
this.doc = docState
|
|
||||||
}
|
|
||||||
|
|
||||||
// check incidentals, see if we should pluck out a match
|
|
||||||
if (incidentalCommands.length > 1) {
|
|
||||||
const bestMatch = incidentalCommands.sort(
|
|
||||||
(a, b) => b[1] + b[2] - (a[1] + a[2])
|
|
||||||
)[0]
|
|
||||||
realCommands.push(bestMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return realCommands
|
|
||||||
}
|
|
||||||
|
|
||||||
nextCommand() {
|
|
||||||
const i = this.doc.search(this.commandRegex)
|
|
||||||
if (i === -1) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
const match = this.doc.match(this.commandRegex)[1]
|
|
||||||
this.doc = this.doc.substr(i + match.length + 1)
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
consumeWhitespace() {
|
|
||||||
const match = this.doc.match(/^[ \t\n]*/m)[0]
|
|
||||||
return (this.doc = this.doc.substr(match.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
consumeArgument(openingBracket, closingBracket) {
|
|
||||||
this.consumeWhitespace()
|
|
||||||
|
|
||||||
if (this.doc[0] === openingBracket) {
|
|
||||||
let i = 1
|
|
||||||
let bracketParity = 1
|
|
||||||
while (bracketParity > 0 && i < this.doc.length) {
|
|
||||||
if (this.doc[i] === openingBracket) {
|
|
||||||
bracketParity++
|
|
||||||
} else if (this.doc[i] === closingBracket) {
|
|
||||||
bracketParity--
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bracketParity === 0) {
|
|
||||||
this.doc = this.doc.substr(i)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Parser.initClass()
|
|
||||||
|
|
||||||
export default CommandManager = class CommandManager {
|
|
||||||
constructor(metadataManager) {
|
|
||||||
this.metadataManager = metadataManager
|
|
||||||
}
|
|
||||||
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
const commandNames = {}
|
|
||||||
for (const snippet of Array.from(topHundred)) {
|
|
||||||
commandNames[snippet.caption.match(/\w+/)[0]] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const packages = this.metadataManager.getAllPackages()
|
|
||||||
const packageCommands = []
|
|
||||||
for (const pkg in packages) {
|
|
||||||
const snippets = packages[pkg]
|
|
||||||
for (const snippet of Array.from(snippets)) {
|
|
||||||
packageCommands.push(snippet)
|
|
||||||
commandNames[snippet.caption.match(/\w+/)[0]] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = session.getValue()
|
|
||||||
const parser = new Parser(doc, prefix)
|
|
||||||
const commands = parser.parse()
|
|
||||||
let completions = []
|
|
||||||
for (const command of Array.from(commands)) {
|
|
||||||
if (!commandNames[command[0]]) {
|
|
||||||
let caption = `\\${command[0]}`
|
|
||||||
const score = caption === prefix ? 99 : 50
|
|
||||||
let snippet = caption
|
|
||||||
let i = 1
|
|
||||||
_.times(command[1], function () {
|
|
||||||
snippet += `[\${${i}}]`
|
|
||||||
caption += '[]'
|
|
||||||
return i++
|
|
||||||
})
|
|
||||||
_.times(command[2], function () {
|
|
||||||
snippet += `{\${${i}}}`
|
|
||||||
caption += '{}'
|
|
||||||
return i++
|
|
||||||
})
|
|
||||||
completions.push({
|
|
||||||
caption,
|
|
||||||
snippet,
|
|
||||||
meta: 'cmd',
|
|
||||||
score,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completions = completions.concat(topHundred, packageCommands)
|
|
||||||
|
|
||||||
return callback(null, completions)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCommandsFromDoc(doc) {
|
|
||||||
const parser = new Parser(doc)
|
|
||||||
return (this.commands = parser.parse())
|
|
||||||
}
|
|
||||||
|
|
||||||
getSuggestions(commandFragment) {
|
|
||||||
const matchingCommands = _.filter(
|
|
||||||
this.commands,
|
|
||||||
command => command[0].slice(0, commandFragment.length) === commandFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
return _.map(matchingCommands, function (command) {
|
|
||||||
let completionAfterCursor, completionBeforeCursor
|
|
||||||
const base = `\\${commandFragment}`
|
|
||||||
|
|
||||||
let args = ''
|
|
||||||
_.times(command[1], () => (args = args + '[]'))
|
|
||||||
_.times(command[2], () => (args = args + '{}'))
|
|
||||||
const completionBase = command[0].slice(commandFragment.length)
|
|
||||||
|
|
||||||
const squareArgsNo = command[1]
|
|
||||||
const curlyArgsNo = command[2]
|
|
||||||
const totalArgs = squareArgsNo + curlyArgsNo
|
|
||||||
if (totalArgs === 0) {
|
|
||||||
completionBeforeCursor = completionBase
|
|
||||||
completionAfterCursor = ''
|
|
||||||
} else {
|
|
||||||
completionBeforeCursor = completionBase + args[0]
|
|
||||||
completionAfterCursor = args.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
base,
|
|
||||||
completion: completionBase + args,
|
|
||||||
completionBeforeCursor,
|
|
||||||
completionAfterCursor,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import Environments from './snippets/Environments'
|
|
||||||
let staticSnippets = Array.from(Environments.withoutSnippets).map(env => ({
|
|
||||||
caption: `\\begin{${env}}...`,
|
|
||||||
snippet: `\
|
|
||||||
\\begin{${env}}
|
|
||||||
\t$1
|
|
||||||
\\end{${env}}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
}))
|
|
||||||
|
|
||||||
staticSnippets = staticSnippets.concat([
|
|
||||||
{
|
|
||||||
caption: '\\begin{array}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{array}{\${1:cc}}
|
|
||||||
\t$2 & $3 \\\\\\\\
|
|
||||||
\t$4 & $5
|
|
||||||
\\end{array}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{figure}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{figure}
|
|
||||||
\t\\centering
|
|
||||||
\t\\includegraphics{$1}
|
|
||||||
\t\\caption{\${2:Caption}}
|
|
||||||
\t\\label{\${3:fig:my_label}}
|
|
||||||
\\end{figure}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{tabular}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{tabular}{\${1:c|c}}
|
|
||||||
\t$2 & $3 \\\\\\\\
|
|
||||||
\t$4 & $5
|
|
||||||
\\end{tabular}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{table}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{table}[$1]
|
|
||||||
\t\\centering
|
|
||||||
\t\\begin{tabular}{\${2:c|c}}
|
|
||||||
\t\t$3 & $4 \\\\\\\\
|
|
||||||
\t\t$5 & $6
|
|
||||||
\t\\end{tabular}
|
|
||||||
\t\\caption{\${7:Caption}}
|
|
||||||
\t\\label{\${8:tab:my_label}}
|
|
||||||
\\end{table}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{list}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{list}
|
|
||||||
\t\\item $1
|
|
||||||
\\end{list}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{enumerate}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{enumerate}
|
|
||||||
\t\\item $1
|
|
||||||
\\end{enumerate}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{itemize}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{itemize}
|
|
||||||
\t\\item $1
|
|
||||||
\\end{itemize}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\begin{frame}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{frame}{\${1:Frame Title}}
|
|
||||||
\t$2
|
|
||||||
\\end{frame}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const documentSnippet = {
|
|
||||||
caption: '\\begin{document}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{document}
|
|
||||||
$1
|
|
||||||
\\end{document}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
}
|
|
||||||
|
|
||||||
const bibliographySnippet = {
|
|
||||||
caption: '\\begin{thebibliography}...',
|
|
||||||
snippet: `\
|
|
||||||
\\begin{thebibliography}{$1}
|
|
||||||
\\bibitem{$2}
|
|
||||||
$3
|
|
||||||
\\end{thebibliography}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
}
|
|
||||||
staticSnippets.push(documentSnippet)
|
|
||||||
|
|
||||||
const parseCustomEnvironments = function (text) {
|
|
||||||
let match
|
|
||||||
const re = /^\\newenvironment{(\w+)}.*$/gm
|
|
||||||
const result = []
|
|
||||||
let iterations = 0
|
|
||||||
while ((match = re.exec(text))) {
|
|
||||||
result.push({ name: match[1], whitespace: null })
|
|
||||||
iterations += 1
|
|
||||||
if (iterations >= 1000) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseBeginCommands = function (text) {
|
|
||||||
let match
|
|
||||||
const re = /^([\t ]*)\\begin{(\w+)}.*\n([\t ]*)/gm
|
|
||||||
const result = []
|
|
||||||
let iterations = 0
|
|
||||||
while ((match = re.exec(text))) {
|
|
||||||
const whitespaceAlignment = match[3].replace(match[1] || '', '')
|
|
||||||
if (
|
|
||||||
!Array.from(Environments.all).includes(match[2]) &&
|
|
||||||
match[2] !== 'document'
|
|
||||||
) {
|
|
||||||
result.push({ name: match[2], whitespace: whitespaceAlignment })
|
|
||||||
iterations += 1
|
|
||||||
if (iterations >= 1000) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
re.lastIndex = match.index + 1
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDocumentEnvironment = function (text) {
|
|
||||||
const re = /^\\begin{document}/m
|
|
||||||
return re.exec(text) !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBibliographyEnvironment = function (text) {
|
|
||||||
const re = /^\\begin{thebibliography}/m
|
|
||||||
return re.exec(text) !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
class EnvironmentManager {
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
let ind
|
|
||||||
const docText = session.getValue()
|
|
||||||
const customEnvironments = parseCustomEnvironments(docText)
|
|
||||||
const beginCommands = parseBeginCommands(docText)
|
|
||||||
if (hasDocumentEnvironment(docText)) {
|
|
||||||
ind = staticSnippets.indexOf(documentSnippet)
|
|
||||||
if (ind !== -1) {
|
|
||||||
staticSnippets.splice(ind, 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
staticSnippets.push(documentSnippet)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasBibliographyEnvironment(docText)) {
|
|
||||||
ind = staticSnippets.indexOf(bibliographySnippet)
|
|
||||||
if (ind !== -1) {
|
|
||||||
staticSnippets.splice(ind, 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
staticSnippets.push(bibliographySnippet)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedItemsMap = {}
|
|
||||||
for (const environment of Array.from(customEnvironments)) {
|
|
||||||
parsedItemsMap[environment.name] = environment
|
|
||||||
}
|
|
||||||
for (const command of Array.from(beginCommands)) {
|
|
||||||
parsedItemsMap[command.name] = command
|
|
||||||
}
|
|
||||||
const parsedItems = _.values(parsedItemsMap)
|
|
||||||
const snippets = staticSnippets
|
|
||||||
.concat(
|
|
||||||
parsedItems.map(item => ({
|
|
||||||
caption: `\\begin{${item.name}}...`,
|
|
||||||
snippet: `\
|
|
||||||
\\begin{${item.name}}
|
|
||||||
${item.whitespace || ''}$0
|
|
||||||
\\end{${item.name}}\
|
|
||||||
`,
|
|
||||||
meta: 'env',
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
// arguably these `end` commands shouldn't be here, as they're not snippets
|
|
||||||
// but this is where we have access to the `begin` environment names
|
|
||||||
// *shrug*
|
|
||||||
parsedItems.map(item => ({
|
|
||||||
caption: `\\end{${item.name}}`,
|
|
||||||
value: `\\end{${item.name}}`,
|
|
||||||
meta: 'env',
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return callback(null, snippets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EnvironmentManager
|
|
|
@ -1,73 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-cond-assign,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import 'ace/ace'
|
|
||||||
import 'ace/ext-language_tools'
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
|
|
||||||
const Helpers = {
|
|
||||||
getLastCommandFragment(lineUpToCursor) {
|
|
||||||
let index
|
|
||||||
if ((index = Helpers.getLastCommandFragmentIndex(lineUpToCursor)) > -1) {
|
|
||||||
return lineUpToCursor.slice(index)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getLastCommandFragmentIndex(lineUpToCursor) {
|
|
||||||
// This is hack to let us skip over commands in arguments, and
|
|
||||||
// go to the command on the same 'level' as us. E.g.
|
|
||||||
// \includegraphics[width=\textwidth]{..
|
|
||||||
// should not match the \textwidth.
|
|
||||||
let m
|
|
||||||
const blankArguments = lineUpToCursor.replace(/\[([^\]]*)\]/g, args =>
|
|
||||||
Array(args.length + 1).join('.')
|
|
||||||
)
|
|
||||||
if ((m = blankArguments.match(/(\\[^\\]*)$/))) {
|
|
||||||
return m.index
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getCommandNameFromFragment(commandFragment) {
|
|
||||||
return __guard__(
|
|
||||||
commandFragment != null ? commandFragment.match(/\\(\w+)\{/) : undefined,
|
|
||||||
x => x[1]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
getContext(editor, pos) {
|
|
||||||
const upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
|
|
||||||
const lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
|
|
||||||
const commandFragment = Helpers.getLastCommandFragment(lineUpToCursor)
|
|
||||||
const commandName = Helpers.getCommandNameFromFragment(commandFragment)
|
|
||||||
const beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
|
|
||||||
const lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
|
|
||||||
return {
|
|
||||||
lineUpToCursor,
|
|
||||||
commandFragment,
|
|
||||||
commandName,
|
|
||||||
lineBeyondCursor,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Helpers
|
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
const packages = [
|
|
||||||
'inputenc',
|
|
||||||
'graphicx',
|
|
||||||
'amsmath',
|
|
||||||
'geometry',
|
|
||||||
'amssymb',
|
|
||||||
'hyperref',
|
|
||||||
'babel',
|
|
||||||
'color',
|
|
||||||
'xcolor',
|
|
||||||
'url',
|
|
||||||
'natbib',
|
|
||||||
'fontenc',
|
|
||||||
'fancyhdr',
|
|
||||||
'amsfonts',
|
|
||||||
'booktabs',
|
|
||||||
'amsthm',
|
|
||||||
'float',
|
|
||||||
'tikz',
|
|
||||||
'caption',
|
|
||||||
'setspace',
|
|
||||||
'multirow',
|
|
||||||
'array',
|
|
||||||
'multicol',
|
|
||||||
'titlesec',
|
|
||||||
'enumitem',
|
|
||||||
'ifthen',
|
|
||||||
'listings',
|
|
||||||
'blindtext',
|
|
||||||
'subcaption',
|
|
||||||
'times',
|
|
||||||
'bm',
|
|
||||||
'subfigure',
|
|
||||||
'algorithm',
|
|
||||||
'fontspec',
|
|
||||||
'biblatex',
|
|
||||||
'tabularx',
|
|
||||||
'microtype',
|
|
||||||
'etoolbox',
|
|
||||||
'parskip',
|
|
||||||
'calc',
|
|
||||||
'verbatim',
|
|
||||||
'mathtools',
|
|
||||||
'epsfig',
|
|
||||||
'wrapfig',
|
|
||||||
'lipsum',
|
|
||||||
'cite',
|
|
||||||
'textcomp',
|
|
||||||
'longtable',
|
|
||||||
'textpos',
|
|
||||||
'algpseudocode',
|
|
||||||
'enumerate',
|
|
||||||
'subfig',
|
|
||||||
'pdfpages',
|
|
||||||
'epstopdf',
|
|
||||||
'latexsym',
|
|
||||||
'lmodern',
|
|
||||||
'pifont',
|
|
||||||
'ragged2e',
|
|
||||||
'rotating',
|
|
||||||
'dcolumn',
|
|
||||||
'xltxtra',
|
|
||||||
'marvosym',
|
|
||||||
'indentfirst',
|
|
||||||
'xspace',
|
|
||||||
'csquotes',
|
|
||||||
'xparse',
|
|
||||||
'changepage',
|
|
||||||
'soul',
|
|
||||||
'xunicode',
|
|
||||||
'comment',
|
|
||||||
'mathrsfs',
|
|
||||||
'tocbibind',
|
|
||||||
'lastpage',
|
|
||||||
'algorithm2e',
|
|
||||||
'pgfplots',
|
|
||||||
'lineno',
|
|
||||||
'graphics',
|
|
||||||
'algorithmic',
|
|
||||||
'fullpage',
|
|
||||||
'mathptmx',
|
|
||||||
'todonotes',
|
|
||||||
'ulem',
|
|
||||||
'tweaklist',
|
|
||||||
'moderncvstyleclassic',
|
|
||||||
'collection',
|
|
||||||
'moderncvcompatibility',
|
|
||||||
'gensymb',
|
|
||||||
'helvet',
|
|
||||||
'siunitx',
|
|
||||||
'adjustbox',
|
|
||||||
'placeins',
|
|
||||||
'colortbl',
|
|
||||||
'appendix',
|
|
||||||
'makeidx',
|
|
||||||
'supertabular',
|
|
||||||
'ifpdf',
|
|
||||||
'framed',
|
|
||||||
'aliascnt',
|
|
||||||
'layaureo',
|
|
||||||
'authblk',
|
|
||||||
]
|
|
||||||
|
|
||||||
class PackageManager {
|
|
||||||
constructor(metadataManager) {
|
|
||||||
this.metadataManager = metadataManager
|
|
||||||
}
|
|
||||||
|
|
||||||
getCompletions(editor, session, pos, prefix, callback) {
|
|
||||||
const usedPackages = Object.keys(this.metadataManager.getAllPackages())
|
|
||||||
const packageSnippets = []
|
|
||||||
for (const pkg of Array.from(packages)) {
|
|
||||||
if (!Array.from(usedPackages).includes(pkg)) {
|
|
||||||
packageSnippets.push({
|
|
||||||
caption: `\\usepackage{${pkg}}`,
|
|
||||||
snippet: `\\usepackage{${pkg}}`,
|
|
||||||
meta: 'pkg',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packageSnippets.push({
|
|
||||||
caption: '\\usepackage{}',
|
|
||||||
snippet: '\\usepackage{$1}',
|
|
||||||
meta: 'pkg',
|
|
||||||
score: 70,
|
|
||||||
})
|
|
||||||
return callback(null, packageSnippets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PackageManager
|
|
|
@ -1,34 +0,0 @@
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
const envs = [
|
|
||||||
'abstract',
|
|
||||||
'align',
|
|
||||||
'align*',
|
|
||||||
'equation',
|
|
||||||
'equation*',
|
|
||||||
'gather',
|
|
||||||
'gather*',
|
|
||||||
'multline',
|
|
||||||
'multline*',
|
|
||||||
'split',
|
|
||||||
'verbatim',
|
|
||||||
'quote',
|
|
||||||
'center',
|
|
||||||
]
|
|
||||||
|
|
||||||
const envsWithSnippets = [
|
|
||||||
'array',
|
|
||||||
'figure',
|
|
||||||
'tabular',
|
|
||||||
'table',
|
|
||||||
'list',
|
|
||||||
'enumerate',
|
|
||||||
'itemize',
|
|
||||||
'frame',
|
|
||||||
'thebibliography',
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
all: envs.concat(envsWithSnippets),
|
|
||||||
withoutSnippets: envs,
|
|
||||||
}
|
|
|
@ -1,699 +0,0 @@
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
caption: '\\begin{}',
|
|
||||||
snippet: '\\begin{$1}',
|
|
||||||
meta: 'env',
|
|
||||||
score: 7.849662248028187,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\end{}',
|
|
||||||
snippet: '\\end{$1}',
|
|
||||||
meta: 'env',
|
|
||||||
score: 7.847906405228455,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\usepackage[]{}',
|
|
||||||
snippet: '\\usepackage[$1]{$2}',
|
|
||||||
meta: 'pkg',
|
|
||||||
score: 5.427890758130527,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\item',
|
|
||||||
snippet: '\\item ',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 3.800886892251021,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\item[]',
|
|
||||||
snippet: '\\item[$1] ',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 3.800886892251021,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\section{}',
|
|
||||||
snippet: '\\section{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 3.0952612541683835,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\textbf{}',
|
|
||||||
snippet: '\\textbf{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 2.627755982816738,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\cite{}',
|
|
||||||
snippet: '\\cite{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 2.341195220791228,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\label{}',
|
|
||||||
snippet: '\\label{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.897791904799601,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\textit{}',
|
|
||||||
snippet: '\\textit{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.6842996195493385,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\includegraphics[]{}',
|
|
||||||
snippet: '\\includegraphics[$1]{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.4595731795525781,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\documentclass[]{}',
|
|
||||||
snippet: '\\documentclass[$1]{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.4425339817971206,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\documentclass{}',
|
|
||||||
snippet: '\\documentclass{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.4425339817971206,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\ref{}',
|
|
||||||
snippet: '\\ref{$1}',
|
|
||||||
meta: 'cross-reference',
|
|
||||||
score: 0.014379554883991673,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\frac{}{}',
|
|
||||||
snippet: '\\frac{$1}{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.4341091141105058,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\subsection{}',
|
|
||||||
snippet: '\\subsection{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.3890912739512353,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\hline',
|
|
||||||
snippet: '\\hline',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.3209538327406387,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\caption{}',
|
|
||||||
snippet: '\\caption{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.2569477427490174,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\centering',
|
|
||||||
snippet: '\\centering',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 1.1642881814937829,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\vspace{}',
|
|
||||||
snippet: '\\vspace{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.9533807826673939,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\title{}',
|
|
||||||
snippet: '\\title{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.9202908262245683,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\author{}',
|
|
||||||
snippet: '\\author{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.8973590434087177,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\author[]{}',
|
|
||||||
snippet: '\\author[$1]{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.8973590434087177,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\maketitle',
|
|
||||||
snippet: '\\maketitle',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7504160124360846,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\textwidth',
|
|
||||||
snippet: '\\textwidth',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7355328080889112,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\newcommand{}{}',
|
|
||||||
snippet: '\\newcommand{$1}{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7264891987129375,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\newcommand{}[]{}',
|
|
||||||
snippet: '\\newcommand{$1}[$2]{$3}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7264891987129375,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\date{}',
|
|
||||||
snippet: '\\date{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7225518453076786,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\emph{}',
|
|
||||||
snippet: '\\emph{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.7060308784832261,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\textsc{}',
|
|
||||||
snippet: '\\textsc{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.6926466355384758,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\multicolumn{}{}{}',
|
|
||||||
snippet: '\\multicolumn{$1}{$2}{$3}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.5473606021405326,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\input{}',
|
|
||||||
snippet: '\\input{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.4966021927742672,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\alpha',
|
|
||||||
snippet: '\\alpha',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.49520006391384913,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\in',
|
|
||||||
snippet: '\\in',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.4716039670146658,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\mathbf{}',
|
|
||||||
snippet: '\\mathbf{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.4682018419466319,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\right',
|
|
||||||
snippet: '\\right',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.4299239459457309,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\left',
|
|
||||||
snippet: '\\left',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.42937815279867964,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\sum',
|
|
||||||
snippet: '\\sum',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.42607994509619934,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\chapter{}',
|
|
||||||
snippet: '\\chapter{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.422097569591803,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\par',
|
|
||||||
snippet: '\\par',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.413853376001159,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\lambda',
|
|
||||||
snippet: '\\lambda',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.39389600578684125,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\subsubsection{}',
|
|
||||||
snippet: '\\subsubsection{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3727781330132016,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\bibitem{}',
|
|
||||||
snippet: '\\bibitem{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3689547570562042,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\bibitem[]{}',
|
|
||||||
snippet: '\\bibitem[$1]{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3689547570562042,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\text{}',
|
|
||||||
snippet: '\\text{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3608680734736821,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\setlength{}{}',
|
|
||||||
snippet: '\\setlength{$1}{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.354445763583904,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\mathcal{}',
|
|
||||||
snippet: '\\mathcal{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.35084018920966636,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\newpage',
|
|
||||||
snippet: '\\newpage',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3277033727934986,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\renewcommand{}{}',
|
|
||||||
snippet: '\\renewcommand{$1}{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3267437011085663,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\theta',
|
|
||||||
snippet: '\\theta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3210417159232142,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\hspace{}',
|
|
||||||
snippet: '\\hspace{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3147206476372336,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\beta',
|
|
||||||
snippet: '\\beta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3061799530337638,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\texttt{}',
|
|
||||||
snippet: '\\texttt{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.3019066753744355,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\times',
|
|
||||||
snippet: '\\times',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2957960629411553,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\color{}',
|
|
||||||
snippet: '\\color{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2864294797053033,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\mu',
|
|
||||||
snippet: '\\mu',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.27635652476799255,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\bibliography{}',
|
|
||||||
snippet: '\\bibliography{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2659628337907604,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\linewidth',
|
|
||||||
snippet: '\\linewidth',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2639498312518439,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\delta',
|
|
||||||
snippet: '\\delta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2620578600722735,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\sigma',
|
|
||||||
snippet: '\\sigma',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.25940147926344487,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\pi',
|
|
||||||
snippet: '\\pi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.25920934567729714,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\hat{}',
|
|
||||||
snippet: '\\hat{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.25264309033778715,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\bibliographystyle{}',
|
|
||||||
snippet: '\\bibliographystyle{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.25122317941387773,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\small',
|
|
||||||
snippet: '\\small',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2447632045426295,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\LaTeX',
|
|
||||||
snippet: '\\LaTeX',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2334089308452787,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\cdot',
|
|
||||||
snippet: '\\cdot',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.23029085545522762,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\footnote{}',
|
|
||||||
snippet: '\\footnote{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2253056071787701,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\newtheorem{}{}',
|
|
||||||
snippet: '\\newtheorem{$1}{$2}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.215689795055434,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Delta',
|
|
||||||
snippet: '\\Delta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.21386475063892618,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\tau',
|
|
||||||
snippet: '\\tau',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.21236188205859796,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\hfill',
|
|
||||||
snippet: '\\hfill',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2058248088519886,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\leq',
|
|
||||||
snippet: '\\leq',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.20498894440637172,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\footnotesize',
|
|
||||||
snippet: '\\footnotesize',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2038592081252624,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\large',
|
|
||||||
snippet: '\\large',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.20377416734108866,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\sqrt{}',
|
|
||||||
snippet: '\\sqrt{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.20240160977404634,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\epsilon',
|
|
||||||
snippet: '\\epsilon',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.2005136761359043,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Large',
|
|
||||||
snippet: '\\Large',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.1987771081149759,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\rho',
|
|
||||||
snippet: '\\rho',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.1959287380541684,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\omega',
|
|
||||||
snippet: '\\omega',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.19326783415115262,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\mathrm{}',
|
|
||||||
snippet: '\\mathrm{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.19117752976172653,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\boldsymbol{}',
|
|
||||||
snippet: '\\boldsymbol{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.18137737738638837,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\gamma',
|
|
||||||
snippet: '\\gamma',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.17940276535431304,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\clearpage',
|
|
||||||
snippet: '\\clearpage',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.1789117552185788,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\infty',
|
|
||||||
snippet: '\\infty',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.17837290019711305,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\phi',
|
|
||||||
snippet: '\\phi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.17405809173097808,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\partial',
|
|
||||||
snippet: '\\partial',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.17168102367966637,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\include{}',
|
|
||||||
snippet: '\\include{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.1547080054979312,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\address{}',
|
|
||||||
snippet: '\\address{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.1525055392611109,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\quad',
|
|
||||||
snippet: '\\quad',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.15242755832392743,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\paragraph{}',
|
|
||||||
snippet: '\\paragraph{$1}',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.152074250347974,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\varepsilon',
|
|
||||||
snippet: '\\varepsilon',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.05411564201390573,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\zeta',
|
|
||||||
snippet: '\\zeta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.023330249803752954,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\eta',
|
|
||||||
snippet: '\\eta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.11088718379889091,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\vartheta',
|
|
||||||
snippet: '\\vartheta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0025822992078068712,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\iota',
|
|
||||||
snippet: '\\iota',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0024774003791525486,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\kappa',
|
|
||||||
snippet: '\\kappa',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.04887876299369008,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\nu',
|
|
||||||
snippet: '\\nu',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.09206962821059342,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\xi',
|
|
||||||
snippet: '\\xi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.06496042899265699,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\varpi',
|
|
||||||
snippet: '\\varpi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0007039358167790341,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\varrho',
|
|
||||||
snippet: '\\varrho',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0011279491613898612,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\varsigma',
|
|
||||||
snippet: '\\varsigma',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0010424880711234978,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\upsilon',
|
|
||||||
snippet: '\\upsilon',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.00420715572598688,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\varphi',
|
|
||||||
snippet: '\\varphi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.03351251516668212,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\chi',
|
|
||||||
snippet: '\\chi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.043373492287805675,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\psi',
|
|
||||||
snippet: '\\psi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.09994508706163642,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Gamma',
|
|
||||||
snippet: '\\Gamma',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.04801549269801977,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Theta',
|
|
||||||
snippet: '\\Theta',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.038090902146599444,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Lambda',
|
|
||||||
snippet: '\\Lambda',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.032206594305977686,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Xi',
|
|
||||||
snippet: '\\Xi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.01060997225400494,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Pi',
|
|
||||||
snippet: '\\Pi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.021264671817473237,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Sigma',
|
|
||||||
snippet: '\\Sigma',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.05769642802079917,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Upsilon',
|
|
||||||
snippet: '\\Upsilon',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.00032875192955749566,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Phi',
|
|
||||||
snippet: '\\Phi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.0538724950042562,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Psi',
|
|
||||||
snippet: '\\Psi',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.03056589143021648,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
caption: '\\Omega',
|
|
||||||
snippet: '\\Omega',
|
|
||||||
meta: 'cmd',
|
|
||||||
score: 0.09490387997853639,
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,57 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
|
||||||
let CursorPositionAdapter
|
|
||||||
|
|
||||||
export default CursorPositionAdapter = class CursorPositionAdapter {
|
|
||||||
constructor(editor) {
|
|
||||||
this.editor = editor
|
|
||||||
}
|
|
||||||
|
|
||||||
getCursor() {
|
|
||||||
return this.editor.getCursorPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
getEditorScrollPosition() {
|
|
||||||
return this.editor.getFirstVisibleRow()
|
|
||||||
}
|
|
||||||
|
|
||||||
setCursor(pos) {
|
|
||||||
pos = pos.cursorPosition || { row: 0, column: 0 }
|
|
||||||
return this.editor.moveCursorToPosition(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditorScrollPosition(pos) {
|
|
||||||
pos = pos.firstVisibleLine || 0
|
|
||||||
return this.editor.scrollToLine(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelection() {
|
|
||||||
return this.editor.selection.clearSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoLine(line, column) {
|
|
||||||
this.editor.gotoLine(line, column)
|
|
||||||
this.editor.scrollToLine(line, true, true) // centre and animate
|
|
||||||
return this.editor.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoOffset(offset) {
|
|
||||||
const lines = this.editor.getSession().getDocument().getAllLines()
|
|
||||||
const position = EditorShareJsCodec.shareJsOffsetToRowColumn(offset, lines)
|
|
||||||
return this.gotoLine(position.row + 1, position.column)
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.editor.focus()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
let CursorPositionManager
|
|
||||||
|
|
||||||
export default CursorPositionManager = class CursorPositionManager {
|
|
||||||
constructor($scope, adapter, localStorage) {
|
|
||||||
this.storePositionAndLine = this.storePositionAndLine.bind(this)
|
|
||||||
this.jumpToPositionInNewDoc = this.jumpToPositionInNewDoc.bind(this)
|
|
||||||
this.onUnload = this.onUnload.bind(this)
|
|
||||||
this.onCursorChange = this.onCursorChange.bind(this)
|
|
||||||
this.onSyncToPdf = this.onSyncToPdf.bind(this)
|
|
||||||
this.$scope = $scope
|
|
||||||
this.adapter = adapter
|
|
||||||
this.localStorage = localStorage
|
|
||||||
this.$scope.$on('editorInit', this.jumpToPositionInNewDoc)
|
|
||||||
|
|
||||||
this.$scope.$on('store-doc-position', this.storePositionAndLine)
|
|
||||||
|
|
||||||
this.$scope.$on('afterChangeDocument', this.jumpToPositionInNewDoc)
|
|
||||||
|
|
||||||
this.$scope.$on('changeEditor', this.storePositionAndLine)
|
|
||||||
|
|
||||||
this.$scope.$on(
|
|
||||||
`${this.$scope.name}:gotoLine`,
|
|
||||||
(e, line, column, syncToPdf) => {
|
|
||||||
if (line != null) {
|
|
||||||
return setTimeout(() => {
|
|
||||||
this.adapter.gotoLine(line, column)
|
|
||||||
if (syncToPdf) this.onSyncToPdf()
|
|
||||||
}, 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) // Hack: Must happen after @gotoStoredPosition
|
|
||||||
|
|
||||||
this.$scope.$on(`${this.$scope.name}:gotoOffset`, (e, offset) => {
|
|
||||||
if (offset != null) {
|
|
||||||
return setTimeout(() => {
|
|
||||||
return this.adapter.gotoOffset(offset)
|
|
||||||
}, 10)
|
|
||||||
}
|
|
||||||
}) // Hack: Must happen after @gotoStoredPosition
|
|
||||||
|
|
||||||
this.$scope.$on(`${this.$scope.name}:clearSelection`, e => {
|
|
||||||
return this.adapter.clearSelection()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
storePositionAndLine() {
|
|
||||||
this.storeCursorPosition()
|
|
||||||
return this.storeFirstVisibleLine()
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpToPositionInNewDoc() {
|
|
||||||
this.doc_id =
|
|
||||||
this.$scope.sharejsDoc != null ? this.$scope.sharejsDoc.doc_id : undefined
|
|
||||||
return setTimeout(() => {
|
|
||||||
return this.gotoStoredPosition()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnload() {
|
|
||||||
this.storeCursorPosition()
|
|
||||||
return this.storeFirstVisibleLine()
|
|
||||||
}
|
|
||||||
|
|
||||||
onCursorChange() {
|
|
||||||
return this.emitCursorUpdateEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSyncToPdf() {
|
|
||||||
return this.$scope.$emit(`cursor:${this.$scope.name}:syncToPdf`)
|
|
||||||
}
|
|
||||||
|
|
||||||
storeFirstVisibleLine() {
|
|
||||||
if (this.doc_id != null) {
|
|
||||||
const docPosition = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
|
||||||
docPosition.firstVisibleLine = this.adapter.getEditorScrollPosition()
|
|
||||||
return this.localStorage(`doc.position.${this.doc_id}`, docPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
storeCursorPosition() {
|
|
||||||
if (this.doc_id != null) {
|
|
||||||
const docPosition = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
|
||||||
docPosition.cursorPosition = this.adapter.getCursor()
|
|
||||||
return this.localStorage(`doc.position.${this.doc_id}`, docPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitCursorUpdateEvent() {
|
|
||||||
const cursor = this.adapter.getCursor()
|
|
||||||
this.$scope.$emit(`cursor:${this.$scope.name}:update`, cursor)
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(`cursor:${this.$scope.name}:update`, {
|
|
||||||
detail: cursor,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoStoredPosition() {
|
|
||||||
if (this.doc_id == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const pos = this.localStorage(`doc.position.${this.doc_id}`) || {}
|
|
||||||
this.adapter.setCursor(pos)
|
|
||||||
this.adapter.setEditorScrollPosition(pos)
|
|
||||||
this.adapter.focus()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,396 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
camelcase,
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* DS205: Consider reworking code to avoid use of IIFEs
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import 'ace/ace'
|
|
||||||
import ColorManager from '../../../../colors/ColorManager'
|
|
||||||
let HighlightsManager
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
|
|
||||||
export default HighlightsManager = class HighlightsManager {
|
|
||||||
constructor($scope, editor, element) {
|
|
||||||
this.$scope = $scope
|
|
||||||
this.editor = editor
|
|
||||||
this.element = element
|
|
||||||
this.markerIds = []
|
|
||||||
this.labels = []
|
|
||||||
|
|
||||||
this.$scope.annotationLabel = {
|
|
||||||
show: false,
|
|
||||||
right: 'auto',
|
|
||||||
left: 'auto',
|
|
||||||
top: 'auto',
|
|
||||||
bottom: 'auto',
|
|
||||||
backgroundColor: 'black',
|
|
||||||
text: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$scope.updateLabels = {
|
|
||||||
updatesAbove: 0,
|
|
||||||
updatesBelow: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$scope.$watch('highlights', value => {
|
|
||||||
return this.redrawAnnotations()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$watch('theme', value => {
|
|
||||||
return this.redrawAnnotations()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.editor.on('mousemove', e => {
|
|
||||||
const position = this.editor.renderer.screenToTextCoordinates(
|
|
||||||
e.clientX,
|
|
||||||
e.clientY
|
|
||||||
)
|
|
||||||
e.position = position
|
|
||||||
return this.showAnnotationLabels(position)
|
|
||||||
})
|
|
||||||
|
|
||||||
const onChangeScrollTop = () => {
|
|
||||||
return this.updateShowMoreLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor.getSession().on('changeScrollTop', onChangeScrollTop)
|
|
||||||
|
|
||||||
this.$scope.$watch('text', () => {
|
|
||||||
if (this.$scope.navigateHighlights) {
|
|
||||||
return setTimeout(() => {
|
|
||||||
return this.scrollToFirstHighlight()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.editor.on('changeSession', e => {
|
|
||||||
if (e.oldSession != null) {
|
|
||||||
e.oldSession.off('changeScrollTop', onChangeScrollTop)
|
|
||||||
}
|
|
||||||
e.session.on('changeScrollTop', onChangeScrollTop)
|
|
||||||
return this.redrawAnnotations()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.gotoHighlightBelow = () => {
|
|
||||||
if (this.firstHiddenHighlightAfter == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return this.editor.scrollToLine(
|
|
||||||
this.firstHiddenHighlightAfter.end.row,
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$scope.gotoHighlightAbove = () => {
|
|
||||||
if (this.lastHiddenHighlightBefore == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return this.editor.scrollToLine(
|
|
||||||
this.lastHiddenHighlightBefore.start.row,
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
redrawAnnotations() {
|
|
||||||
this._clearMarkers()
|
|
||||||
this._clearLabels()
|
|
||||||
|
|
||||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
|
||||||
;(annotation => {
|
|
||||||
const colorScheme = ColorManager.getColorScheme(
|
|
||||||
annotation.hue,
|
|
||||||
this.element
|
|
||||||
)
|
|
||||||
if (annotation.cursor != null) {
|
|
||||||
this.labels.push({
|
|
||||||
text: annotation.label,
|
|
||||||
range: new Range(
|
|
||||||
annotation.cursor.row,
|
|
||||||
annotation.cursor.column,
|
|
||||||
annotation.cursor.row,
|
|
||||||
annotation.cursor.column + 1
|
|
||||||
),
|
|
||||||
colorScheme,
|
|
||||||
snapToStartOfRange: true,
|
|
||||||
})
|
|
||||||
return this._drawCursor(annotation, colorScheme)
|
|
||||||
} else if (annotation.highlight != null) {
|
|
||||||
this.labels.push({
|
|
||||||
text: annotation.label,
|
|
||||||
range: new Range(
|
|
||||||
annotation.highlight.start.row,
|
|
||||||
annotation.highlight.start.column,
|
|
||||||
annotation.highlight.end.row,
|
|
||||||
annotation.highlight.end.column
|
|
||||||
),
|
|
||||||
colorScheme,
|
|
||||||
})
|
|
||||||
return this._drawHighlight(annotation, colorScheme)
|
|
||||||
} else if (annotation.strikeThrough != null) {
|
|
||||||
this.labels.push({
|
|
||||||
text: annotation.label,
|
|
||||||
range: new Range(
|
|
||||||
annotation.strikeThrough.start.row,
|
|
||||||
annotation.strikeThrough.start.column,
|
|
||||||
annotation.strikeThrough.end.row,
|
|
||||||
annotation.strikeThrough.end.column
|
|
||||||
),
|
|
||||||
colorScheme,
|
|
||||||
})
|
|
||||||
return this._drawStrikeThrough(annotation, colorScheme)
|
|
||||||
}
|
|
||||||
})(annotation)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.updateShowMoreLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
showAnnotationLabels(position) {
|
|
||||||
let labelToShow = null
|
|
||||||
for (const label of Array.from(this.labels || [])) {
|
|
||||||
if (label.range.contains(position.row, position.column)) {
|
|
||||||
labelToShow = label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labelToShow == null) {
|
|
||||||
// this is the most common path, triggered on mousemove, so
|
|
||||||
// for performance only apply setting when it changes
|
|
||||||
if (
|
|
||||||
__guard__(
|
|
||||||
this.$scope != null ? this.$scope.annotationLabel : undefined,
|
|
||||||
x => x.show
|
|
||||||
) !== false
|
|
||||||
) {
|
|
||||||
return this.$scope.$apply(() => {
|
|
||||||
return (this.$scope.annotationLabel.show = false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let bottom, coords, left, right, top
|
|
||||||
const $ace = $(this.editor.renderer.container).find('.ace_scroller')
|
|
||||||
// Move the label into the Ace content area so that offsets and positions are easy to calculate.
|
|
||||||
$ace.append(this.element.find('.annotation-label'))
|
|
||||||
|
|
||||||
if (labelToShow.snapToStartOfRange) {
|
|
||||||
coords = this.editor.renderer.textToScreenCoordinates(
|
|
||||||
labelToShow.range.start.row,
|
|
||||||
labelToShow.range.start.column
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
coords = this.editor.renderer.textToScreenCoordinates(
|
|
||||||
position.row,
|
|
||||||
position.column
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = $ace.offset()
|
|
||||||
const height = $ace.height()
|
|
||||||
coords.pageX = coords.pageX - offset.left
|
|
||||||
coords.pageY = coords.pageY - offset.top
|
|
||||||
|
|
||||||
if (coords.pageY > this.editor.renderer.lineHeight * 2) {
|
|
||||||
top = 'auto'
|
|
||||||
bottom = height - coords.pageY
|
|
||||||
} else {
|
|
||||||
top = coords.pageY + this.editor.renderer.lineHeight
|
|
||||||
bottom = 'auto'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply this first that the label has the correct width when calculating below
|
|
||||||
this.$scope.$apply(() => {
|
|
||||||
this.$scope.annotationLabel.text = labelToShow.text
|
|
||||||
return (this.$scope.annotationLabel.show = true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const $label = this.element.find('.annotation-label')
|
|
||||||
|
|
||||||
if (coords.pageX + $label.outerWidth() < $ace.width()) {
|
|
||||||
left = coords.pageX
|
|
||||||
right = 'auto'
|
|
||||||
} else {
|
|
||||||
right = 0
|
|
||||||
left = 'auto'
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$scope.$apply(() => {
|
|
||||||
return (this.$scope.annotationLabel = {
|
|
||||||
show: true,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
top,
|
|
||||||
backgroundColor: labelToShow.colorScheme.labelBackgroundColor,
|
|
||||||
text: labelToShow.text,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateShowMoreLabels() {
|
|
||||||
if (!this.$scope.navigateHighlights) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return setTimeout(() => {
|
|
||||||
const firstRow = this.editor.getFirstVisibleRow()
|
|
||||||
const lastRow = this.editor.getLastVisibleRow()
|
|
||||||
let highlightsBefore = 0
|
|
||||||
let highlightsAfter = 0
|
|
||||||
this.lastHiddenHighlightBefore = null
|
|
||||||
this.firstHiddenHighlightAfter = null
|
|
||||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
|
||||||
const range = annotation.highlight || annotation.strikeThrough
|
|
||||||
if (range == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (range.start.row < firstRow) {
|
|
||||||
highlightsBefore += 1
|
|
||||||
this.lastHiddenHighlightBefore = range
|
|
||||||
}
|
|
||||||
if (range.end.row > lastRow) {
|
|
||||||
highlightsAfter += 1
|
|
||||||
if (!this.firstHiddenHighlightAfter) {
|
|
||||||
this.firstHiddenHighlightAfter = range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$scope.$apply(() => {
|
|
||||||
return (this.$scope.updateLabels = {
|
|
||||||
highlightsBefore,
|
|
||||||
highlightsAfter,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToFirstHighlight() {
|
|
||||||
return (() => {
|
|
||||||
const result = []
|
|
||||||
for (const annotation of Array.from(this.$scope.highlights || [])) {
|
|
||||||
const range = annotation.highlight || annotation.strikeThrough
|
|
||||||
if (range == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
this.editor.scrollToLine(range.start.row, true, false)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearMarkers() {
|
|
||||||
for (const marker_id of Array.from(this.markerIds)) {
|
|
||||||
this.editor.getSession().removeMarker(marker_id)
|
|
||||||
}
|
|
||||||
return (this.markerIds = [])
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearLabels() {
|
|
||||||
return (this.labels = [])
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawCursor(annotation, colorScheme) {
|
|
||||||
return this._addMarkerWithCustomStyle(
|
|
||||||
new Range(
|
|
||||||
annotation.cursor.row,
|
|
||||||
annotation.cursor.column,
|
|
||||||
annotation.cursor.row,
|
|
||||||
annotation.cursor.column + 1
|
|
||||||
),
|
|
||||||
'annotation remote-cursor',
|
|
||||||
false,
|
|
||||||
`border-color: ${colorScheme.cursor};`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawHighlight(annotation, colorScheme) {
|
|
||||||
return this._addMarkerWithCustomStyle(
|
|
||||||
new Range(
|
|
||||||
annotation.highlight.start.row,
|
|
||||||
annotation.highlight.start.column,
|
|
||||||
annotation.highlight.end.row,
|
|
||||||
annotation.highlight.end.column
|
|
||||||
),
|
|
||||||
'annotation highlight',
|
|
||||||
false,
|
|
||||||
`background-color: ${colorScheme.highlightBackgroundColor}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawStrikeThrough(annotation, colorScheme) {
|
|
||||||
this._addMarkerWithCustomStyle(
|
|
||||||
new Range(
|
|
||||||
annotation.strikeThrough.start.row,
|
|
||||||
annotation.strikeThrough.start.column,
|
|
||||||
annotation.strikeThrough.end.row,
|
|
||||||
annotation.strikeThrough.end.column
|
|
||||||
),
|
|
||||||
'annotation strike-through-background',
|
|
||||||
false,
|
|
||||||
`background-color: ${colorScheme.strikeThroughBackgroundColor}`
|
|
||||||
)
|
|
||||||
return this._addMarkerWithCustomStyle(
|
|
||||||
new Range(
|
|
||||||
annotation.strikeThrough.start.row,
|
|
||||||
annotation.strikeThrough.start.column,
|
|
||||||
annotation.strikeThrough.end.row,
|
|
||||||
annotation.strikeThrough.end.column
|
|
||||||
),
|
|
||||||
'annotation strike-through-foreground',
|
|
||||||
true,
|
|
||||||
`color: ${colorScheme.strikeThroughForegroundColor};`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_addMarkerWithCustomStyle(range, klass, foreground, style) {
|
|
||||||
let markerLayer
|
|
||||||
if (!foreground) {
|
|
||||||
markerLayer = this.editor.renderer.$markerBack
|
|
||||||
} else {
|
|
||||||
markerLayer = this.editor.renderer.$markerFront
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.markerIds.push(
|
|
||||||
this.editor.getSession().addMarker(
|
|
||||||
range,
|
|
||||||
klass,
|
|
||||||
function (html, range, left, top, config) {
|
|
||||||
if (range.isMultiLine()) {
|
|
||||||
return markerLayer.drawTextMarker(html, range, klass, config, style)
|
|
||||||
} else {
|
|
||||||
return markerLayer.drawSingleLineMarker(
|
|
||||||
html,
|
|
||||||
range,
|
|
||||||
`${klass} ace_start`,
|
|
||||||
config,
|
|
||||||
0,
|
|
||||||
style
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
foreground
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-cond-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import 'ace/ace'
|
|
||||||
let MetadataManager
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
|
|
||||||
const getLastCommandFragment = function (lineUpToCursor) {
|
|
||||||
let m
|
|
||||||
if ((m = lineUpToCursor.match(/(\\[^\\]+)$/))) {
|
|
||||||
return m[1]
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetadataManager = class MetadataManager {
|
|
||||||
constructor($scope, editor, element, Metadata) {
|
|
||||||
this.$scope = $scope
|
|
||||||
this.editor = editor
|
|
||||||
this.element = element
|
|
||||||
this.Metadata = Metadata
|
|
||||||
this.debouncer = {} // DocId => Timeout
|
|
||||||
|
|
||||||
const onChange = change => {
|
|
||||||
if (change.origin === 'remote') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!['remove', 'insert'].includes(change.action)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cursorPosition = this.editor.getCursorPosition()
|
|
||||||
const { end } = change
|
|
||||||
let range = new Range(end.row, 0, end.row, end.column)
|
|
||||||
let lineUpToCursor = this.editor.getSession().getTextRange(range)
|
|
||||||
if (
|
|
||||||
lineUpToCursor.trim() === '%' ||
|
|
||||||
lineUpToCursor.slice(0, 1) === '\\'
|
|
||||||
) {
|
|
||||||
range = new Range(end.row, 0, end.row, end.column + 80)
|
|
||||||
lineUpToCursor = this.editor.getSession().getTextRange(range)
|
|
||||||
}
|
|
||||||
const commandFragment = getLastCommandFragment(lineUpToCursor)
|
|
||||||
|
|
||||||
const linesContainPackage = _.some(change.lines, line =>
|
|
||||||
line.match(/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
|
|
||||||
)
|
|
||||||
const linesContainReqPackage = _.some(change.lines, line =>
|
|
||||||
line.match(/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
|
|
||||||
)
|
|
||||||
const linesContainLabel = _.some(change.lines, line =>
|
|
||||||
line.match(/\\label{(.{0,80}?)}/)
|
|
||||||
)
|
|
||||||
const linesContainMeta =
|
|
||||||
linesContainPackage || linesContainLabel || linesContainReqPackage
|
|
||||||
|
|
||||||
const lastCommandFragmentIsLabel =
|
|
||||||
(commandFragment != null ? commandFragment.slice(0, 7) : undefined) ===
|
|
||||||
'\\label{'
|
|
||||||
const lastCommandFragmentIsPackage =
|
|
||||||
(commandFragment != null ? commandFragment.slice(0, 11) : undefined) ===
|
|
||||||
'\\usepackage'
|
|
||||||
const lastCommandFragmentIsReqPack =
|
|
||||||
(commandFragment != null ? commandFragment.slice(0, 15) : undefined) ===
|
|
||||||
'\\RequirePackage'
|
|
||||||
const lastCommandFragmentIsMeta =
|
|
||||||
lastCommandFragmentIsPackage ||
|
|
||||||
lastCommandFragmentIsLabel ||
|
|
||||||
lastCommandFragmentIsReqPack
|
|
||||||
|
|
||||||
if (linesContainMeta || lastCommandFragmentIsMeta) {
|
|
||||||
return this.Metadata.scheduleLoadDocMetaFromServer(this.$scope.docId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor.on('changeSession', e => {
|
|
||||||
e.oldSession.off('change', onChange)
|
|
||||||
return e.session.on('change', onChange)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllLabels() {
|
|
||||||
return this.Metadata.getAllLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllPackages() {
|
|
||||||
return this.Metadata.getAllPackages()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import 'ace/ace'
|
|
||||||
let HighlightedWordManager
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
|
|
||||||
class Highlight {
|
|
||||||
constructor(markerId, range, options) {
|
|
||||||
this.markerId = markerId
|
|
||||||
this.range = range
|
|
||||||
this.word = options.word
|
|
||||||
this.suggestions = options.suggestions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HighlightedWordManager = class HighlightedWordManager {
|
|
||||||
constructor(editor) {
|
|
||||||
this.editor = editor
|
|
||||||
this.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (this.highlights != null) {
|
|
||||||
this.highlights.forEach(highlight => {
|
|
||||||
return this.editor.getSession().removeMarker(highlight.markerId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return (this.highlights = [])
|
|
||||||
}
|
|
||||||
|
|
||||||
addHighlight(options) {
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const doc = session.getDocument()
|
|
||||||
// Set up Range that will automatically update it's positions when the
|
|
||||||
// document changes
|
|
||||||
const range = new Range()
|
|
||||||
range.start = doc.createAnchor({
|
|
||||||
row: options.row,
|
|
||||||
column: options.column,
|
|
||||||
})
|
|
||||||
range.end = doc.createAnchor({
|
|
||||||
row: options.row,
|
|
||||||
column: options.column + options.word.length,
|
|
||||||
})
|
|
||||||
// Prevent range from adding newly typed characters to the end of the word.
|
|
||||||
// This makes it appear as if the spelling error continues to the next word
|
|
||||||
// even after a space
|
|
||||||
range.end.$insertRight = true
|
|
||||||
|
|
||||||
const markerId = session.addMarker(
|
|
||||||
range,
|
|
||||||
'spelling-highlight',
|
|
||||||
'text',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
return this.highlights.push(new Highlight(markerId, range, options))
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHighlight(highlight) {
|
|
||||||
this.editor.getSession().removeMarker(highlight.markerId)
|
|
||||||
return (this.highlights = this.highlights.filter(hl => hl !== highlight))
|
|
||||||
}
|
|
||||||
|
|
||||||
removeWord(word) {
|
|
||||||
return this.highlights
|
|
||||||
.filter(highlight => highlight.word === word)
|
|
||||||
.forEach(highlight => {
|
|
||||||
return this.removeHighlight(highlight)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clearRow(row) {
|
|
||||||
return this.highlights
|
|
||||||
.filter(highlight => highlight.range.start.row === row)
|
|
||||||
.forEach(highlight => {
|
|
||||||
return this.removeHighlight(highlight)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
findHighlightWithinRange(range) {
|
|
||||||
return _.find(this.highlights, highlight => {
|
|
||||||
return this._doesHighlightOverlapRange(highlight, range.start, range.end)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_doesHighlightOverlapRange(highlight, start, end) {
|
|
||||||
const highlightRow = highlight.range.start.row
|
|
||||||
const highlightStartColumn = highlight.range.start.column
|
|
||||||
const highlightEndColumn = highlight.range.end.column
|
|
||||||
|
|
||||||
const highlightIsAllBeforeRange =
|
|
||||||
highlightRow < start.row ||
|
|
||||||
(highlightRow === start.row && highlightEndColumn <= start.column)
|
|
||||||
const highlightIsAllAfterRange =
|
|
||||||
highlightRow > end.row ||
|
|
||||||
(highlightRow === end.row && highlightStartColumn >= end.column)
|
|
||||||
return !(highlightIsAllBeforeRange || highlightIsAllAfterRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearHighlightTouchingRange(range) {
|
|
||||||
const highlight = _.find(this.highlights, hl => {
|
|
||||||
return this._doesHighlightTouchRange(hl, range.start, range.end)
|
|
||||||
})
|
|
||||||
if (highlight) {
|
|
||||||
return this.removeHighlight(highlight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_doesHighlightTouchRange(highlight, start, end) {
|
|
||||||
const highlightRow = highlight.range.start.row
|
|
||||||
const highlightStartColumn = highlight.range.start.column
|
|
||||||
const highlightEndColumn = highlight.range.end.column
|
|
||||||
|
|
||||||
const rangeStartIsWithinHighlight =
|
|
||||||
highlightStartColumn <= start.column && highlightEndColumn >= start.column
|
|
||||||
const rangeEndIsWithinHighlight =
|
|
||||||
highlightStartColumn <= end.column && highlightEndColumn >= end.column
|
|
||||||
|
|
||||||
return (
|
|
||||||
highlightRow === start.row &&
|
|
||||||
(rangeStartIsWithinHighlight || rangeEndIsWithinHighlight)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
export const IGNORED_MISSPELLINGS = [
|
|
||||||
'Overleaf',
|
|
||||||
'overleaf',
|
|
||||||
'ShareLaTeX',
|
|
||||||
'sharelatex',
|
|
||||||
'LaTeX',
|
|
||||||
'TeX',
|
|
||||||
'BibTeX',
|
|
||||||
'BibLaTeX',
|
|
||||||
'XeTeX',
|
|
||||||
'XeLaTeX',
|
|
||||||
'LuaTeX',
|
|
||||||
'LuaLaTeX',
|
|
||||||
'http',
|
|
||||||
'https',
|
|
||||||
'www',
|
|
||||||
'COVID',
|
|
||||||
'Lockdown',
|
|
||||||
'lockdown',
|
|
||||||
'Coronavirus',
|
|
||||||
'coronavirus',
|
|
||||||
]
|
|
|
@ -1,96 +0,0 @@
|
||||||
import HighlightedWordManager from './HighlightedWordManager'
|
|
||||||
import 'ace/ace'
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
|
|
||||||
class SpellCheckAdapter {
|
|
||||||
constructor(editor) {
|
|
||||||
this.replaceWord = this.replaceWord.bind(this)
|
|
||||||
this.editor = editor
|
|
||||||
this.highlightedWordManager = new HighlightedWordManager(this.editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
getLines() {
|
|
||||||
return this.editor.getValue().split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
getLineCount() {
|
|
||||||
return this.editor.session.getLength()
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstVisibleRowNum() {
|
|
||||||
return this.editor.renderer.layerConfig.firstRow
|
|
||||||
}
|
|
||||||
|
|
||||||
getLastVisibleRowNum() {
|
|
||||||
return this.editor.renderer.layerConfig.lastRow
|
|
||||||
}
|
|
||||||
|
|
||||||
getLinesByRows(rows) {
|
|
||||||
return rows.map(rowIdx => this.editor.session.doc.getLine(rowIdx))
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectionContents() {
|
|
||||||
return this.editor.getSelectedText()
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeChangeEvent(e) {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
getCoordsFromContextMenuEvent(e) {
|
|
||||||
e.domEvent.stopPropagation()
|
|
||||||
return {
|
|
||||||
x: e.domEvent.clientX,
|
|
||||||
y: e.domEvent.clientY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preventContextMenuEventDefault(e) {
|
|
||||||
e.domEvent.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
getHighlightFromCoords(coords) {
|
|
||||||
const position = this.editor.renderer.screenToTextCoordinates(
|
|
||||||
coords.x,
|
|
||||||
coords.y
|
|
||||||
)
|
|
||||||
return this.highlightedWordManager.findHighlightWithinRange({
|
|
||||||
start: position,
|
|
||||||
end: position,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
isContextMenuEventOnBottomHalf(e) {
|
|
||||||
const { clientY } = e.domEvent
|
|
||||||
const editorBoundingRect = e.target.container.getBoundingClientRect()
|
|
||||||
const relativeYPos =
|
|
||||||
(clientY - editorBoundingRect.top) / editorBoundingRect.height
|
|
||||||
return relativeYPos > 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
selectHighlightedWord(highlight) {
|
|
||||||
const { row } = highlight.range.start
|
|
||||||
const startColumn = highlight.range.start.column
|
|
||||||
const endColumn = highlight.range.end.column
|
|
||||||
|
|
||||||
this.editor
|
|
||||||
.getSession()
|
|
||||||
.getSelection()
|
|
||||||
.setSelectionRange(new Range(row, startColumn, row, endColumn))
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceWord(highlight, newWord) {
|
|
||||||
const { row } = highlight.range.start
|
|
||||||
const startColumn = highlight.range.start.column
|
|
||||||
const endColumn = highlight.range.end.column
|
|
||||||
|
|
||||||
this.editor
|
|
||||||
.getSession()
|
|
||||||
.replace(new Range(row, startColumn, row, endColumn), newWord)
|
|
||||||
|
|
||||||
// Bring editor back into focus after clicking on suggestion
|
|
||||||
this.editor.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SpellCheckAdapter
|
|
File diff suppressed because one or more lines are too long
|
@ -1,240 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
camelcase
|
|
||||||
*/
|
|
||||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
|
||||||
import 'ace/ace'
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
class TrackChangesAdapter {
|
|
||||||
constructor(editor) {
|
|
||||||
this.editor = editor
|
|
||||||
this.changeIdToMarkerIdMap = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
tearDown() {
|
|
||||||
this.changeIdToMarkerIdMap = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAnnotations() {
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
for (const change_id in this.changeIdToMarkerIdMap) {
|
|
||||||
const markers = this.changeIdToMarkerIdMap[change_id]
|
|
||||||
for (const marker_name in markers) {
|
|
||||||
const marker_id = markers[marker_name]
|
|
||||||
session.removeMarker(marker_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.changeIdToMarkerIdMap = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
onInsertAdded(change) {
|
|
||||||
const start = this.shareJsOffsetToRowColumn(change.op.p)
|
|
||||||
const end = this.shareJsOffsetToRowColumn(change.op.p + change.op.i.length)
|
|
||||||
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const background_range = new Range(
|
|
||||||
start.row,
|
|
||||||
start.column,
|
|
||||||
end.row,
|
|
||||||
end.column
|
|
||||||
)
|
|
||||||
const background_marker_id = session.addMarker(
|
|
||||||
background_range,
|
|
||||||
'track-changes-marker track-changes-added-marker',
|
|
||||||
'text'
|
|
||||||
)
|
|
||||||
const callout_marker_id = this.createCalloutMarker(
|
|
||||||
start,
|
|
||||||
'track-changes-added-marker-callout'
|
|
||||||
)
|
|
||||||
|
|
||||||
this.changeIdToMarkerIdMap[change.id] = {
|
|
||||||
background_marker_id,
|
|
||||||
callout_marker_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteAdded(change) {
|
|
||||||
const position = this.shareJsOffsetToRowColumn(change.op.p)
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
|
|
||||||
const markerLayer = this.editor.renderer.$markerBack
|
|
||||||
const klass = 'track-changes-marker track-changes-deleted-marker'
|
|
||||||
const background_range = this.makeZeroWidthRange(position)
|
|
||||||
const background_marker_id = session.addMarker(
|
|
||||||
background_range,
|
|
||||||
klass,
|
|
||||||
(html, range, left, top, config) =>
|
|
||||||
markerLayer.drawSingleLineMarker(
|
|
||||||
html,
|
|
||||||
range,
|
|
||||||
`${klass} ace_start`,
|
|
||||||
config,
|
|
||||||
0,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const callout_marker_id = this.createCalloutMarker(
|
|
||||||
position,
|
|
||||||
'track-changes-deleted-marker-callout'
|
|
||||||
)
|
|
||||||
|
|
||||||
this.changeIdToMarkerIdMap[change.id] = {
|
|
||||||
background_marker_id,
|
|
||||||
callout_marker_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onInsertRemoved(change) {
|
|
||||||
if (this.changeIdToMarkerIdMap[change.id] == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { background_marker_id, callout_marker_id } =
|
|
||||||
this.changeIdToMarkerIdMap[change.id]
|
|
||||||
delete this.changeIdToMarkerIdMap[change.id]
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
session.removeMarker(background_marker_id)
|
|
||||||
session.removeMarker(callout_marker_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteRemoved(change) {
|
|
||||||
if (this.changeIdToMarkerIdMap[change.id] == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { background_marker_id, callout_marker_id } =
|
|
||||||
this.changeIdToMarkerIdMap[change.id]
|
|
||||||
delete this.changeIdToMarkerIdMap[change.id]
|
|
||||||
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
session.removeMarker(background_marker_id)
|
|
||||||
session.removeMarker(callout_marker_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeMoved(change) {
|
|
||||||
let end
|
|
||||||
const start = this.shareJsOffsetToRowColumn(change.op.p)
|
|
||||||
if (change.op.i != null) {
|
|
||||||
end = this.shareJsOffsetToRowColumn(change.op.p + change.op.i.length)
|
|
||||||
} else {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
this.updateMarker(change.id, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCommentAdded(comment) {
|
|
||||||
if (this.changeIdToMarkerIdMap[comment.id] == null) {
|
|
||||||
// Only create new markers if they don't already exist
|
|
||||||
const start = this.shareJsOffsetToRowColumn(comment.op.p)
|
|
||||||
const end = this.shareJsOffsetToRowColumn(
|
|
||||||
comment.op.p + comment.op.c.length
|
|
||||||
)
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const background_range = new Range(
|
|
||||||
start.row,
|
|
||||||
start.column,
|
|
||||||
end.row,
|
|
||||||
end.column
|
|
||||||
)
|
|
||||||
const background_marker_id = session.addMarker(
|
|
||||||
background_range,
|
|
||||||
'track-changes-marker track-changes-comment-marker',
|
|
||||||
'text'
|
|
||||||
)
|
|
||||||
const callout_marker_id = this.createCalloutMarker(
|
|
||||||
start,
|
|
||||||
'track-changes-comment-marker-callout'
|
|
||||||
)
|
|
||||||
this.changeIdToMarkerIdMap[comment.id] = {
|
|
||||||
background_marker_id,
|
|
||||||
callout_marker_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCommentMoved(comment) {
|
|
||||||
const start = this.shareJsOffsetToRowColumn(comment.op.p)
|
|
||||||
const end = this.shareJsOffsetToRowColumn(
|
|
||||||
comment.op.p + comment.op.c.length
|
|
||||||
)
|
|
||||||
this.updateMarker(comment.id, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCommentRemoved(comment) {
|
|
||||||
if (this.changeIdToMarkerIdMap[comment.id] != null) {
|
|
||||||
// Resolved comments may not have marker ids
|
|
||||||
const { background_marker_id, callout_marker_id } =
|
|
||||||
this.changeIdToMarkerIdMap[comment.id]
|
|
||||||
delete this.changeIdToMarkerIdMap[comment.id]
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
session.removeMarker(background_marker_id)
|
|
||||||
session.removeMarker(callout_marker_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMarker(change_id, start, end) {
|
|
||||||
if (this.changeIdToMarkerIdMap[change_id] == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const markers = session.getMarkers()
|
|
||||||
const { background_marker_id, callout_marker_id } =
|
|
||||||
this.changeIdToMarkerIdMap[change_id]
|
|
||||||
if (background_marker_id != null && markers[background_marker_id] != null) {
|
|
||||||
const background_marker = markers[background_marker_id]
|
|
||||||
background_marker.range.start = start
|
|
||||||
background_marker.range.end = end
|
|
||||||
}
|
|
||||||
if (callout_marker_id != null && markers[callout_marker_id] != null) {
|
|
||||||
const callout_marker = markers[callout_marker_id]
|
|
||||||
callout_marker.range.start = start
|
|
||||||
callout_marker.range.end = start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shareJsOffsetToRowColumn(offset) {
|
|
||||||
const lines = this.editor.getSession().getDocument().getAllLines()
|
|
||||||
return EditorShareJsCodec.shareJsOffsetToRowColumn(offset, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
createCalloutMarker(position, klass) {
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const callout_range = this.makeZeroWidthRange(position)
|
|
||||||
const markerLayer = this.editor.renderer.$markerBack
|
|
||||||
return session.addMarker(
|
|
||||||
callout_range,
|
|
||||||
klass,
|
|
||||||
(html, range, left, top, config) =>
|
|
||||||
markerLayer.drawSingleLineMarker(
|
|
||||||
html,
|
|
||||||
range,
|
|
||||||
`track-changes-marker-callout ${klass} ace_start`,
|
|
||||||
config,
|
|
||||||
0,
|
|
||||||
'width: auto; right: 0;'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeZeroWidthRange(position) {
|
|
||||||
const ace_range = new Range(
|
|
||||||
position.row,
|
|
||||||
position.column,
|
|
||||||
position.row,
|
|
||||||
position.column
|
|
||||||
)
|
|
||||||
// Our delete marker is zero characters wide, but Ace doesn't draw ranges
|
|
||||||
// that are empty. So we monkey patch the range to tell Ace it's not empty
|
|
||||||
// We do want to claim to be empty if we're off screen after clipping rows
|
|
||||||
// though. This is the code we need to trick:
|
|
||||||
// var range = marker.range.clipRows(config.firstRow, config.lastRow);
|
|
||||||
// if (range.isEmpty()) continue;
|
|
||||||
ace_range.clipRows = function (first_row, last_row) {
|
|
||||||
this.isEmpty = function () {
|
|
||||||
return first_row > this.end.row || last_row < this.start.row
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return ace_range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default TrackChangesAdapter
|
|
|
@ -1,664 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
camelcase,
|
|
||||||
max-len
|
|
||||||
*/
|
|
||||||
import EditorShareJsCodec from '../../../EditorShareJsCodec'
|
|
||||||
import 'ace/ace'
|
|
||||||
import '../../../../../utils/EventEmitter'
|
|
||||||
import '../../../../colors/ColorManager'
|
|
||||||
import { debugConsole } from '@/utils/debugging'
|
|
||||||
const { Range } = ace.require('ace/range')
|
|
||||||
class TrackChangesManager {
|
|
||||||
constructor($scope, editor, element, adapter) {
|
|
||||||
this._doneUpdateThisLoop = false
|
|
||||||
this._pendingUpdates = false
|
|
||||||
|
|
||||||
this.onChangeSession = this.onChangeSession.bind(this)
|
|
||||||
this.onChangeSelection = this.onChangeSelection.bind(this)
|
|
||||||
this.onCut = this.onCut.bind(this)
|
|
||||||
this.onPaste = this.onPaste.bind(this)
|
|
||||||
this.onResize = this.onResize.bind(this)
|
|
||||||
this.tearDown = this.tearDown.bind(this)
|
|
||||||
|
|
||||||
this.$scope = $scope
|
|
||||||
this.editor = editor
|
|
||||||
this.element = element
|
|
||||||
this.adapter = adapter
|
|
||||||
this._scrollTimeout = null
|
|
||||||
this.changingSelection = false
|
|
||||||
|
|
||||||
if (window.trackChangesManager == null) {
|
|
||||||
window.trackChangesManager = this
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$scope.$watch('trackChanges', track_changes => {
|
|
||||||
if (track_changes == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.setTrackChanges(track_changes)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$watch('sharejsDoc', (doc, oldDoc) => {
|
|
||||||
if (doc == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (oldDoc != null) {
|
|
||||||
this.disconnectFromDoc(oldDoc)
|
|
||||||
}
|
|
||||||
this.setTrackChanges(this.$scope.trackChanges)
|
|
||||||
this.connectToDoc(doc)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('comment:add', (e, thread_id, offset, length) => {
|
|
||||||
this.addCommentToSelection(thread_id, offset, length)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('comment:select_line', e => {
|
|
||||||
this.selectLineIfNoSelection()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('changes:accept', (e, change_ids) => {
|
|
||||||
this.acceptChangeIds(change_ids)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('changes:reject', (e, change_ids) => {
|
|
||||||
this.rejectChangeIds(change_ids)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('comment:remove', (e, comment_id) => {
|
|
||||||
this.removeCommentId(comment_id)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('comment:resolve_threads', (e, thread_ids) => {
|
|
||||||
this.hideCommentsByThreadIds(thread_ids)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('comment:unresolve_thread', (e, thread_id) => {
|
|
||||||
this.showCommentByThreadId(thread_id)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$scope.$on('review-panel:recalculate-screen-positions', () => {
|
|
||||||
this.recalculateReviewEntriesScreenPositions()
|
|
||||||
})
|
|
||||||
|
|
||||||
this._resetCutState()
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeSession(e) {
|
|
||||||
this.clearAnnotations()
|
|
||||||
this.redrawAnnotations()
|
|
||||||
|
|
||||||
if (this.editor) {
|
|
||||||
this.editor.session.on('changeScrollTop', this.onChangeScroll.bind(this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeScroll() {
|
|
||||||
if (this._scrollTimeout == null) {
|
|
||||||
return (this._scrollTimeout = setTimeout(() => {
|
|
||||||
this.recalculateVisibleEntries()
|
|
||||||
this.$scope.$apply()
|
|
||||||
return (this._scrollTimeout = null)
|
|
||||||
}, 200))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeSelection() {
|
|
||||||
// Deletes can send about 5 changeSelection events, so
|
|
||||||
// just act on the last one.
|
|
||||||
if (!this.changingSelection) {
|
|
||||||
this.changingSelection = true
|
|
||||||
return this.$scope.$evalAsync(() => {
|
|
||||||
this.changingSelection = false
|
|
||||||
return this.updateFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize() {
|
|
||||||
this.recalculateReviewEntriesScreenPositions()
|
|
||||||
}
|
|
||||||
|
|
||||||
connectToDoc(doc) {
|
|
||||||
this.rangesTracker = doc.ranges
|
|
||||||
this.setTrackChanges(this.$scope.trackChanges)
|
|
||||||
|
|
||||||
doc.on('ranges:dirty', () => {
|
|
||||||
this.updateAnnotations()
|
|
||||||
})
|
|
||||||
doc.on('ranges:clear', () => {
|
|
||||||
this.clearAnnotations()
|
|
||||||
})
|
|
||||||
doc.on('ranges:redraw', () => {
|
|
||||||
this.redrawAnnotations()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectFromDoc(doc) {
|
|
||||||
doc.off('ranges:clear')
|
|
||||||
doc.off('ranges:redraw')
|
|
||||||
doc.off('ranges:dirty')
|
|
||||||
}
|
|
||||||
|
|
||||||
tearDown() {
|
|
||||||
this.adapter.tearDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
setTrackChanges(value) {
|
|
||||||
if (value) {
|
|
||||||
if (this.$scope.sharejsDoc != null) {
|
|
||||||
this.$scope.sharejsDoc.track_changes_as = window.user.id || 'anonymous'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.$scope.sharejsDoc != null) {
|
|
||||||
this.$scope.sharejsDoc.track_changes_as = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAnnotations() {
|
|
||||||
this.adapter.clearAnnotations()
|
|
||||||
}
|
|
||||||
|
|
||||||
redrawAnnotations() {
|
|
||||||
for (const change of Array.from(this.rangesTracker.changes)) {
|
|
||||||
if (change.op.i != null) {
|
|
||||||
this.adapter.onInsertAdded(change)
|
|
||||||
} else if (change.op.d != null) {
|
|
||||||
this.adapter.onDeleteAdded(change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.from(this.rangesTracker.comments).forEach(comment => {
|
|
||||||
if (!this.isCommentResolved(comment)) {
|
|
||||||
this.adapter.onCommentAdded(comment)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.broadcastChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAnnotations() {
|
|
||||||
// Doc updates with multiple ops, like search/replace or block comments
|
|
||||||
// will call this with every individual op in a single event loop. So only
|
|
||||||
// do the first this loop, then schedule an update for the next loop for
|
|
||||||
// the rest.
|
|
||||||
if (!this._doneUpdateThisLoop) {
|
|
||||||
this._doUpdateAnnotations()
|
|
||||||
this._doneUpdateThisLoop = true
|
|
||||||
return setTimeout(() => {
|
|
||||||
if (this._pendingUpdates) {
|
|
||||||
this._doUpdateAnnotations()
|
|
||||||
}
|
|
||||||
this._doneUpdateThisLoop = false
|
|
||||||
this._pendingUpdates = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._pendingUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_doUpdateAnnotations() {
|
|
||||||
let change, comment
|
|
||||||
const dirty = this.rangesTracker.getDirtyState()
|
|
||||||
|
|
||||||
let updateMarkers = false
|
|
||||||
|
|
||||||
for (const id in dirty.change.added) {
|
|
||||||
change = dirty.change.added[id]
|
|
||||||
if (change.op.i != null) {
|
|
||||||
this.adapter.onInsertAdded(change)
|
|
||||||
} else if (change.op.d != null) {
|
|
||||||
this.adapter.onDeleteAdded(change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id in dirty.change.removed) {
|
|
||||||
change = dirty.change.removed[id]
|
|
||||||
if (change.op.i != null) {
|
|
||||||
this.adapter.onInsertRemoved(change)
|
|
||||||
} else if (change.op.d != null) {
|
|
||||||
this.adapter.onDeleteRemoved(change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id in dirty.change.moved) {
|
|
||||||
change = dirty.change.moved[id]
|
|
||||||
updateMarkers = true
|
|
||||||
this.adapter.onChangeMoved(change)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id in dirty.comment.added) {
|
|
||||||
comment = dirty.comment.added[id]
|
|
||||||
if (!this.isCommentResolved(comment)) {
|
|
||||||
this.adapter.onCommentAdded(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id in dirty.comment.removed) {
|
|
||||||
comment = dirty.comment.removed[id]
|
|
||||||
if (!this.isCommentResolved(comment)) {
|
|
||||||
this.adapter.onCommentRemoved(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id in dirty.comment.moved) {
|
|
||||||
comment = dirty.comment.moved[id]
|
|
||||||
if (this.adapter.onCommentMoved && !this.isCommentResolved(comment)) {
|
|
||||||
updateMarkers = true
|
|
||||||
this.adapter.onCommentMoved(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For now, if not using ACE don't worry about the markers
|
|
||||||
*/
|
|
||||||
if (!this.editor) {
|
|
||||||
updateMarkers = false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rangesTracker.resetDirtyState()
|
|
||||||
if (updateMarkers) {
|
|
||||||
this.editor.renderer.updateBackMarkers()
|
|
||||||
}
|
|
||||||
this.broadcastChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
addComment(offset, content, thread_id) {
|
|
||||||
const op = { c: content, p: offset, t: thread_id }
|
|
||||||
// @rangesTracker.applyOp op # Will apply via sharejs
|
|
||||||
this.$scope.sharejsDoc.submitOp(op)
|
|
||||||
}
|
|
||||||
|
|
||||||
addCommentToSelection(thread_id, offset, length) {
|
|
||||||
const start = this.adapter.shareJsOffsetToRowColumn(offset)
|
|
||||||
const end = this.adapter.shareJsOffsetToRowColumn(offset + length)
|
|
||||||
const range = new Range(start.row, start.column, end.row, end.column)
|
|
||||||
const content = this.editor.session.getTextRange(range)
|
|
||||||
this.addComment(offset, content, thread_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
isCommentResolved(comment) {
|
|
||||||
return this.rangesTracker.resolvedThreadIds[comment.op.t]
|
|
||||||
}
|
|
||||||
|
|
||||||
selectLineIfNoSelection() {
|
|
||||||
if (this.editor.selection.isEmpty()) {
|
|
||||||
this.editor.selection.selectLine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptChangeIds(change_ids) {
|
|
||||||
this.rangesTracker.removeChangeIds(change_ids)
|
|
||||||
this.updateAnnotations()
|
|
||||||
this.updateFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectChangeIds(change_ids) {
|
|
||||||
const changes = this.rangesTracker.getChanges(change_ids)
|
|
||||||
if (changes.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// When doing bulk rejections, adjacent changes might interact with each other.
|
|
||||||
// Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
|
|
||||||
//
|
|
||||||
// "foo bar baz" -> "foo quux baz"
|
|
||||||
//
|
|
||||||
// The change above will be modeled with two ops, with the insertion going first:
|
|
||||||
//
|
|
||||||
// foo quux baz
|
|
||||||
// |--| -> insertion of "quux", op 1, at position 4
|
|
||||||
// | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
|
|
||||||
//
|
|
||||||
// When rejecting these changes at once, if the insertion is rejected first, we get unexpected
|
|
||||||
// results. What happens is:
|
|
||||||
//
|
|
||||||
// 1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
|
||||||
// starting from position 4;
|
|
||||||
//
|
|
||||||
// "foo quux baz" -> "foo baz"
|
|
||||||
// |--| -> 4 characters to be removed
|
|
||||||
//
|
|
||||||
// 2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
|
|
||||||
// the word "quuux" was still present).
|
|
||||||
//
|
|
||||||
// "foo baz" -> "foo bazbar"
|
|
||||||
// | -> deletion of "bar" is reverted by reinserting "bar" at position 8
|
|
||||||
//
|
|
||||||
// While the intended result would be "foo bar baz", what we get is:
|
|
||||||
//
|
|
||||||
// "foo bazbar" (note "bar" readded at position 8)
|
|
||||||
//
|
|
||||||
// The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
|
|
||||||
// from position 4. This includes the position where the deletion exists; when that position is
|
|
||||||
// cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
|
|
||||||
// As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
|
|
||||||
// does so at the outdated position (position 8, which was valid when "quux" was present).
|
|
||||||
//
|
|
||||||
// To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
|
|
||||||
// subsequent operations that come after. Reverse sorting the operations based on position will
|
|
||||||
// achieve it; in the case above, it makes sure that the the deletion is reverted first:
|
|
||||||
//
|
|
||||||
// 1) Rejecting the deletion adds the deleted word "bar" at position 8
|
|
||||||
//
|
|
||||||
// "foo quux baz" -> "foo quuxbar baz"
|
|
||||||
// | -> deletion of "bar" is reverted by
|
|
||||||
// reinserting "bar" at position 8
|
|
||||||
//
|
|
||||||
// 2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
|
||||||
// starting from position 4 and achieves the expected result:
|
|
||||||
//
|
|
||||||
// "foo quuxbar baz" -> "foo bar baz"
|
|
||||||
// |--| -> 4 characters to be removed
|
|
||||||
|
|
||||||
changes.sort((a, b) => b.op.p - a.op.p)
|
|
||||||
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
for (const change of Array.from(changes)) {
|
|
||||||
if (change.op.d != null) {
|
|
||||||
const content = change.op.d
|
|
||||||
const position = this.adapter.shareJsOffsetToRowColumn(change.op.p)
|
|
||||||
session.$fromReject = true // Tell track changes to cancel out delete
|
|
||||||
session.insert(position, content)
|
|
||||||
session.$fromReject = false
|
|
||||||
} else if (change.op.i != null) {
|
|
||||||
const start = this.adapter.shareJsOffsetToRowColumn(change.op.p)
|
|
||||||
const end = this.adapter.shareJsOffsetToRowColumn(
|
|
||||||
change.op.p + change.op.i.length
|
|
||||||
)
|
|
||||||
const editor_text = session.getDocument().getTextRange({ start, end })
|
|
||||||
if (editor_text !== change.op.i) {
|
|
||||||
throw new Error(
|
|
||||||
`Op to be removed (${JSON.stringify(
|
|
||||||
change.op
|
|
||||||
)}), does not match editor text, '${editor_text}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
session.$fromReject = true
|
|
||||||
session.remove({ start, end })
|
|
||||||
session.$fromReject = false
|
|
||||||
} else {
|
|
||||||
throw new Error(`unknown change: ${JSON.stringify(change)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(() => this.updateFocus())
|
|
||||||
}
|
|
||||||
|
|
||||||
removeCommentId(comment_id) {
|
|
||||||
this.rangesTracker.removeCommentId(comment_id)
|
|
||||||
return this.updateAnnotations()
|
|
||||||
}
|
|
||||||
|
|
||||||
hideCommentsByThreadIds(thread_ids) {
|
|
||||||
const resolve_ids = {}
|
|
||||||
const comments = this.rangesTracker.comments || []
|
|
||||||
for (const id of Array.from(thread_ids)) {
|
|
||||||
resolve_ids[id] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const comment of comments) {
|
|
||||||
if (resolve_ids[comment.op.t]) {
|
|
||||||
this.adapter.onCommentRemoved(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.broadcastChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
showCommentByThreadId(thread_id) {
|
|
||||||
const comments = this.rangesTracker.comments || []
|
|
||||||
for (const comment of comments) {
|
|
||||||
if (comment.op.t === thread_id && !this.isCommentResolved(comment)) {
|
|
||||||
this.adapter.onCommentAdded(comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.broadcastChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
_resetCutState() {
|
|
||||||
return (this._cutState = {
|
|
||||||
text: null,
|
|
||||||
comments: [],
|
|
||||||
docId: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onCut() {
|
|
||||||
this._resetCutState()
|
|
||||||
const selection = this.editor.getSelectionRange()
|
|
||||||
const selection_start = this._rangeToShareJs(selection.start)
|
|
||||||
const selection_end = this._rangeToShareJs(selection.end)
|
|
||||||
this._cutState.text = this.editor.getSelectedText()
|
|
||||||
this._cutState.docId = this.$scope.docId
|
|
||||||
return (() => {
|
|
||||||
const result = []
|
|
||||||
for (const comment of Array.from(this.rangesTracker.comments)) {
|
|
||||||
const comment_start = comment.op.p
|
|
||||||
const comment_end = comment_start + comment.op.c.length
|
|
||||||
if (selection_start <= comment_start && comment_end <= selection_end) {
|
|
||||||
result.push(
|
|
||||||
this._cutState.comments.push({
|
|
||||||
offset: comment.op.p - selection_start,
|
|
||||||
text: comment.op.c,
|
|
||||||
comment,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result.push(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaste() {
|
|
||||||
this.editor.once('change', change => {
|
|
||||||
if (change.action !== 'insert') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const pasted_text = change.lines.join('\n')
|
|
||||||
const paste_offset = this._rangeToShareJs(change.start)
|
|
||||||
// We have to wait until the change has been processed by the range
|
|
||||||
// tracker, since if we move the ops into place beforehand, they will be
|
|
||||||
// moved again when the changes are processed by the range tracker. This
|
|
||||||
// ranges:dirty event is fired after the doc has applied the changes to
|
|
||||||
// the range tracker.
|
|
||||||
this.$scope.sharejsDoc.on('ranges:dirty.paste', () => {
|
|
||||||
// Doc event emitter uses namespaced events
|
|
||||||
this.$scope.sharejsDoc.off('ranges:dirty.paste')
|
|
||||||
if (
|
|
||||||
pasted_text === this._cutState.text &&
|
|
||||||
this.$scope.docId === this._cutState.docId
|
|
||||||
) {
|
|
||||||
for (const { comment, offset, text } of Array.from(
|
|
||||||
this._cutState.comments
|
|
||||||
)) {
|
|
||||||
const op = { c: text, p: paste_offset + offset, t: comment.id }
|
|
||||||
this.$scope.sharejsDoc.submitOp(op)
|
|
||||||
} // Resubmitting an existing comment op (by thread id) will move it
|
|
||||||
}
|
|
||||||
this._resetCutState()
|
|
||||||
// Check that comments still match text. Will throw error if not.
|
|
||||||
this.rangesTracker.validate(this.editor.getValue())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMapping() {
|
|
||||||
// TODO: reintroduce this check
|
|
||||||
let background_marker_id, callout_marker_id, end, marker, op, start
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
|
|
||||||
// Make a copy of session.getMarkers() so we can modify it
|
|
||||||
const markers = {}
|
|
||||||
const object = session.getMarkers()
|
|
||||||
for (const marker_id in object) {
|
|
||||||
marker = object[marker_id]
|
|
||||||
markers[marker_id] = marker
|
|
||||||
}
|
|
||||||
|
|
||||||
const expected_markers = []
|
|
||||||
for (const change of Array.from(this.rangesTracker.changes)) {
|
|
||||||
if (this.adapter.changeIdToMarkerIdMap[change.id] != null) {
|
|
||||||
;({ op } = change)
|
|
||||||
;({ background_marker_id, callout_marker_id } =
|
|
||||||
this.adapter.changeIdToMarkerIdMap[change.id])
|
|
||||||
start = this.adapter.shareJsOffsetToRowColumn(op.p)
|
|
||||||
if (op.i != null) {
|
|
||||||
end = this.adapter.shareJsOffsetToRowColumn(op.p + op.i.length)
|
|
||||||
} else if (op.d != null) {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
expected_markers.push({
|
|
||||||
marker_id: background_marker_id,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
expected_markers.push({
|
|
||||||
marker_id: callout_marker_id,
|
|
||||||
start,
|
|
||||||
end: start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const comment of Array.from(this.rangesTracker.comments)) {
|
|
||||||
if (this.adapter.changeIdToMarkerIdMap[comment.id] != null) {
|
|
||||||
;({ background_marker_id, callout_marker_id } =
|
|
||||||
this.adapter.changeIdToMarkerIdMap[comment.id])
|
|
||||||
start = this.adapter.shareJsOffsetToRowColumn(comment.op.p)
|
|
||||||
end = this.adapter.shareJsOffsetToRowColumn(
|
|
||||||
comment.op.p + comment.op.c.length
|
|
||||||
)
|
|
||||||
expected_markers.push({
|
|
||||||
marker_id: background_marker_id,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
expected_markers.push({
|
|
||||||
marker_id: callout_marker_id,
|
|
||||||
start,
|
|
||||||
end: start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { marker_id, start, end } of Array.from(expected_markers)) {
|
|
||||||
marker = markers[marker_id]
|
|
||||||
delete markers[marker_id]
|
|
||||||
if (
|
|
||||||
marker.range.start.row !== start.row ||
|
|
||||||
marker.range.start.column !== start.column ||
|
|
||||||
marker.range.end.row !== end.row ||
|
|
||||||
marker.range.end.column !== end.column
|
|
||||||
) {
|
|
||||||
debugConsole.error("Change doesn't match marker anymore", {
|
|
||||||
marker,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (() => {
|
|
||||||
const result = []
|
|
||||||
for (const marker_id in markers) {
|
|
||||||
marker = markers[marker_id]
|
|
||||||
if (/track-changes/.test(marker.clazz)) {
|
|
||||||
result.push(debugConsole.error('Orphaned ace marker', marker))
|
|
||||||
} else {
|
|
||||||
result.push(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastChange() {
|
|
||||||
this.$scope.$emit('editor:track-changes:changed', this.$scope.docId)
|
|
||||||
}
|
|
||||||
|
|
||||||
recalculateReviewEntriesScreenPositions() {
|
|
||||||
if (!this.editor) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const session = this.editor.getSession()
|
|
||||||
const { renderer } = this.editor
|
|
||||||
const entries = this._getCurrentDocEntries()
|
|
||||||
const object = entries || {}
|
|
||||||
for (const entry_id in object) {
|
|
||||||
const entry = object[entry_id]
|
|
||||||
const doc_position = this.adapter.shareJsOffsetToRowColumn(entry.offset)
|
|
||||||
const screen_position = session.documentToScreenPosition(
|
|
||||||
doc_position.row,
|
|
||||||
doc_position.column
|
|
||||||
)
|
|
||||||
const y = screen_position.row * renderer.lineHeight
|
|
||||||
if (entry.screenPos == null) {
|
|
||||||
entry.screenPos = {}
|
|
||||||
}
|
|
||||||
entry.screenPos.y = y
|
|
||||||
entry.docPos = doc_position
|
|
||||||
}
|
|
||||||
this.recalculateVisibleEntries()
|
|
||||||
this.$scope.$apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
recalculateVisibleEntries() {
|
|
||||||
const OFFSCREEN_ROWS = 20
|
|
||||||
|
|
||||||
// With less than this number of entries, don't bother culling to avoid
|
|
||||||
// little UI jumps when scrolling.
|
|
||||||
const CULL_AFTER = 100
|
|
||||||
|
|
||||||
const { firstRow, lastRow } = this.editor.renderer.layerConfig
|
|
||||||
const entries = this._getCurrentDocEntries() || {}
|
|
||||||
const entriesLength = Object.keys(entries).length
|
|
||||||
let changed = false
|
|
||||||
for (const entry_id in entries) {
|
|
||||||
const entry = entries[entry_id]
|
|
||||||
const old = entry.visible
|
|
||||||
entry.visible =
|
|
||||||
entriesLength < CULL_AFTER ||
|
|
||||||
(firstRow - OFFSCREEN_ROWS <= entry.docPos.row &&
|
|
||||||
entry.docPos.row <= lastRow + OFFSCREEN_ROWS)
|
|
||||||
if (entry.visible !== old) {
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
this.$scope.$emit('editor:track-changes:visibility_changed')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCurrentDocEntries() {
|
|
||||||
const doc_id = this.$scope.docId
|
|
||||||
const entries = this.$scope.reviewPanel.entries[doc_id] || {}
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFocus() {
|
|
||||||
if (this.editor) {
|
|
||||||
const selection = this.editor.getSelectionRange()
|
|
||||||
const selection_start = this._rangeToShareJs(selection.start)
|
|
||||||
const selection_end = this._rangeToShareJs(selection.end)
|
|
||||||
const is_selection = selection_start !== selection_end
|
|
||||||
this.$scope.$emit(
|
|
||||||
'editor:focus:changed',
|
|
||||||
selection_start,
|
|
||||||
selection_end,
|
|
||||||
is_selection
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_rangeToShareJs(range) {
|
|
||||||
const lines = this.editor.getSession().getDocument().getLines(0, range.row)
|
|
||||||
return EditorShareJsCodec.rangeToShareJs(range, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
_changeToShareJs(delta) {
|
|
||||||
const lines = this.editor
|
|
||||||
.getSession()
|
|
||||||
.getDocument()
|
|
||||||
.getLines(0, delta.start.row)
|
|
||||||
return EditorShareJsCodec.changeToShareJs(delta, lines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default TrackChangesManager
|
|
|
@ -1,30 +0,0 @@
|
||||||
import 'ace/ace'
|
|
||||||
const BuiltInUndoManager = ace.require('ace/undomanager').UndoManager
|
|
||||||
|
|
||||||
class UndoManager {
|
|
||||||
constructor(editor) {
|
|
||||||
this.editor = editor
|
|
||||||
|
|
||||||
this.onChangeSession = this.onChangeSession.bind(this)
|
|
||||||
this.onChange = this.onChange.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeSession(session) {
|
|
||||||
session.setUndoManager(new BuiltInUndoManager())
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(change) {
|
|
||||||
if (change.origin !== 'remote') return
|
|
||||||
|
|
||||||
// HACK: remote changes in Ace are added by the ShareJS/Ace adapter
|
|
||||||
// asynchronously via a timeout (see attach_ace function). This makes it
|
|
||||||
// impossible to clear to undo stack when remote changes are received.
|
|
||||||
// To hack around this we queue the undo stack clear so that it applies
|
|
||||||
// after the change is applied
|
|
||||||
setTimeout(() => {
|
|
||||||
this.editor.getSession().getUndoManager().reset()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UndoManager
|
|
|
@ -1,25 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
import App from '../../../base'
|
|
||||||
|
|
||||||
export default App.factory('files', [
|
|
||||||
'ide',
|
|
||||||
function (ide) {
|
|
||||||
const Files = {
|
|
||||||
getTeXFiles() {
|
|
||||||
const texFiles = []
|
|
||||||
ide.fileTreeManager.forEachEntity(function (entity, _folder, path) {
|
|
||||||
if (
|
|
||||||
entity.type === 'doc' &&
|
|
||||||
/.*\.(tex|md|txt|tikz)/.test(entity.name)
|
|
||||||
) {
|
|
||||||
const cloned = _.clone(entity)
|
|
||||||
cloned.path = path
|
|
||||||
texFiles.push(cloned)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return texFiles
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return Files
|
|
||||||
},
|
|
||||||
])
|
|
|
@ -1,54 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import App from '../../../base'
|
|
||||||
|
|
||||||
export default App.factory('graphics', [
|
|
||||||
'ide',
|
|
||||||
function (ide) {
|
|
||||||
const Graphics = {
|
|
||||||
getGraphicsFiles() {
|
|
||||||
const graphicsFiles = []
|
|
||||||
ide.fileTreeManager.forEachEntity(function (entity, folder, path) {
|
|
||||||
if (
|
|
||||||
entity.type === 'file' &&
|
|
||||||
__guardMethod__(
|
|
||||||
entity != null ? entity.name : undefined,
|
|
||||||
'match',
|
|
||||||
o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const cloned = _.clone(entity)
|
|
||||||
cloned.path = path
|
|
||||||
return graphicsFiles.push(cloned)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return graphicsFiles
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return Graphics
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
function __guardMethod__(obj, methodName, transform) {
|
|
||||||
if (
|
|
||||||
typeof obj !== 'undefined' &&
|
|
||||||
obj !== null &&
|
|
||||||
typeof obj[methodName] === 'function'
|
|
||||||
) {
|
|
||||||
return transform(obj, methodName)
|
|
||||||
} else {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import App from '../../../base'
|
|
||||||
|
|
||||||
export default App.factory('preamble', function (ide) {
|
|
||||||
const Preamble = {
|
|
||||||
getPreambleText() {
|
|
||||||
const text = ide.editorManager.getCurrentDocValue().slice(0, 5000)
|
|
||||||
const preamble =
|
|
||||||
__guard__(text.match(/([^]*)^\\begin\{document\}/m), x => x[1]) || ''
|
|
||||||
return preamble
|
|
||||||
},
|
|
||||||
|
|
||||||
getGraphicsPaths() {
|
|
||||||
let match
|
|
||||||
const preamble = Preamble.getPreambleText()
|
|
||||||
const graphicsPathsArgs =
|
|
||||||
__guard__(preamble.match(/\\graphicspath\{(.*)\}/), x => x[1]) || ''
|
|
||||||
const paths = []
|
|
||||||
const re = /\{([^}]*)\}/g
|
|
||||||
while ((match = re.exec(graphicsPathsArgs))) {
|
|
||||||
paths.push(match[1])
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return Preamble
|
|
||||||
})
|
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
|
@ -280,26 +280,6 @@ export default App.controller('ReviewPanelController', [
|
||||||
return rangesTrackers[doc_id]
|
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) {
|
||||||
|
|
|
@ -249,16 +249,14 @@ 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
|
dispatchScrollEvent(deltaY * 4)
|
||||||
scrollAce(-top)
|
return e.preventDefault()
|
||||||
dispatchScrollEvent(deltaY * 4)
|
})
|
||||||
return e.preventDefault()
|
|
||||||
})
|
|
||||||
|
|
||||||
// We always scroll by telling Ace to scroll and then updating the
|
// We always scroll by telling Ace to scroll and then updating the
|
||||||
// review panel. This lets Ace manage the size of the scroller and
|
// review panel. This lets Ace manage the size of the scroller and
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import App from '../../base'
|
|
||||||
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
|
|
||||||
|
|
||||||
const eModules = importOverleafModules('editorToolbarButtons')
|
|
||||||
const editorToolbarButtons = eModules.map(item => item.import.default)
|
|
||||||
|
|
||||||
export default App.controller('EditorToolbarController', [
|
|
||||||
'$scope',
|
|
||||||
'ide',
|
|
||||||
function ($scope, ide) {
|
|
||||||
const editorButtons = []
|
|
||||||
|
|
||||||
for (const editorToolbarButton of editorToolbarButtons) {
|
|
||||||
const button = editorToolbarButton.button($scope, ide)
|
|
||||||
|
|
||||||
if (editorToolbarButton.source) {
|
|
||||||
editorButtons.push(button)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.editorButtons = editorButtons
|
|
||||||
},
|
|
||||||
])
|
|
|
@ -1,2 +1 @@
|
||||||
import './EditorLoaderController'
|
import './EditorLoaderController'
|
||||||
import './EditorToolbarController'
|
|
||||||
|
|
173
services/web/frontend/js/vendor/libs/sharejs.js
vendored
173
services/web/frontend/js/vendor/libs/sharejs.js
vendored
|
@ -26,7 +26,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
|
||||||
* DS207: Consider shorter variations of null checks
|
* 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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
***************************************/
|
***************************************/
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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} />)
|
||||||
|
|
||||||
|
|
|
@ -1,204 +0,0 @@
|
||||||
import { expect } from 'chai'
|
|
||||||
|
|
||||||
import {
|
|
||||||
matchOutline,
|
|
||||||
nestOutline,
|
|
||||||
} from '../../../../frontend/js/features/outline/outline-parser'
|
|
||||||
|
|
||||||
describe('OutlineParser', function () {
|
|
||||||
describe('matchOutline', function () {
|
|
||||||
it('matches all levels', function () {
|
|
||||||
const content = `
|
|
||||||
\\book{Book}
|
|
||||||
\\part{Part}
|
|
||||||
\\addpart{Part 2}
|
|
||||||
\\chapter{Chapter}
|
|
||||||
\\addchap{Chapter 2}
|
|
||||||
\\section{Section 1}
|
|
||||||
\\addsec{Section 1b}
|
|
||||||
\\subsection{Subsection}
|
|
||||||
\\subsubsection{Subsubsection}
|
|
||||||
\\section{Section 2}
|
|
||||||
\\subsubsection{Subsubsection without subsection}
|
|
||||||
\\paragraph{a paragraph} Here is some text.
|
|
||||||
\\subparagraph{a subparagraph} Here is some more text.
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 2, title: 'Book', level: 10 },
|
|
||||||
{ line: 3, title: 'Part', level: 20 },
|
|
||||||
{ line: 4, title: 'Part 2', level: 20 },
|
|
||||||
{ line: 5, title: 'Chapter', level: 30 },
|
|
||||||
{ line: 6, title: 'Chapter 2', level: 30 },
|
|
||||||
{ line: 7, title: 'Section 1', level: 40 },
|
|
||||||
{ line: 8, title: 'Section 1b', level: 40 },
|
|
||||||
{ line: 9, title: 'Subsection', level: 50 },
|
|
||||||
{ line: 10, title: 'Subsubsection', level: 60 },
|
|
||||||
{ line: 11, title: 'Section 2', level: 40 },
|
|
||||||
{ line: 12, title: 'Subsubsection without subsection', level: 60 },
|
|
||||||
{ line: 13, title: 'a paragraph', level: 70 },
|
|
||||||
{ line: 14, title: 'a subparagraph', level: 80 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches display titles', function () {
|
|
||||||
const content = `
|
|
||||||
\\section{\\label{foo} Label before}
|
|
||||||
\\section{Label after \\label{foo}}
|
|
||||||
\\section{Label \\label{foo} between}
|
|
||||||
\\section{TT \\texttt{Bar}}
|
|
||||||
\\section{plain title}
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 2, title: ' Label before', level: 40 },
|
|
||||||
{ line: 3, title: 'Label after ', level: 40 },
|
|
||||||
{ line: 4, title: 'Label between', level: 40 },
|
|
||||||
{ line: 5, title: 'TT Bar', level: 40 },
|
|
||||||
{ line: 6, title: 'plain title', level: 40 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes spurious commands after title definition', function () {
|
|
||||||
const content = `
|
|
||||||
\\section{Plain title} more text \\href{link}{link}
|
|
||||||
\\section{\\label{foo} Label before} more text \\href{link}{link}
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 2, title: 'Plain title', level: 40 },
|
|
||||||
{ line: 3, title: ' Label before', level: 40 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches empty sections', function () {
|
|
||||||
const outline = matchOutline('\\section{}')
|
|
||||||
expect(outline).to.deep.equal([{ line: 1, title: '', level: 40 }])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches indented sections', function () {
|
|
||||||
const outline = matchOutline('\t\\section{Indented}')
|
|
||||||
expect(outline).to.deep.equal([{ line: 1, title: 'Indented', level: 40 }])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches unnumbered sections', function () {
|
|
||||||
const outline = matchOutline('\\section*{Unnumbered}')
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 1, title: 'Unnumbered', level: 40 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches short titles', function () {
|
|
||||||
const outline = matchOutline(
|
|
||||||
'\\chapter[Short Title For TOC]{Very Long Title for Text}'
|
|
||||||
)
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 1, title: 'Short Title For TOC', level: 30 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('handles spacing', function () {
|
|
||||||
const content = `
|
|
||||||
\\section {Weird Spacing}
|
|
||||||
\\section * {Weird Spacing Unnumbered}
|
|
||||||
\\section [Weird Spacing for TOC] {Weird Spacing}
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([
|
|
||||||
{ line: 2, title: 'Weird Spacing', level: 40 },
|
|
||||||
{ line: 3, title: 'Weird Spacing Unnumbered', level: 40 },
|
|
||||||
{ line: 4, title: 'Weird Spacing for TOC', level: 40 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("doesn't match commented lines", function () {
|
|
||||||
const content = `
|
|
||||||
% \\section{I should not appear in the outline}
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("doesn't match inline sections", function () {
|
|
||||||
const content = `
|
|
||||||
I like to write \\section{inline} on one line.
|
|
||||||
`
|
|
||||||
const outline = matchOutline(content)
|
|
||||||
expect(outline).to.deep.equal([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('nestOutline', function () {
|
|
||||||
it('matches all levels', function () {
|
|
||||||
const flatOutline = [
|
|
||||||
{ line: 10, title: 'Book', level: 10 },
|
|
||||||
{ line: 20, title: 'Part A', level: 20 },
|
|
||||||
{ line: 30, title: 'Section A 1', level: 40 },
|
|
||||||
{ line: 40, title: 'Subsection A 1 1', level: 50 },
|
|
||||||
{ line: 50, title: 'Subsection A 1 2', level: 50 },
|
|
||||||
{ line: 60, title: 'Section A 2', level: 40 },
|
|
||||||
{ line: 70, title: 'Section A 3', level: 40 },
|
|
||||||
{ line: 80, title: 'Subsection A 3 1', level: 50 },
|
|
||||||
{ line: 90, title: 'Chapter', level: 30 },
|
|
||||||
{ line: 100, title: 'Part B', level: 20 },
|
|
||||||
{ line: 110, title: 'Section 2', level: 40 },
|
|
||||||
{ line: 120, title: 'Subsubsection without subsection', level: 60 },
|
|
||||||
]
|
|
||||||
const nestedOutline = nestOutline(flatOutline)
|
|
||||||
expect(nestedOutline).to.deep.equal([
|
|
||||||
{
|
|
||||||
line: 10,
|
|
||||||
title: 'Book',
|
|
||||||
level: 10,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
line: 20,
|
|
||||||
title: 'Part A',
|
|
||||||
level: 20,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
line: 30,
|
|
||||||
title: 'Section A 1',
|
|
||||||
level: 40,
|
|
||||||
children: [
|
|
||||||
{ line: 40, title: 'Subsection A 1 1', level: 50 },
|
|
||||||
{ line: 50, title: 'Subsection A 1 2', level: 50 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ line: 60, title: 'Section A 2', level: 40 },
|
|
||||||
{
|
|
||||||
line: 70,
|
|
||||||
title: 'Section A 3',
|
|
||||||
level: 40,
|
|
||||||
children: [
|
|
||||||
{ line: 80, title: 'Subsection A 3 1', level: 50 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ line: 90, title: 'Chapter', level: 30 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
line: 100,
|
|
||||||
title: 'Part B',
|
|
||||||
level: 20,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
line: 110,
|
|
||||||
title: 'Section 2',
|
|
||||||
level: 40,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
line: 120,
|
|
||||||
title: 'Subsubsection without subsection',
|
|
||||||
level: 60,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -61,18 +61,11 @@ function getModuleDirectory(moduleName) {
|
||||||
|
|
||||||
const mathjaxDir = getModuleDirectory('mathjax')
|
const 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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue