diff --git a/package-lock.json b/package-lock.json index 342495dadc..fb9df4765b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14497,12 +14497,6 @@ "node": ">= 0.6" } }, - "node_modules/ace-builds": { - "version": "1.4.12", - "resolved": "git+ssh://git@github.com/overleaf/ace-builds.git#80aa64e7098fead36c15a3f15c6cc6ca5f0e56b1", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -43692,7 +43686,6 @@ "@uppy/utils": "^4.0.7", "@uppy/xhr-upload": "^1.6.8", "abort-controller": "^3.0.0", - "ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "algoliasearch": "^3.35.1", @@ -52208,7 +52201,6 @@ "@xmldom/xmldom": "^0.7.13", "abort-controller": "^3.0.0", "accepts": "^1.3.7", - "ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "algoliasearch": "^3.35.1", @@ -57685,11 +57677,6 @@ "negotiator": "0.6.3" } }, - "ace-builds": { - "version": "git+ssh://git@github.com/overleaf/ace-builds.git#80aa64e7098fead36c15a3f15c6cc6ca5f0e56b1", - "dev": true, - "from": "ace-builds@overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9" - }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", diff --git a/services/web/.eslintrc b/services/web/.eslintrc index a6e9dafabb..73bcc8de0b 100644 --- a/services/web/.eslintrc +++ b/services/web/.eslintrc @@ -166,7 +166,6 @@ "__webpack_public_path__": true, "$": true, "angular": true, - "ace": true, "ga": true, // Injected in layout.pug "user_id": true, diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index dc745d5222..10efe2daf5 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -799,11 +799,6 @@ const ProjectController = { const detachRole = req.params.detachRole - // Allow override via legacy_source_editor=true in query string - const showLegacySourceEditor = shouldDisplayFeature( - 'legacy_source_editor' - ) - const showSymbolPalette = !Features.hasFeature('saas') || (user.features && user.features.symbolPalette) @@ -896,8 +891,6 @@ const ProjectController = { showTemplatesServerPro, pdfjsVariant: pdfjsAssignment.variant, debugPdfDetach, - showLegacySourceEditor, - showSourceToolbar: !showLegacySourceEditor, showSymbolPalette, symbolPaletteAvailable: Features.hasFeature('symbol-palette'), detachRole, @@ -905,7 +898,6 @@ const ProjectController = { showUpgradePrompt, fixedSizeDocument: true, useOpenTelemetry: Settings.useOpenTelemetryClient, - showCM6SwitchAwaySurvey: Settings.showCM6SwitchAwaySurvey, isReviewPanelReact: reviewPanelAssignment.variant === 'react', idePageReact, showPersonalAccessToken, diff --git a/services/web/app/src/infrastructure/PackageVersions.js b/services/web/app/src/infrastructure/PackageVersions.js index 4ed3d55915..06d4e7dfc3 100644 --- a/services/web/app/src/infrastructure/PackageVersions.js +++ b/services/web/app/src/infrastructure/PackageVersions.js @@ -1,6 +1,5 @@ const version = { // Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace - ace: '1.4.12', mathjax: '2.7.9', 'mathjax-3': '3.2.2', } diff --git a/services/web/app/views/project/editor/editor-pane.pug b/services/web/app/views/project/editor/editor-pane.pug index 8b7352e35f..5cf7e220fb 100644 --- a/services/web/app/views/project/editor/editor-pane.pug +++ b/services/web/app/views/project/editor/editor-pane.pug @@ -21,7 +21,6 @@ include ./file-view .editor-container.full-size( - class={"has-source-toolbar" : showSourceToolbar}, ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0" vertical-resizable-panes="south-pane-resizer" vertical-resizable-panes-hidden-externally-on="south-pane-toggled" @@ -44,14 +43,10 @@ |   #{translate("open_a_file_on_the_left")} div(ng-controller="EditorLoaderController") - if (!showSourceToolbar) - include ./toolbar - div(ng-if="editor.newSourceEditor") - include ../../source-editor/source-editor - div(ng-if="!editor.newSourceEditor") - include ./source-editor - div(ng-if="!editor.newSourceEditor || !reviewPanel.isReact") + include ../../source-editor/source-editor + + div(ng-if="!reviewPanel.isReact") if !isRestrictedTokenMember include ./review-panel diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 8ca4914973..7d69c244a8 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -12,9 +12,6 @@ div.full-size( custom-toggler-msg-when-open=translate("tooltip_hide_pdf") custom-toggler-msg-when-closed=translate("tooltip_show_pdf") ) - if (settings.showCM6SwitchAwaySurvey) - cm6-switch-away-survey() - include ./editor-pane .ui-layout-east diff --git a/services/web/app/views/project/editor/file-tree-react.pug b/services/web/app/views/project/editor/file-tree-react.pug index 5838ab541d..4640f7c3d9 100644 --- a/services/web/app/views/project/editor/file-tree-react.pug +++ b/services/web/app/views/project/editor/file-tree-react.pug @@ -24,15 +24,4 @@ aside.editor-sidebar.full-size( .outline-container( vertical-resizable-bottom - ng-controller="OutlineController" ) - outline-pane( - is-tex-file="isTexFile" - outline="outline" - project-id="project_id" - jump-to-line="jumpToLine" - on-toggle="onToggle" - event-tracking="eventTracking" - highlighted-line="highlightedLine" - show="show" - ) diff --git a/services/web/app/views/project/editor/meta.pug b/services/web/app/views/project/editor/meta.pug index a012125a2f..85e3fe2ce1 100644 --- a/services/web/app/views/project/editor/meta.pug +++ b/services/web/app/views/project/editor/meta.pug @@ -13,16 +13,12 @@ meta(name="ol-wikiEnabled" data-type="boolean" content=settings.proxyLearn) meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl) meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) meta(name="ol-compilesUserContentDomain" content=settings.compilesUserContentDomain) -//- Set base path for Ace scripts loaded on demand/workers and don't use cdn -meta(name="ol-aceBasePath" content="/js/" + lib('ace')) //- enable doc hash checking for all projects //- used in public/js/libs/sharejs.js meta(name="ol-useShareJsHash" data-type="boolean" content=true) meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandshake) meta(name="ol-pdfjsVariant" content=pdfjsVariant) meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach) -meta(name="ol-showLegacySourceEditor", data-type="boolean" content=showLegacySourceEditor) -meta(name="ol-showSourceToolbar", data-type="boolean" content=showSourceToolbar) meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette) meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable) meta(name="ol-detachRole" data-type="string" content=detachRole) @@ -34,7 +30,6 @@ meta(name="ol-showUpgradePrompt" data-type="boolean" content=showUpgradePrompt) meta(name="ol-useOpenTelemetry" data-type="boolean" content=useOpenTelemetry) meta(name="ol-showSupport", data-type="boolean" content=showSupport) meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro) -meta(name="ol-showCM6SwitchAwaySurvey", data-type="boolean" content=showCM6SwitchAwaySurvey) meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken) meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken) meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact) diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 83a7602d38..532ad8da80 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -1,5 +1,5 @@ #review-panel( - ng-class="{ 'rp-collapsed-displaying-entry': reviewPanel.entryHover, 'rp-offset-widgets': editor.showVisual }" + ng-class="{ 'rp-collapsed-displaying-entry': reviewPanel.entryHover, 'rp-offset-widgets': true }" ) .rp-in-editor-widgets a.rp-track-changes-indicator( diff --git a/services/web/app/views/project/editor/source-editor.pug b/services/web/app/views/project/editor/source-editor.pug deleted file mode 100644 index ea5e115bd5..0000000000 --- a/services/web/app/views/project/editor/source-editor.pug +++ /dev/null @@ -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" -) diff --git a/services/web/app/views/project/editor/toolbar.pug b/services/web/app/views/project/editor/toolbar.pug deleted file mode 100644 index 39f1ff39e0..0000000000 --- a/services/web/app/views/project/editor/toolbar.pug +++ /dev/null @@ -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}} diff --git a/services/web/frontend/js/features/dictionary/ignored-words.ts b/services/web/frontend/js/features/dictionary/ignored-words.ts index b720822a9d..d7b141656f 100644 --- a/services/web/frontend/js/features/dictionary/ignored-words.ts +++ b/services/web/frontend/js/features/dictionary/ignored-words.ts @@ -1,5 +1,27 @@ import getMeta from '../../utils/meta' -import { IGNORED_MISSPELLINGS } from '../../ide/editor/directives/aceEditor/spell-check/IgnoredMisspellings' + +const IGNORED_MISSPELLINGS = [ + 'Overleaf', + 'overleaf', + 'ShareLaTeX', + 'sharelatex', + 'LaTeX', + 'TeX', + 'BibTeX', + 'BibLaTeX', + 'XeTeX', + 'XeLaTeX', + 'LuaTeX', + 'LuaLaTeX', + 'http', + 'https', + 'www', + 'COVID', + 'Lockdown', + 'lockdown', + 'Coronavirus', + 'coronavirus', +] export class IgnoredWords { public learnedWords!: Set diff --git a/services/web/frontend/js/features/editor-left-menu/components/help-show-hotkeys.tsx b/services/web/frontend/js/features/editor-left-menu/components/help-show-hotkeys.tsx index 1930c17599..686547342f 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/help-show-hotkeys.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/help-show-hotkeys.tsx @@ -3,12 +3,10 @@ import { useTranslation } from 'react-i18next' import * as eventTracking from '../../../infrastructure/event-tracking' import { useProjectContext } from '../../../shared/context/project-context' import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal' -import useScopeValue from '../../../shared/hooks/use-scope-value' import LeftMenuButton from './left-menu-button' export default function HelpShowHotkeys() { const [showModal, setShowModal] = useState(false) - const [newSourceEditor] = useScopeValue('editor.newSourceEditor') const { t } = useTranslation() const { features } = useProjectContext() const isMac = /Mac/.test(window.navigator?.platform) @@ -34,7 +32,6 @@ export default function HelpShowHotkeys() { handleHide={() => setShowModal(false)} isMac={isMac} trackChangesVisible={features?.trackChangesVisible} - newSourceEditor={newSourceEditor} /> ) diff --git a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.jsx b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.jsx index ebb212e269..fe20c47703 100644 --- a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.jsx +++ b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.jsx @@ -10,17 +10,11 @@ export default function HotkeysModal({ show, isMac = false, trackChangesVisible = false, - newSourceEditor = false, }) { const { t } = useTranslation() - const goToLineSuffix = newSourceEditor ? 'Shift + L' : 'L' const ctrl = isMac ? 'Cmd' : 'Ctrl' - const modalTitle = newSourceEditor - ? `${t('hotkeys')} (Source editor)` - : `${t('hotkeys')} (Legacy source editor)` - return ( - {modalTitle} + {t('hotkeys')} (Source editor) @@ -77,7 +71,7 @@ export default function HotkeysModal({ @@ -211,7 +205,6 @@ HotkeysModal.propTypes = { show: PropTypes.bool.isRequired, handleHide: PropTypes.func.isRequired, trackChangesVisible: PropTypes.bool, - newSourceEditor: PropTypes.bool, } function Hotkey({ combination, description }) { diff --git a/services/web/frontend/js/features/outline/controllers/outline-controller.js b/services/web/frontend/js/features/outline/controllers/outline-controller.js deleted file mode 100644 index 62728f3a35..0000000000 --- a/services/web/frontend/js/features/outline/controllers/outline-controller.js +++ /dev/null @@ -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', - ]) -) diff --git a/services/web/frontend/js/features/outline/outline-manager.js b/services/web/frontend/js/features/outline/outline-manager.js deleted file mode 100644 index b49299718f..0000000000 --- a/services/web/frontend/js/features/outline/outline-manager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/features/outline/outline-parser.js b/services/web/frontend/js/features/outline/outline-parser.js deleted file mode 100644 index c2ca223ca6..0000000000 --- a/services/web/frontend/js/features/outline/outline-parser.js +++ /dev/null @@ -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 } diff --git a/services/web/frontend/js/features/source-editor/components/cm6-switch-away-survey.tsx b/services/web/frontend/js/features/source-editor/components/cm6-switch-away-survey.tsx deleted file mode 100644 index 009ac5193e..0000000000 --- a/services/web/frontend/js/features/source-editor/components/cm6-switch-away-survey.tsx +++ /dev/null @@ -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('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 ( -
- -
-
-
- We noticed that you're still using the{' '} - Source (legacy) editor. -
-
Could you let us know why?
-
- -
-
- ) -} diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx index 6fcafa912f..d267c7bfa2 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx @@ -17,7 +17,6 @@ import EditorSwitch from './editor-switch' import SwitchToPDFButton from './switch-to-pdf-button' import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control' import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper' -import getMeta from '../../../utils/meta' import { isVisual } from '../extensions/visual/visual' import { language } from '@codemirror/language' import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors' @@ -34,8 +33,6 @@ export const CodeMirrorToolbar = () => { } const Toolbar = memo(function Toolbar() { - const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar') - const state = useCodeMirrorStateContext() const view = useCodeMirrorViewContext() @@ -117,7 +114,7 @@ const Toolbar = memo(function Toolbar() { return (
- {showSourceToolbar && } + {showActions && ( )} +
{showActions && ( )} +
+
- {showSourceToolbar && ( - <> - - - - - )} + + + +
- Overleaf has upgraded the source editor. -
- You can still use the old editor by selecting "Source (legacy)". -
-
- Click to learn more and give feedback - - ) - - return ( - - - {content} - - - ) -} - -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 ( -
- {showLegacySourceEditor ? : null} - -
- Editor mode. - - - - - {showLegacySourceEditor ? ( - <> - - - - ) : null} - - -
-
- ) -} - -const RichTextToggle: FC<{ - checked: boolean - disabled: boolean - handleChange: (event: ChangeEvent) => void -}> = ({ checked, disabled, handleChange }) => { - const { t } = useTranslation() - - const toggle = ( - - - - - ) - - if (disabled) { - return ( - - {toggle} - - ) - } - - return toggle -} - -export default memo(EditorSwitch) diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx index b6e7896580..f4d38b4209 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx @@ -9,40 +9,28 @@ import { FeedbackBadge } from '@/shared/components/feedback-badge' function EditorSwitch() { const { t } = useTranslation() - const [newSourceEditor, setNewSourceEditor] = useScopeValue( - 'editor.newSourceEditor' - ) const [visual, setVisual] = useScopeValue('editor.showVisual') - const [docName] = useScopeValue('editor.open_doc_name') + const richTextAvailable = isValidTeXFile(docName) - // TODO: rename this after legacy & toolbar split tests are complete - const richTextOrVisual = richTextAvailable && visual const handleChange = useCallback( event => { const editorType = event.target.value switch (editorType) { - case 'ace': - setVisual(false) - setNewSourceEditor(false) - break - case 'cm6': setVisual(false) - setNewSourceEditor(true) break case 'rich-text': setVisual(true) - setNewSourceEditor(true) break } sendMB('editor-switch-change', { editorType }) }, - [setVisual, setNewSourceEditor] + [setVisual] ) return ( @@ -56,7 +44,7 @@ function EditorSwitch() { value="cm6" id="editor-switch-cm6" className="toggle-switch-input" - checked={!richTextOrVisual && !!newSourceEditor} + checked={!richTextAvailable || !visual} onChange={handleChange} /> - {!!richTextOrVisual && ( + {richTextAvailable && visual && ( { - 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 ( -
- -
-
We're retiring our Source (legacy) editor in late May 2023.
-
- - Read the blog post - {' '} - to learn more and find out how to report any problems. -
-
-
- ) -}) diff --git a/services/web/frontend/js/features/source-editor/controllers/cm6-switch-away-survey-controller.js b/services/web/frontend/js/features/source-editor/controllers/cm6-switch-away-survey-controller.js deleted file mode 100644 index c4881f503d..0000000000 --- a/services/web/frontend/js/features/source-editor/controllers/cm6-switch-away-survey-controller.js +++ /dev/null @@ -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)) -) diff --git a/services/web/frontend/js/features/source-editor/controllers/editor-switch-controller.js b/services/web/frontend/js/features/source-editor/controllers/editor-switch-controller.js deleted file mode 100644 index 9964a9f425..0000000000 --- a/services/web/frontend/js/features/source-editor/controllers/editor-switch-controller.js +++ /dev/null @@ -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))) diff --git a/services/web/frontend/js/features/source-editor/controllers/legacy-editor-warning-controller.js b/services/web/frontend/js/features/source-editor/controllers/legacy-editor-warning-controller.js deleted file mode 100644 index 2da0e248bd..0000000000 --- a/services/web/frontend/js/features/source-editor/controllers/legacy-editor-warning-controller.js +++ /dev/null @@ -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']) -) diff --git a/services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts b/services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts index 526d8c7bd0..c073676c54 100644 --- a/services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts +++ b/services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts @@ -15,8 +15,7 @@ import getMeta from '../../../../utils/meta' // If a toolbar row sits alongside the review panel, the review panel entries need to be shifted down by 32px. // Once the review panel is always inside the editor, this offset can be removed. -const offsetTop = - getMeta('ol-showSourceToolbar') && !getMeta('ol-isReviewPanelReact') ? 32 : 0 +const offsetTop = getMeta('ol-isReviewPanelReact') ? 0 : 32 // With less than this number of entries, don't bother culling to avoid // little UI jumps when scrolling. diff --git a/services/web/frontend/js/features/source-editor/utils/switch-away-survey.ts b/services/web/frontend/js/features/source-editor/utils/switch-away-survey.ts deleted file mode 100644 index d73e3f6a17..0000000000 --- a/services/web/frontend/js/features/source-editor/utils/switch-away-survey.ts +++ /dev/null @@ -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) -} diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js index 9ff1375116..9c5199c128 100644 --- a/services/web/frontend/js/ide.js +++ b/services/web/frontend/js/ide.js @@ -28,8 +28,6 @@ import BinaryFilesManager from './ide/binary-files/BinaryFilesManager' import ReferencesManager from './ide/references/ReferencesManager' import MetadataManager from './ide/metadata/MetadataManager' import './ide/review-panel/ReviewPanelManager' -import OutlineManager from './features/outline/outline-manager' -import SafariScrollPatcher from './ide/SafariScrollPatcher' import './ide/cobranding/CobrandingDataService' import './ide/chat/index' import './ide/file-view/index' @@ -59,14 +57,10 @@ import './shared/context/controllers/root-context-controller' import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller' import './features/pdf-preview/controllers/pdf-preview-controller' import './features/share-project-modal/controllers/react-share-project-modal-controller' -import './features/source-editor/controllers/editor-switch-controller' -import './features/source-editor/controllers/cm6-switch-away-survey-controller' -import './features/source-editor/controllers/legacy-editor-warning-controller' import './features/history/controllers/history-controller' import './features/editor-left-menu/controllers/editor-left-menu-controller' import { cleanupServiceWorker } from './utils/service-worker-cleanup' import { reportCM6Perf } from './infrastructure/cm6-performance' -import { reportAcePerf } from './ide/editor/ace-performance' import { debugConsole } from '@/utils/debugging' App.controller('IdeController', [ @@ -215,7 +209,6 @@ App.controller('IdeController', [ ide.permissionsManager = new PermissionsManager(ide, $scope) ide.binaryFilesManager = new BinaryFilesManager(ide, $scope) ide.metadataManager = new MetadataManager(ide, $scope, metadata) - ide.outlineManager = new OutlineManager(ide, $scope) let inited = false $scope.$on('project:joined', function () { @@ -301,32 +294,6 @@ If the project has been renamed please look in your project list for a new proje } } } - } else if (editorType === 'ace') { - const acePerfData = reportAcePerf() - - if (acePerfData.numberOfEntries > 0) { - const perfProps = [ - 'NumberOfEntries', - 'MeanKeypressPaint', - 'Grammarly', - 'SessionLength', - 'Memory', - 'Lags', - 'NonLags', - 'LongestLag', - 'MeanLagsPerMeasure', - 'MeanKeypressesPerMeasure', - 'Release', - ] - - for (const prop of perfProps) { - const perfValue = - acePerfData[prop.charAt(0).toLowerCase() + prop.slice(1)] - if (perfValue !== null) { - segmentation['acePerf' + prop] = perfValue - } - } - } } return segmentation @@ -374,8 +341,6 @@ If the project has been renamed please look in your project list for a new proje ide.localStorage = localStorage - ide.browserIsSafari = false - $scope.switchToFlatLayout = function (view) { $scope.ui.pdfLayout = 'flat' $scope.ui.view = view @@ -418,39 +383,6 @@ If the project has been renamed please look in your project list for a new proje // unused? } - try { - ;({ userAgent } = navigator) - ide.browserIsSafari = - userAgent && - /.*Safari\/.*/.test(userAgent) && - !/.*Chrome\/.*/.test(userAgent) && - !/.*Chromium\/.*/.test(userAgent) - } catch (error) { - err = error - debugConsole.error(err) - } - - if (ide.browserIsSafari) { - ide.safariScrollPatcher = new SafariScrollPatcher($scope) - } - - // Fix Chrome 61 and 62 text-shadow rendering - let browserIsChrome61or62 = false - try { - const chromeVersion = - parseFloat(navigator.userAgent.split(' Chrome/')[1]) || null - browserIsChrome61or62 = chromeVersion != null - if (browserIsChrome61or62) { - document.styleSheets[0].insertRule( - '.ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; font-weight: bold; }', - 1 - ) - } - } catch (error1) { - err = error1 - debugConsole.error(err) - } - // User can append ?ft=somefeature to url to activate a feature toggle ide.featureToggle = __guard__( __guard__( diff --git a/services/web/frontend/js/ide/SafariScrollPatcher.js b/services/web/frontend/js/ide/SafariScrollPatcher.js deleted file mode 100644 index b75d990872..0000000000 --- a/services/web/frontend/js/ide/SafariScrollPatcher.js +++ /dev/null @@ -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() - }) - } -} diff --git a/services/web/frontend/js/ide/editor/Document.js b/services/web/frontend/js/ide/editor/Document.js index 07640c62ce..1d8e4a5044 100644 --- a/services/web/frontend/js/ide/editor/Document.js +++ b/services/web/frontend/js/ide/editor/Document.js @@ -85,46 +85,19 @@ export default Document = (function () { this.connected = this.ide.socket.socket.connected this.joined = false this.wantToBeJoined = false - this._checkAceConsistency = () => this._checkConsistency(this.ace) this._checkCM6Consistency = () => this._checkConsistency(this.cm6) this._bindToEditorEvents() this._bindToSocketEvents() } editorType() { - if (this.ace) { - return 'ace' - } else if (this.cm6) { + if (this.cm6) { return 'cm6' } else { return null } } - attachToAce(ace) { - this.ace = ace - if (this.doc != null) { - this.doc.attachToAce(this.ace) - } - const editorDoc = this.ace.getSession().getDocument() - editorDoc.on('change', this._checkAceConsistency) - return this.ide.$scope.$emit('document:opened', this.doc) - } - - detachFromAce() { - if (this.doc != null) { - this.doc.detachFromAce() - } - const editorDoc = - this.ace != null ? this.ace.getSession().getDocument() : undefined - if (editorDoc != null) { - editorDoc.off('change', this._checkAceConsistency) - } - delete this.ace - this.clearChaosMonkey() - return this.ide.$scope.$emit('document:closed', this.doc) - } - attachToCM6(cm6) { this.cm6 = cm6 if (this.doc != null) { @@ -328,9 +301,7 @@ export default Document = (function () { } char = copy[0] copy = copy.slice(1) - if (this.ace) { - this.ace.session.insert({ row: line, column: pos }, char) - } else if (this.cm6) { + if (this.cm6) { this.cm6.view.dispatch({ changes: { from: Math.min(pos, this.cm6.view.state.doc.length), @@ -757,7 +728,7 @@ export default Document = (function () { this.ranges.setIdSeed(old_id_seed) } if (remote_op) { - // With remote ops, Ace hasn't been updated when we receive this op, + // With remote ops, the editor hasn't been updated when we receive this op, // so defer updating track changes until it has return setTimeout(() => this.emit('ranges:dirty')) } else { diff --git a/services/web/frontend/js/ide/editor/EditorManager.js b/services/web/frontend/js/ide/editor/EditorManager.js index b96208b247..0f5f6c4229 100644 --- a/services/web/frontend/js/ide/editor/EditorManager.js +++ b/services/web/frontend/js/ide/editor/EditorManager.js @@ -15,15 +15,12 @@ import _ from 'lodash' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import Document from './Document' -import './components/spellMenu' -import './directives/aceEditor' import './directives/formattingButtons' import './directives/toggleSwitch' import './controllers/SavingNotificationController' import './controllers/CompileButton' import './controllers/SwitchToPDFButton' -import getMeta from '../../utils/meta' -import { hasSeenCM6SwitchAwaySurvey } from '../../features/source-editor/utils/switch-away-survey' +import '../metadata/services/metadata' import { debugConsole } from '@/utils/debugging' let EditorManager @@ -48,7 +45,6 @@ export default EditorManager = (function () { wantTrackChanges: false, docTooLongErrorShown: false, showVisual: this.showVisual(), - newSourceEditor: this.newSourceEditor(), showSymbolPalette: false, toggleSymbolPalette: () => { const newValue = !this.$scope.editor.showSymbolPalette @@ -171,35 +167,6 @@ export default EditorManager = (function () { ) } - newSourceEditor() { - // Use the new source editor if the legacy editor is disabled - if (!getMeta('ol-showLegacySourceEditor')) { - return true - } - - const storedPrefIsCM6 = () => { - const sourceEditor = this.localStorage( - `editor.source_editor.${this.$scope.project_id}` - ) - - return sourceEditor === 'cm6' || sourceEditor == null - } - - const showCM6SwitchAwaySurvey = getMeta('ol-showCM6SwitchAwaySurvey') - - if (!showCM6SwitchAwaySurvey) { - return storedPrefIsCM6() - } - - if (hasSeenCM6SwitchAwaySurvey()) { - return storedPrefIsCM6() - } else { - // force user to switch to cm6 if they haven't seen either of the - // switch-away surveys - return true - } - } - autoOpenDoc() { const open_doc_id = this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) || diff --git a/services/web/frontend/js/ide/editor/ShareJsDoc.js b/services/web/frontend/js/ide/editor/ShareJsDoc.js index 5f44448ee7..9e47980dc4 100644 --- a/services/web/frontend/js/ide/editor/ShareJsDoc.js +++ b/services/web/frontend/js/ide/editor/ShareJsDoc.js @@ -357,19 +357,6 @@ export default ShareJsDoc = (function () { } } - attachToAce(ace) { - this._attachToEditor('Ace', ace, () => { - this._doc.attach_ace(ace, window.maxDocLength) - }) - } - - detachFromAce() { - this._maybeDetachEditorWatchdogManager() - return typeof this._doc.detach_ace === 'function' - ? this._doc.detach_ace() - : undefined - } - attachToCM6(cm6) { this._attachToEditor('CM6', cm6, () => { cm6.attachShareJs(this._doc, window.maxDocLength) diff --git a/services/web/frontend/js/ide/editor/ace-performance.ts b/services/web/frontend/js/ide/editor/ace-performance.ts deleted file mode 100644 index 49c29df49c..0000000000 --- a/services/web/frontend/js/ide/editor/ace-performance.ts +++ /dev/null @@ -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()) -} diff --git a/services/web/frontend/js/ide/editor/components/spellMenu.js b/services/web/frontend/js/ide/editor/components/spellMenu.js deleted file mode 100644 index a70108e25b..0000000000 --- a/services/web/frontend/js/ide/editor/components/spellMenu.js +++ /dev/null @@ -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: `\ -
- -
\ -`, -}) diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor.js b/services/web/frontend/js/ide/editor/directives/aceEditor.js deleted file mode 100644 index 32503697ad..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor.js +++ /dev/null @@ -1,1012 +0,0 @@ -import _ from 'lodash' -/* eslint-disable - camelcase, - max-len - */ -import App from '../../../base' -import UndoManager from './aceEditor/undo/UndoManager' -import AutoCompleteManager from './aceEditor/auto-complete/AutoCompleteManager' -import SpellCheckManager from './aceEditor/spell-check/SpellCheckManager' -import SpellCheckAdapter from './aceEditor/spell-check/SpellCheckAdapter' -import HighlightsManager from './aceEditor/highlights/HighlightsManager' -import CursorPositionManager from './aceEditor/cursor-position/CursorPositionManager' -import CursorPositionAdapter from './aceEditor/cursor-position/CursorPositionAdapter' -import TrackChangesManager from './aceEditor/track-changes/TrackChangesManager' -import TrackChangesAdapter from './aceEditor/track-changes/TrackChangesAdapter' -import MetadataManager from './aceEditor/metadata/MetadataManager' -import 'ace/ace' -import 'ace/ext-searchbox' -import 'ace/ext-modelist' -import 'ace/keybinding-vim' -import '../../metadata/services/metadata' -import '../../graphics/services/graphics' -import '../../preamble/services/preamble' -import '../../files/services/files' -import { - initAcePerfListener, - tearDownAcePerfListener, -} from '../ace-performance' -let syntaxValidationEnabled -const { EditSession } = ace.require('ace/edit_session') -const ModeList = ace.require('ace/ext/modelist') -const { Vim } = ace.require('ace/keyboard/vim') -const SearchBox = ace.require('ace/ext/searchbox') - -// Set the base path that ace will fetch modes/snippets/workers from -if (window.aceBasePath !== '') { - syntaxValidationEnabled = true - ace.config.set('basePath', window.aceBasePath) - ace.config.set('workerPath', window.aceBasePath) -} else { - syntaxValidationEnabled = false -} - -// By default, don't use workers - enable them per-session as required -ace.config.setDefaultValue('session', 'useWorker', false) - -// Ace loads its script itself, so we need to hook in to be able to clear -// the cache. -if (ace.config._moduleUrl == null) { - ace.config._moduleUrl = ace.config.moduleUrl - ace.config.moduleUrl = function (...args) { - const url = ace.config._moduleUrl(...Array.from(args || [])) - return url - } -} - -App.directive('aceEditor', [ - 'ide', - '$compile', - '$rootScope', - 'eventTracking', - 'localStorage', - '$cacheFactory', - 'metadata', - 'graphics', - 'preamble', - 'files', - '$http', - '$q', - function ( - ide, - $compile, - $rootScope, - eventTracking, - localStorage, - $cacheFactory, - metadata, - graphics, - preamble, - files, - $http, - $q - ) { - monkeyPatchSearch($rootScope, $compile) - - return { - scope: { - theme: '=', - showPrintMargin: '=', - keybindings: '=', - fontSize: '=', - autoComplete: '=', - autoPairDelimiters: '=', - sharejsDoc: '=', - spellCheck: '=', - spellCheckLanguage: '=', - highlights: '=', - text: '=', - readOnly: '=', - annotations: '=', - navigateHighlights: '=', - fileName: '=', - onCtrlEnter: '=', // Compile - onCtrlJ: '=', // Toggle the review panel - onCtrlShiftC: '=', // Add a new comment - onCtrlShiftA: '=', // Toggle track-changes on/off - onSave: '=', // Cmd/Ctrl-S or :w in Vim - syntaxValidation: '=', - reviewPanel: '=', - eventsBridge: '=', - trackChanges: '=', - docId: '=', - rendererData: '=', - lineHeight: '=', - fontFamily: '=', - }, - link(scope, element, attrs) { - // Don't freak out if we're already in an apply callback - let spellCheckManager - scope.$originalApply = scope.$apply - scope.$apply = function (fn) { - if (fn == null) { - fn = function () {} - } - const phase = this.$root.$$phase - if (phase === '$apply' || phase === '$digest') { - return fn() - } else { - return this.$originalApply(fn) - } - } - - const editor = ace.edit(element.find('.ace-editor-body')[0]) - editor.$blockScrolling = Infinity - - // Besides the main editor, other elements will re-use this directive - // for displaying read-only content -- e.g. the history panes. - const editorAcceptsChanges = attrs.aceEditor === 'editor' - if (editorAcceptsChanges) { - // end-to-end check for edits -> acks, globally on any doc - // This may catch a missing attached ShareJsDoc that in turn bails out - // on missing acks. - ide.globalEditorWatchdogManager.attachToEditor('Ace', editor) - } - - // auto-insertion of braces, brackets, dollars - editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false) - editor.setOption('wrapBehavioursEnabled', false) - - ide.$scope.$on('editor:replace-selection', (event, text) => { - editor.focus() - const document = editor.session.getDocument() - const ranges = editor.selection.getAllRanges() - for (const range of ranges) { - document.replace(range, text) - } - }) - - ide.$scope.$on('symbol-palette-toggled', (event, isToggled) => { - if (!isToggled) { - editor.focus() - } - }) - - scope.$watch('autoPairDelimiters', autoPairDelimiters => { - if (autoPairDelimiters) { - return editor.setOption('behavioursEnabled', true) - } else { - return editor.setOption('behavioursEnabled', false) - } - }) - - if (!window._debug_editors) { - window._debug_editors = [] - } - window._debug_editors.push(editor) - - scope.name = attrs.aceEditor - - if (scope.spellCheck) { - // only enable spellcheck when explicitly required - spellCheckManager = new SpellCheckManager( - scope, - $cacheFactory, - $http, - $q, - new SpellCheckAdapter(editor) - ) - } - - /* eslint-disable no-unused-vars */ - const undoManager = new UndoManager(editor) - const highlightsManager = new HighlightsManager(scope, editor, element) - const cursorPositionManager = new CursorPositionManager( - scope, - new CursorPositionAdapter(editor), - localStorage - ) - const trackChangesManager = new TrackChangesManager( - scope, - editor, - element, - new TrackChangesAdapter(editor) - ) - - const metadataManager = new MetadataManager( - scope, - editor, - element, - metadata - ) - const autoCompleteManager = new AutoCompleteManager( - scope, - editor, - element, - metadataManager, - graphics, - preamble, - files - ) - - // prevent user entering null and non-BMP unicode characters in Ace - const BAD_CHARS_REGEXP = /[\0\uD800-\uDFFF]/g - const BAD_CHARS_REPLACEMENT_CHAR = '\uFFFD' - // the 'exec' event fires for ace functions before they are executed. - // you can modify the input or reject the event with e.preventDefault() - editor.commands.on('exec', function (e) { - // replace bad characters in paste content - if (e.command && e.command.name === 'paste') { - BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage - if (e.args && BAD_CHARS_REGEXP.test(e.args.text)) { - e.args.text = e.args.text.replace( - BAD_CHARS_REGEXP, - BAD_CHARS_REPLACEMENT_CHAR - ) - } - } - // replace bad characters in keyboard input - if (e.command && e.command.name === 'insertstring') { - BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage - if (e.args && BAD_CHARS_REGEXP.test(e.args)) { - e.args = e.args.replace( - BAD_CHARS_REGEXP, - BAD_CHARS_REPLACEMENT_CHAR - ) - } - } - }) - - /* eslint-enable no-unused-vars */ - - Vim.defineEx('write', 'w', function () { - window.dispatchEvent(new Event('pdf:recompile')) - }) - editor.commands.removeCommand('transposeletters') - editor.commands.removeCommand('showSettingsMenu') - editor.commands.removeCommand('foldall') - - // For European keyboards, the / is above 7 so needs Shift pressing. - // This comes through as Command-Shift-/ on OS X, which is mapped to - // toggleBlockComment. - // This doesn't do anything for LaTeX, so remap this to togglecomment to - // work for European keyboards as normal. - // On Windows, the key combo comes as Ctrl-Shift-7. - editor.commands.removeCommand('toggleBlockComment') - editor.commands.removeCommand('togglecomment') - - editor.commands.addCommand({ - name: 'togglecomment', - bindKey: { - win: 'Ctrl-/|Ctrl-Shift-7', - mac: 'Command-/|Command-Shift-/', - }, - exec(editor) { - return editor.toggleCommentLines() - }, - multiSelectAction: 'forEachLine', - scrollIntoView: 'selectionPart', - }) - - // Trigger search AND replace on CMD+F - editor.commands.addCommand({ - name: 'find', - bindKey: { - win: 'Ctrl-F', - mac: 'Command-F', - }, - exec(editor) { - return SearchBox.Search(editor, true) - }, - readOnly: true, - }) - - // Bold text on CMD+B - editor.commands.addCommand({ - name: 'bold', - bindKey: { - win: 'Ctrl-B', - mac: 'Command-B', - }, - exec(editor) { - const selection = editor.getSelection() - if (selection.isEmpty()) { - editor.insert('\\textbf{}') - return editor.navigateLeft(1) - } else { - const text = editor.getCopyText() - return editor.insert(`\\textbf{${text}}`) - } - }, - readOnly: false, - }) - - // Italicise text on CMD+I - editor.commands.addCommand({ - name: 'italics', - bindKey: { - win: 'Ctrl-I', - mac: 'Command-I', - }, - exec(editor) { - const selection = editor.getSelection() - if (selection.isEmpty()) { - editor.insert('\\textit{}') - return editor.navigateLeft(1) - } else { - const text = editor.getCopyText() - return editor.insert(`\\textit{${text}}`) - } - }, - readOnly: false, - }) - - scope.$watch('onCtrlEnter', function (callback) { - if (callback != null) { - return editor.commands.addCommand({ - name: 'compile', - bindKey: { - win: 'Ctrl-Enter', - mac: 'Command-Enter', - }, - exec: editor => { - return callback() - }, - readOnly: true, - }) - } - }) - - scope.$watch('onCtrlJ', function (callback) { - if (callback != null) { - return editor.commands.addCommand({ - name: 'toggle-review-panel', - bindKey: { - win: 'Ctrl-J', - mac: 'Command-J', - }, - exec: editor => { - return callback() - }, - readOnly: true, - }) - } - }) - - scope.$watch('onCtrlShiftC', function (callback) { - if (callback != null) { - return editor.commands.addCommand({ - name: 'add-new-comment', - bindKey: { - win: 'Ctrl-Shift-C', - mac: 'Command-Shift-C', - }, - exec: editor => { - return callback() - }, - readOnly: true, - }) - } - }) - - scope.$watch('onCtrlShiftA', function (callback) { - if (callback != null) { - return editor.commands.addCommand({ - name: 'toggle-track-changes', - bindKey: { - win: 'Ctrl-Shift-A', - mac: 'Command-Shift-A', - }, - exec: editor => { - return callback() - }, - readOnly: true, - }) - } - }) - - // Make '/' work for search in vim mode. - editor.showCommandLine = arg => { - if (arg === '/') { - return SearchBox.Search(editor, true) - } - } - - const getCursorScreenPosition = function () { - const session = editor.getSession() - const cursorPosition = session.selection.getCursor() - const sessionPos = session.documentToScreenPosition( - cursorPosition.row, - cursorPosition.column - ) - return ( - sessionPos.row * editor.renderer.lineHeight - session.getScrollTop() - ) - } - - if (attrs.resizeOn != null) { - for (const event of Array.from(attrs.resizeOn.split(','))) { - scope.$on(event, function () { - scope.$applyAsync(() => { - const previousScreenPosition = getCursorScreenPosition() - editor.resize() - // Put cursor back to same vertical position on screen - const newScreenPosition = getCursorScreenPosition() - const session = editor.getSession() - return session.setScrollTop( - session.getScrollTop() + - newScreenPosition - - previousScreenPosition - ) - }) - }) - } - } - - scope.$on(`${scope.name}:set-scroll-size`, function (e, size) { - // Make sure that the editor has enough scroll margin above and below - // to scroll the review panel with the given size - const marginTop = size.overflowTop - const { maxHeight } = editor.renderer.layerConfig - const marginBottom = Math.max(size.height - maxHeight, 0) - return setScrollMargins(marginTop, marginBottom) - }) - - function setScrollMargins(marginTop, marginBottom) { - let marginChanged = false - if (editor.renderer.scrollMargin.top !== marginTop) { - editor.renderer.scrollMargin.top = marginTop - marginChanged = true - } - if (editor.renderer.scrollMargin.bottom !== marginBottom) { - editor.renderer.scrollMargin.bottom = marginBottom - marginChanged = true - } - if (marginChanged) { - return editor.renderer.updateFull() - } - } - - const resetScrollMargins = () => setScrollMargins(0, 0) - - scope.$watch('theme', value => editor.setTheme(`ace/theme/${value}`)) - - scope.$watch('showPrintMargin', value => - editor.setShowPrintMargin(value) - ) - - scope.$watch('keybindings', function (value) { - if (['vim', 'emacs'].includes(value)) { - return editor.setKeyboardHandler(`ace/keyboard/${value}`) - } else { - return editor.setKeyboardHandler(null) - } - }) - - scope.$watch('fontSize', value => - element.find('.ace_editor, .ace_content').css({ - 'font-size': value + 'px', - }) - ) - - scope.$watch('fontFamily', function (value) { - const monospaceFamilies = [ - 'Monaco', - 'Menlo', - 'Ubuntu Mono', - 'Consolas', - 'monospace', - ] - - if (value != null) { - switch (value) { - case 'monaco': - return editor.setOption( - 'fontFamily', - monospaceFamilies.join(', ') - ) - case 'lucida': - return editor.setOption( - 'fontFamily', - '"Lucida Console", "Source Code Pro", monospace' - ) - default: - return editor.setOption('fontFamily', null) - } - } - }) - - scope.$watch('lineHeight', function (value) { - if (value != null) { - switch (value) { - case 'compact': - editor.container.style.lineHeight = 1.33 - break - case 'normal': - editor.container.style.lineHeight = 1.6 - break - case 'wide': - editor.container.style.lineHeight = 2 - break - default: - editor.container.style.lineHeight = 1.6 - } - return editor.renderer.updateFontSize() - } - }) - - scope.$watch('sharejsDoc', function (sharejs_doc, old_sharejs_doc) { - if (old_sharejs_doc != null) { - scope.$broadcast('beforeChangeDocument') - detachFromAce(old_sharejs_doc) - } - if (sharejs_doc != null) { - attachToAce(sharejs_doc) - } - if (sharejs_doc != null && old_sharejs_doc != null) { - return scope.$broadcast('afterChangeDocument') - } - }) - - scope.$watch('text', function (text) { - if (text != null) { - editor.setValue(text, -1) - const session = editor.getSession() - return session.setUseWrapMode(true) - } - }) - - scope.$watch('annotations', function (annotations) { - const session = editor.getSession() - return session.setAnnotations(annotations) - }) - - scope.$watch('readOnly', value => editor.setReadOnly(!!value)) - - scope.$watch('syntaxValidation', function (value) { - // ignore undefined settings here - // only instances of ace with an explicit value should set useWorker - // the history instance will have syntaxValidation undefined - if (value != null && syntaxValidationEnabled) { - const session = editor.getSession() - return session.setOption('useWorker', value) - } - }) - - editor.setOption('scrollPastEnd', true) - - let updateCount = 0 - const onChange = function () { - updateCount++ - - if (updateCount === 100) { - eventTracking.send('editor-interaction', 'multi-doc-update') - } - return scope.$emit(`${scope.name}:change`) - } - - let currentFirstVisibleRow = null - const emitMiddleVisibleRowChanged = () => { - const firstVisibleRow = editor.getFirstVisibleRow() - if (firstVisibleRow === currentFirstVisibleRow) return - - currentFirstVisibleRow = firstVisibleRow - const lastVisibleRow = editor.getLastVisibleRow() - scope.$emit( - `scroll:editor:update`, - Math.floor((firstVisibleRow + lastVisibleRow) / 2) - ) - } - - const onScroll = function (scrollTop) { - if (scope.eventsBridge == null) { - return - } - const height = editor.renderer.layerConfig.maxHeight - emitMiddleVisibleRowChanged() - return scope.eventsBridge.emit('aceScroll', scrollTop, height) - } - - const onScrollbarVisibilityChanged = function (event, vRenderer) { - if (scope.eventsBridge == null) { - return - } - return scope.eventsBridge.emit( - 'aceScrollbarVisibilityChanged', - vRenderer.scrollBarV.isVisible, - vRenderer.scrollBarV.width - ) - } - - if (scope.eventsBridge != null) { - editor.renderer.on( - 'scrollbarVisibilityChanged', - onScrollbarVisibilityChanged - ) - - scope.eventsBridge.on('externalScroll', position => - editor.getSession().setScrollTop(position) - ) - scope.eventsBridge.on('refreshScrollPosition', function () { - const session = editor.getSession() - session.setScrollTop(session.getScrollTop() + 1) - return session.setScrollTop(session.getScrollTop() - 1) - }) - } - - const onSessionChangeForSpellCheck = function (e) { - spellCheckManager.onSessionChange() - if (e.oldSession != null) { - e.oldSession.getDocument().off('change', spellCheckManager.onChange) - } - e.session.getDocument().on('change', spellCheckManager.onChange) - if (e.oldSession != null) { - e.oldSession.off('changeScrollTop', spellCheckManager.onScroll) - } - return e.session.on('changeScrollTop', spellCheckManager.onScroll) - } - - const initSpellCheck = function () { - if (!spellCheckManager) return - spellCheckManager.init() - editor.on('changeSession', onSessionChangeForSpellCheck) - onSessionChangeForSpellCheck({ - session: editor.getSession(), - }) // Force initial setup - return editor.on('nativecontextmenu', spellCheckManager.onContextMenu) - } - - const tearDownSpellCheck = function () { - if (!spellCheckManager) return - editor.off('changeSession', onSessionChangeForSpellCheck) - return editor.off( - 'nativecontextmenu', - spellCheckManager.onContextMenu - ) - } - - const initTrackChanges = function () { - if (!trackChangesManager) return - - trackChangesManager.rangesTracker = scope.sharejsDoc.ranges - - // Force onChangeSession in order to set up highlights etc. - trackChangesManager.onChangeSession() - - editor.on('changeSelection', trackChangesManager.onChangeSelection) - - // Selection also moves with updates elsewhere in the document - editor.on('change', trackChangesManager.onChangeSelection) - - editor.on('changeSession', trackChangesManager.onChangeSession) - editor.on('cut', trackChangesManager.onCut) - editor.on('paste', trackChangesManager.onPaste) - editor.renderer.on('resize', trackChangesManager.onResize) - } - - const tearDownTrackChanges = function () { - if (!trackChangesManager) return - trackChangesManager.tearDown() - editor.off('changeSelection', trackChangesManager.onChangeSelection) - - editor.off('change', trackChangesManager.onChangeSelection) - editor.off('changeSession', trackChangesManager.onChangeSession) - editor.off('cut', trackChangesManager.onCut) - editor.off('paste', trackChangesManager.onPaste) - editor.renderer.off('resize', trackChangesManager.onResize) - } - - const initUndo = function () { - // Emulate onChangeSession event. Note: listening to changeSession - // event is unnecessary since this method is called when we switch - // sessions (via ShareJS changing) anyway - undoManager.onChangeSession(editor.getSession()) - editor.on('change', undoManager.onChange) - } - - const tearDownUndo = function () { - editor.off('change', undoManager.onChange) - } - - const onSessionChangeForCursorPosition = function (e) { - if (e.oldSession != null) { - e.oldSession.selection.off( - 'changeCursor', - cursorPositionManager.onCursorChange - ) - } - return e.session.selection.on( - 'changeCursor', - cursorPositionManager.onCursorChange - ) - } - - const onUnloadForCursorPosition = () => - cursorPositionManager.onUnload(editor.getSession()) - - const initCursorPosition = function () { - editor.on('changeSession', onSessionChangeForCursorPosition) - - // Force initial setup - onSessionChangeForCursorPosition({ session: editor.getSession() }) - - return $(window).on('unload', onUnloadForCursorPosition) - } - - const tearDownCursorPosition = function () { - editor.off('changeSession', onSessionChangeForCursorPosition) - return $(window).off('unload', onUnloadForCursorPosition) - } - - initCursorPosition() - - // Trigger the event once *only* - this is called after Ace is connected - // to the ShareJs instance but this event should only be triggered the - // first time the editor is opened. Not every time the docs opened - const triggerEditorInitEvent = _.once(() => - scope.$broadcast('editorInit') - ) - - function attachToAce(sharejs_doc) { - let mode - const lines = sharejs_doc.getSnapshot().split('\n') - let session = editor.getSession() - if (session != null) { - session.destroy() - } - - // see if we can lookup a suitable mode from ace - // but fall back to text by default - try { - if (/\.(Rtex|bbl|tikz)$/i.test(scope.fileName)) { - // recognise Rtex and bbl as latex - mode = 'ace/mode/latex' - } else if (/\.(sty|cls|clo)$/.test(scope.fileName)) { - // recognise some common files as tex - mode = 'ace/mode/tex' - } else { - ;({ mode } = ModeList.getModeForPath(scope.fileName)) - // we prefer plain_text mode over text mode because ace's - // text mode is actually for code and has unwanted - // indenting (see wrapMethod in ace edit_session.js) - if (mode === 'ace/mode/text') { - mode = 'ace/mode/plain_text' - } - } - } catch (error) { - mode = 'ace/mode/plain_text' - } - - // create our new session - session = new EditSession(lines, mode) - - session.setUseWrapMode(true) - // use syntax validation only when explicitly set - if ( - scope.syntaxValidation != null && - syntaxValidationEnabled && - !/\.bib$/.test(scope.fileName) - ) { - session.setOption('useWorker', scope.syntaxValidation) - } - - // set to readonly until document change handlers are attached - editor.setReadOnly(true) - - // now attach session to editor - editor.setSession(session) - - initAcePerfListener(editor.textInput.getElement()) - - const doc = session.getDocument() - doc.on('change', onChange) - - editor.initing = true - sharejs_doc.attachToAce(editor) - editor.initing = false - - // now ready to edit document - // respect the readOnly setting, normally false - editor.setReadOnly(scope.readOnly) - triggerEditorInitEvent() - - if (!scope.readOnly) { - initSpellCheck() - } - - initTrackChanges() - initUndo() - - resetScrollMargins() - - // need to set annotations after attaching because attaching - // deletes and then inserts document content - session.setAnnotations(scope.annotations) - - if (scope.eventsBridge != null) { - session.on('changeScrollTop', onScroll) - } - - $rootScope.hasLintingError = false - session.on('changeAnnotation', function () { - // Both linter errors and compile logs are set as error annotations, - // however when the user types something, the compile logs are - // replaced with linter errors. When we check for lint errors before - // autocompile we are guaranteed to get linter errors - const hasErrors = - session - .getAnnotations() - .filter( - annotation => - annotation.type !== 'info' && - annotation.source !== 'compile' - ).length > 0 - - if ($rootScope.hasLintingError !== hasErrors) { - return ($rootScope.hasLintingError = hasErrors) - } - }) - - setTimeout(() => - // Let any listeners init themselves - onScroll(editor.renderer.getScrollTop()) - ) - - return editor.focus() - } - - function detachFromAce(sharejs_doc) { - tearDownSpellCheck() - tearDownTrackChanges() - tearDownUndo() - - tearDownAcePerfListener(editor.textInput.getElement()) - - sharejs_doc.detachFromAce() - sharejs_doc.off('remoteop.recordRemote') - - const session = editor.getSession() - session.off('changeScrollTop') - - const doc = session.getDocument() - return doc.off('change', onChange) - } - - if (scope.rendererData != null) { - editor.renderer.on('changeCharacterSize', () => { - scope.$apply( - () => (scope.rendererData.lineHeight = editor.renderer.lineHeight) - ) - }) - } - - scope.$watch('rendererData', function (rendererData) { - if (rendererData != null) { - return (rendererData.lineHeight = editor.renderer.lineHeight) - } - }) - - scope.$on('$destroy', function () { - if (scope.sharejsDoc != null) { - scope.$broadcast('changeEditor') - tearDownSpellCheck() - tearDownCursorPosition() - tearDownUndo() - detachFromAce(scope.sharejsDoc) - const session = editor.getSession() - if (session != null) { - session.destroy() - } - return scope.eventsBridge.emit( - 'aceScrollbarVisibilityChanged', - false, - 0 - ) - } - }) - - return scope.$emit(`${scope.name}:inited`, editor) - }, - - template: `\ -\ -`, - } - }, -]) - -function monkeyPatchSearch($rootScope, $compile) { - const searchHtml = `\ -\ -` - - // Remove Ace CSS - $('#ace_searchbox').remove() - - const SB = SearchBox.SearchBox - const { $init } = SB.prototype - SB.prototype.$init = function () { - this.element = $compile(searchHtml)($rootScope.$new())[0] - return $init.apply(this) - } -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.js deleted file mode 100644 index d3c28768e4..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/CommandManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/CommandManager.js deleted file mode 100644 index cd13330d8b..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/CommandManager.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/EnvironmentManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/EnvironmentManager.js deleted file mode 100644 index 119356dbd6..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/EnvironmentManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/Helpers.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/Helpers.js deleted file mode 100644 index 228b697432..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/Helpers.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/PackageManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/PackageManager.js deleted file mode 100644 index 5b23099104..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/PackageManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/Environments.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/Environments.js deleted file mode 100644 index 6f62be574b..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/Environments.js +++ /dev/null @@ -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, -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/TopHundredSnippets.js b/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/TopHundredSnippets.js deleted file mode 100644 index 1ac3792287..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/auto-complete/snippets/TopHundredSnippets.js +++ /dev/null @@ -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, - }, -] diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.js b/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.js deleted file mode 100644 index 046099774f..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.js +++ /dev/null @@ -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() - } -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.js deleted file mode 100644 index 0bb13e655f..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.js +++ /dev/null @@ -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() - } -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/highlights/HighlightsManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/highlights/HighlightsManager.js deleted file mode 100644 index e66c6a5131..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/highlights/HighlightsManager.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/metadata/MetadataManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/metadata/MetadataManager.js deleted file mode 100644 index eea37c8a06..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/metadata/MetadataManager.js +++ /dev/null @@ -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() - } -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.js deleted file mode 100644 index b193ca7989..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.js +++ /dev/null @@ -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) - ) - } -} diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/IgnoredMisspellings.js b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/IgnoredMisspellings.js deleted file mode 100644 index 398908aebf..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/IgnoredMisspellings.js +++ /dev/null @@ -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', -] diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.js b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.js deleted file mode 100644 index 2339875564..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js deleted file mode 100644 index 5808b10ae1..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js +++ /dev/null @@ -1,463 +0,0 @@ -import ignoredWords from '../../../../../features/dictionary/ignored-words' -import { debugConsole } from '@/utils/debugging' - -// eslint-disable-next-line prefer-regex-literals -const BLACKLISTED_COMMAND_REGEX = new RegExp( - `\ -\\\\\ -(label\ -|[a-z]{0,16}ref\ -|usepackage\ -|begin\ -|end\ -|[a-z]{0,16}cite\ -|input\ -|include\ -|includegraphics)\ -(\\[[^\\]]*\\])?\ -\\{[^}]*\\}\ -`, - 'g' -) -// Regex generated from /\\?['\p{L}]+/g via https://mothereff.in/regexpu. -// \p{L} matches unicode characters in the 'letter' category, but is not supported until ES6. -const WORD_REGEX = - /\\?(?:['A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D])+/g -const DEBOUNCE_DELAY = 500 - -class SpellCheckManager { - constructor($scope, $cacheFactory, $http, $q, adapter) { - this.onChange = this.onChange.bind(this) - this.onSessionChange = this.onSessionChange.bind(this) - this.onContextMenu = this.onContextMenu.bind(this) - this.onScroll = this.onScroll.bind(this) - this.learnWord = this.learnWord.bind(this) - this.$scope = $scope - this.$http = $http - this.$q = $q - this.adapter = adapter - this.$scope.spellMenu = { - open: false, - top: '0px', - left: '0px', - suggestions: [], - } - this.inProgressRequest = null - this.changedLines = [] - this.firstCheck = true - - this.$scope.$watch('spellCheckLanguage', (language, oldLanguage) => { - if (language !== oldLanguage && oldLanguage != null) { - return this.reInitForLangChange() - } - }) - this.$scope.replaceWord = this.adapter.replaceWord - this.$scope.learnWord = this.learnWord - - this.cache = - $cacheFactory.get(`spellCheck-cache`) || - $cacheFactory(`spellCheck-cache`, { - capacity: 15000, - }) - - this.selectedHighlightContents = null - - window.addEventListener('learnedWords:reset', () => this.reset()) - - window.addEventListener('learnedWords:remove', ({ detail: word }) => - this.removeWordFromCache(word) - ) - - $(document).on('click', e => { - // There is a bug (?) in Safari when ctrl-clicking an element, and the - // the contextmenu event is preventDefault-ed. In this case, the - // contextmenu event *and* a click event propagate from the element. - // If the contextmenu is *not* preventDefault-ed, then only the - // contextmenu event propagates. The latter behaviour is what other - // browsers do no matter if the event is preventDefault-ed or not. - // See: https://stackoverflow.com/questions/53660359/preventing-default-on-contextmenu-event-triggers-subsequent-click-event - // This means that on Safari the spelling menu is opened in response to - // the contextmenu event, but then quickly closed due to the - // (propagated) click event. - // We still need to handle the click event, to enable the "click-away" - // behaviour. The workaround is to check if we're in Safari and the Ctrl - // key is pressed in the click handler, and if so, then prevent the - // spelling menu from closing. - // That when clicking as normal, the "click-away" behaviour still works - // as before. - // When ctrl-clicking, the click event is not propagated, so the handler - // is ignored. However this triggers another contextmenu event, which - // closes the previous spelling menu. - if (window._ide.browserIsSafari && e.ctrlKey) { - return - } - - // Ignore if right click - if (e.which !== 3) { - this.closeContextMenu() - } - return true - }) - } - - init() { - this.changedLines = Array(this.adapter.getLineCount()).fill(true) - this.firstCheck = true - if (this.isSpellCheckEnabled()) { - this.runSpellCheckSoon(DEBOUNCE_DELAY) - } - } - - reset() { - this.adapter.highlightedWordManager.reset() - this.init() - } - - reInitForLangChange() { - this.reset() - } - - isSpellCheckEnabled() { - return !!( - this.$scope.spellCheck && - this.$scope.spellCheckLanguage && - this.$scope.spellCheckLanguage !== '' - ) - } - - onChange(e) { - if (this.isSpellCheckEnabled()) { - this.$scope.$applyAsync(() => { - if ( - this.selectedHighlightContents && - this.selectedHighlightContents !== this.adapter.getSelectionContents() - ) { - this.closeContextMenu() - } - }) - this.markLinesAsUpdated(this.adapter.normalizeChangeEvent(e)) - this.adapter.highlightedWordManager.clearHighlightTouchingRange(e) - this.runSpellCheckSoon(DEBOUNCE_DELAY) - } - } - - onSessionChange() { - this.adapter.highlightedWordManager.reset() - if (this.inProgressRequest != null) { - this.inProgressRequest.abort() - } - - if (this.isSpellCheckEnabled()) { - this.runSpellCheckSoon(DEBOUNCE_DELAY) - } - } - - onContextMenu(e) { - this.closeContextMenu() - this.openContextMenu(e) - } - - onScroll() { - if (this.isSpellCheckEnabled()) { - this.runSpellCheckSoon(DEBOUNCE_DELAY) - } - this.closeContextMenu() - } - - openContextMenu(e) { - const coords = this.adapter.getCoordsFromContextMenuEvent(e) - const highlight = this.adapter.getHighlightFromCoords(coords) - const shouldPositionFromBottom = - this.adapter.isContextMenuEventOnBottomHalf(e) - if (highlight) { - this.adapter.preventContextMenuEventDefault(e) - this.adapter.selectHighlightedWord(highlight) - this.$scope.$applyAsync(() => { - this.selectedHighlightContents = this.adapter.getSelectionContents() - this.$scope.spellMenu = { - open: true, - top: coords.y + 'px', - left: coords.x + 'px', - layoutFromBottom: shouldPositionFromBottom, - highlight, - } - }) - } - } - - closeContextMenu() { - // This is triggered on scroll, so for performance only apply setting when - // it changes - if ( - (this.$scope != null ? this.$scope.spellMenu : undefined) && - this.$scope.spellMenu.open !== false - ) { - this.selectedHighlightContents = null - this.$scope.$applyAsync(() => { - this.$scope.spellMenu.open = false - }) - } - } - - removeWordFromCache(word) { - const language = this.$scope.spellCheckLanguage - this.cache.remove(`${language}:${word}`) - } - - learnWord(highlight) { - this.apiRequest('/learn', { word: highlight.word }) - this.adapter.highlightedWordManager.removeWord(highlight.word) - const language = this.$scope.spellCheckLanguage - this.cache.put(`${language}:${highlight.word}`, true) - ignoredWords.add(highlight.word) - } - - markLinesAsUpdated(change) { - const { start } = change - const { end } = change - - const insertLines = () => { - let lines = end.row - start.row - while (lines--) { - this.changedLines.splice(start.row, 0, true) - } - } - - const removeLines = () => { - let lines = end.row - start.row - while (lines--) { - this.changedLines.splice(start.row + 1, 1) - } - } - - if (change.action === 'insert') { - this.changedLines[start.row] = true - insertLines() - } else if (change.action === 'remove') { - this.changedLines[start.row] = true - removeLines() - } - } - - runSpellCheckSoon(delay) { - if (delay == null) { - delay = 1000 - } - const run = () => { - delete this.timeoutId - this.runSpellCheck() - } - if (this.timeoutId != null) { - clearTimeout(this.timeoutId) - } - this.timeoutId = setTimeout(run, delay) - } - - runSpellCheck() { - let j - let i, key, word - const rowNumsToCheck = this.getRowNumsToCheck() - const language = this.$scope.spellCheckLanguage - let { words, positions } = this.getWords(rowNumsToCheck) - - const highlights = [] - const seen = {} - const newWords = [] - const newPositions = [] - - // iterate through all words, building up a list of - // newWords/newPositions not in the cache - for (j = 0, i = j; j < words.length; j++, i = j) { - word = words[i] - key = `${language}:${word}` - if (seen[key] == null) { - seen[key] = this.cache.get(key) - } // avoid hitting the cache unnecessarily - const cached = seen[key] - if (cached == null) { - newWords.push(words[i]) - newPositions.push(positions[i]) - } else if (cached === true) { - // word is correct - } else { - highlights.push({ - column: positions[i].column, - row: positions[i].row, - word, - suggestions: cached, - }) - } - } - words = newWords - positions = newPositions - - const displayResult = highlights => { - if (this.timeoutId != null) { - return - } - this.firstCheck = false - rowNumsToCheck.forEach(row => { - this.changedLines[row] = false - this.adapter.highlightedWordManager.clearRow(row) - }) - highlights.map(highlight => - this.adapter.highlightedWordManager.addHighlight(highlight) - ) - } - - if (!words.length) { - displayResult(highlights) - } else { - this.inProgressRequest = this.apiRequest( - '/check', - { language, words, skipLearnedWords: true }, - (error, result) => { - delete this.inProgressRequest - if (error != null || result == null || result.misspellings == null) { - return null - } - const misspelled = [] - for (const misspelling of result.misspellings) { - word = words[misspelling.index] - const position = positions[misspelling.index] - misspelled[misspelling.index] = true - highlights.push({ - column: position.column, - row: position.row, - word, - suggestions: misspelling.suggestions, - }) - key = `${language}:${word}` - if (!seen[key]) { - this.cache.put(key, misspelling.suggestions) - seen[key] = true - } - } - for (i = 0; i < words.length; i++) { - word = words[i] - if (!misspelled[i]) { - key = `${language}:${word}` - if (!seen[key]) { - this.cache.put(key, true) - seen[key] = true - } - } - } - displayResult(highlights) - } - ) - } - } - - apiRequest(endpoint, data, callback) { - if (callback == null) { - callback = function (error, result) { - if (error) { - debugConsole.error(error) - } - } - } - data.token = window.user.id - data._csrf = window.csrfToken - // use angular timeout option to cancel request if doc is changed - const requestHandler = this.$q.defer() - const options = { timeout: requestHandler.promise } - this.$http - .post(`/spelling${endpoint}`, data, options) - .then(response => { - return callback(null, response.data) - }) - .catch(response => { - return callback(new Error('api failure')) - }) - // provide a method to cancel the request - const abortRequest = () => requestHandler.resolve() - return { abort: abortRequest } - } - - getWords(rowNumsToCheck) { - const lines = this.adapter.getLinesByRows(rowNumsToCheck) - const words = [] - const positions = [] - for (let row = 0; row < lines.length; row++) { - let line = lines[row] - const rowIdx = rowNumsToCheck[row] - line = this.blankOutBlacklistedCommands(line) - let result - WORD_REGEX.lastIndex = 0 // reset global stateful regexp for this usage - while ((result = WORD_REGEX.exec(line))) { - let word = result[0] - // Skip latex commands, as they are ignored by the backend anyway - if (word.slice(0, 1) === '\\') { - continue - } - if (word[0] === "'") { - word = word.slice(1) - } - if (word[word.length - 1] === "'") { - word = word.slice(0, -1) - } - if (!ignoredWords.has(word)) { - positions.push({ row: rowIdx, column: result.index }) - words.push(word) - } - } - } - return { words, positions } - } - - // Returns row numbers for dirty lines (i.e. with changes, need to be checked). - // Initially, all lines are considered to be dirty/have changes (i.e. weren't checked before). - // Generally, only the visible viewport is considered. Upon initialization, a larger area is - // considered (visible viewport ± 2x the size of the visible viewport). - // - // Examples: - // 1. Document with 10 lines; initial check - // Viewport show lines 3 to 7 - // Initial check means that we are going to also check the area above and below the visible viewport - // Extra area to be checked is 2x the size of visible viewport (5 visible lines; 2 * 5 = 10), i.e. - // 3 - 10 = -7 (clamped to 0, the lower boundary of document lines) - // 7 + 10 = 17 (clamped to 9, the upper boundary of document lines) - // Expected result: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] - // 2. Document with 10 lines; document has already been initially checked; user changes lines 5 and 6 - // Viewport show lines 3 to 7 - // Expected result: [ 5, 6 ] (i.e. unchanged lines are ignored; intersection of viewport and changed lines) - // 3. Document with 10 lines, document has already been initially checked, remote user changes lines 1 and 2 - // Viewport show lines 3 to 7 - // Expected result: [ ] (i.e. changes were outside viewport, so there are no visible changed lines) - getRowNumsToCheck() { - const firstVisibleRowNum = this.adapter.getFirstVisibleRowNum() - const lastVisibleRowNum = this.adapter.getLastVisibleRowNum() - let firstRowNumtoCheck = firstVisibleRowNum - let lastRowNumtoCheck = lastVisibleRowNum - - if (this.firstCheck) { - const lastRowNum = this.adapter.getLineCount() - 1 - const nVisibleLines = lastVisibleRowNum - firstVisibleRowNum + 1 - firstRowNumtoCheck = Math.max(0, firstVisibleRowNum - 2 * nVisibleLines) - lastRowNumtoCheck = Math.min( - lastVisibleRowNum + 2 * nVisibleLines, - lastRowNum - ) - } - - return this.changedLines.reduce((rowNumsToCheck, curVal, curRow) => { - if ( - curVal && - curRow >= firstRowNumtoCheck && - curRow <= lastRowNumtoCheck - ) { - rowNumsToCheck.push(curRow) - } - return rowNumsToCheck - }, []) - } - - blankOutBlacklistedCommands(line) { - return line.replace(BLACKLISTED_COMMAND_REGEX, command => - Array(command.length + 1).join('.') - ) - } -} - -export default SpellCheckManager diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesAdapter.js b/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesAdapter.js deleted file mode 100644 index 123381558e..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesAdapter.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.js deleted file mode 100644 index 2a6fb0504c..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/undo/UndoManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/undo/UndoManager.js deleted file mode 100644 index 0c2b32ef32..0000000000 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/undo/UndoManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/files/services/files.js b/services/web/frontend/js/ide/files/services/files.js deleted file mode 100644 index 98cf1745d2..0000000000 --- a/services/web/frontend/js/ide/files/services/files.js +++ /dev/null @@ -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 - }, -]) diff --git a/services/web/frontend/js/ide/graphics/services/graphics.js b/services/web/frontend/js/ide/graphics/services/graphics.js deleted file mode 100644 index 9d42597059..0000000000 --- a/services/web/frontend/js/ide/graphics/services/graphics.js +++ /dev/null @@ -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 - } -} diff --git a/services/web/frontend/js/ide/preamble/services/preamble.js b/services/web/frontend/js/ide/preamble/services/preamble.js deleted file mode 100644 index 6eeaf69c30..0000000000 --- a/services/web/frontend/js/ide/preamble/services/preamble.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js b/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js index 5ec471dd04..9c202d1b6a 100644 --- a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js +++ b/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js @@ -280,26 +280,6 @@ export default App.controller('ReviewPanelController', [ return rangesTrackers[doc_id] } - let scrollbar = {} - ide.$scope.reviewPanelEventsBridge.on( - 'aceScrollbarVisibilityChanged', - function (isVisible, scrollbarWidth) { - scrollbar = { isVisible, scrollbarWidth } - return updateScrollbar() - } - ) - - function updateScrollbar() { - if ( - scrollbar.isVisible && - ide.$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE - ) { - return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`) - } else { - return $reviewPanelEl.css('right', '0') - } - } - $scope.$watch( '!ui.reviewPanelOpen && reviewPanel.hasEntries', function (open, prevVal) { @@ -335,7 +315,6 @@ export default App.controller('ReviewPanelController', [ if (view == null) { return } - updateScrollbar() if (view === $scope.SubViews.OVERVIEW) { return refreshOverviewPanel() } else if (oldView === $scope.SubViews.OVERVIEW) { diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js index b706148816..014e741f61 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js @@ -249,16 +249,14 @@ export default App.directive('reviewPanelSorted', function () { // mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into // scroll events ourselves, then it makes the review panel slightly less smooth (barely) // noticeable, but keeps it perfectly in step with Ace. - ace - .require('ace/lib/event') - .addMouseWheelListener(scroller[0], function (e) { - const deltaY = e.wheelY - const old_top = parseInt(list.css('top')) - const top = old_top - deltaY * 4 - scrollAce(-top) - dispatchScrollEvent(deltaY * 4) - return e.preventDefault() - }) + scroller[0].addEventListener('wheel', e => { + // FIXME (or remove this): https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event + const deltaY = e.wheelY + const old_top = parseInt(list.css('top')) + const top = old_top - deltaY * 4 + dispatchScrollEvent(deltaY * 4) + return e.preventDefault() + }) // We always scroll by telling Ace to scroll and then updating the // 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 window.addEventListener('editor:scroll', event => { const { scrollTop, height, paddingTop } = event.detail diff --git a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js b/services/web/frontend/js/ide/toolbar/EditorLoaderController.js index 59c233b6b0..d38d67a5af 100644 --- a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js +++ b/services/web/frontend/js/ide/toolbar/EditorLoaderController.js @@ -10,12 +10,5 @@ App.controller('EditorLoaderController', [ val === true ? 'rich-text' : 'source' ) }) - - $scope.$watch('editor.newSourceEditor', function (val) { - localStorage( - `editor.source_editor.${$scope.project_id}`, - val === true ? 'cm6' : 'ace' - ) - }) }, ]) diff --git a/services/web/frontend/js/ide/toolbar/EditorToolbarController.js b/services/web/frontend/js/ide/toolbar/EditorToolbarController.js deleted file mode 100644 index f68fb70526..0000000000 --- a/services/web/frontend/js/ide/toolbar/EditorToolbarController.js +++ /dev/null @@ -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 - }, -]) diff --git a/services/web/frontend/js/ide/toolbar/index.js b/services/web/frontend/js/ide/toolbar/index.js index ce97241d2f..fd9e505cdb 100644 --- a/services/web/frontend/js/ide/toolbar/index.js +++ b/services/web/frontend/js/ide/toolbar/index.js @@ -1,2 +1 @@ import './EditorLoaderController' -import './EditorToolbarController' diff --git a/services/web/frontend/js/vendor/libs/sharejs.js b/services/web/frontend/js/vendor/libs/sharejs.js index ca8a4e23ca..bee6afa272 100644 --- a/services/web/frontend/js/vendor/libs/sharejs.js +++ b/services/web/frontend/js/vendor/libs/sharejs.js @@ -26,7 +26,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -define(['ace/ace','crypto-js/sha1', '@/utils/debugging'], function (_ignore, CryptoJSSHA1, { debugging, debugConsole }) { +define(['crypto-js/sha1', '@/utils/debugging'], function (CryptoJSSHA1, { debugging, debugConsole }) { var append = void 0, bootstrapTransform = void 0, exports = void 0, @@ -1410,177 +1410,6 @@ define(['ace/ace','crypto-js/sha1', '@/utils/debugging'], function (_ignore, Cry MicroEvent.mixin(Doc); exports.Doc = Doc; - // This is some utility code to connect an ace editor to a sharejs document. - - var _ace$require = ace.require('ace/range'), - Range = _ace$require.Range; - - // Convert an ace delta into an op understood by share.js - - - var applyAceToShareJS = function applyAceToShareJS(editorDoc, delta, doc, fromUndo) { - // Get the start position of the range, in no. of characters - var getStartOffsetPosition = function getStartOffsetPosition(start) { - // This is quite inefficient - getLines makes a copy of the entire - // lines array in the document. It would be nice if we could just - // access them directly. - var lines = editorDoc.getLines(0, start.row); - - var offset = 0; - - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - offset += i < start.row ? line.length : start.column; - } - - // Add the row number to include newlines. - return offset + start.row; - }; - - var pos = getStartOffsetPosition(delta.start); - - // NOTE: Keep in sync with EditorWatchdogManager. - switch (delta.action) { - case 'insert': - text = delta.lines.join('\n'); - doc.insert(pos, text, fromUndo); - break; - - case 'remove': - text = delta.lines.join('\n'); - doc.del(pos, text.length, fromUndo); - break; - - default: - throw new Error('unknown action: ' + delta.action); - } - }; - - // Attach an ace editor to the document. The editor's contents are replaced - // with the document's contents. - window.sharejs.extendDoc('attach_ace', function (editor, maxDocLength) { - if (!this.provides['text']) { - throw new Error('Only text documents can be attached to ace'); - } - - var doc = this; - var editorDoc = editor.getSession().getDocument(); - editorDoc.setNewLineMode('unix'); - - function check() { - return window.setTimeout(function () { - var editorText = editorDoc.getValue(); - var otText = doc.getText(); - - if (editorText !== otText) { - doc.emit('error','Text does not match in ace') - debugConsole.error('Text does not match!'); - debugConsole.error('editor: ' + editorText); - debugConsole.error('ot: ' + otText); - } - } - // Should probably also replace the editor text with the doc snapshot. - , 0); - }; - - onDelete(0, editorDoc.getValue()); - onInsert(0, doc.getText()); - - check(); - - // Listen for edits in ace - function editorListener(change) { - if (change.origin === 'remote') { - // this change has been injected via sharejs - return; - } - - if (maxDocLength != null && editorDoc.getValue().length >= maxDocLength) { - doc.emit('error', new Error('document length is greater than maxDocLength')); - return; - } - - var fromUndo = !!(editor.getSession().$fromUndo || editor.getSession().$fromReject); - - applyAceToShareJS(editorDoc, change, doc, fromUndo); - - return check(); - }; - - editorDoc.on('change', editorListener); - - // Horribly inefficient. - function offsetToPos(offset) { - // Again, very inefficient. - var lines = editorDoc.getAllLines(); - - var row = 0; - for (row = 0; row < lines.length; row++) { - var line = lines[row]; - if (offset <= line.length) { - break; - } - - // +1 for the newline. - offset -= lines[row].length + 1; - } - - return { row: row, column: offset }; - }; - - // We want to insert the flag `origin: 'remote'` into the delta if the op - // is the initial document write or comes from the underlying sharejs doc - // (which means it is from a remote op), so we have to do the work of - // editorDoc.insert and editorDoc.remove manually. - // These methods are copied from ace.js doc#insert and #remove, and then - // inject the `origin: 'remote'` flag into the delta. - function onInsert(pos, text) { - if (editorDoc.getLength() <= 1) { - editorDoc.$detectNewLine(text); - } - - var lines = editorDoc.$split(text); - var position = offsetToPos(pos); - var start = editorDoc.clippedPos(position.row, position.column); - var end = { - row: start.row + lines.length - 1, - column: (lines.length === 1 ? start.column : 0) + lines[lines.length - 1].length - }; - - editorDoc.applyDelta({ - start: start, - end: end, - action: 'insert', - lines: lines, - origin: 'remote' - }); - return check(); - }; - - function onDelete(pos, text) { - var range = Range.fromPoints(offsetToPos(pos), offsetToPos(pos + text.length)); - var start = editorDoc.clippedPos(range.start.row, range.start.column); - var end = editorDoc.clippedPos(range.end.row, range.end.column); - editorDoc.applyDelta({ - start: start, - end: end, - action: 'remove', - lines: editorDoc.getLinesForRange({ start: start, end: end }), - origin: 'remote' - }); - return check(); - }; - - doc.on('insert', onInsert); - doc.on('delete', onDelete); - - doc.detach_ace = function () { - doc.removeListener('insert', onInsert); - doc.removeListener('delete', onDelete); - editorDoc.removeListener('change', editorListener); - return delete doc.detach_ace; - }; - }); return window.sharejs; }); diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 8e3b3bfaae..127f8c0cf2 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -92,7 +92,6 @@ const initialize = () => { toggleHistory: () => {}, editor: { richText: false, - newSourceEditor: false, sharejs_doc: { doc_id: 'test-doc', getSnapshot: () => 'some doc content', diff --git a/services/web/frontend/stories/editor-switch.stories.jsx b/services/web/frontend/stories/editor-switch.stories.jsx index 7ce62fbcef..2db4255768 100644 --- a/services/web/frontend/stories/editor-switch.stories.jsx +++ b/services/web/frontend/stories/editor-switch.stories.jsx @@ -1,4 +1,4 @@ -import EditorSwitch from '../js/features/source-editor/components/editor-switch-legacy' +import EditorSwitch from '../js/features/source-editor/components/editor-switch' import { ScopeDecorator } from './decorators/scope' export default { diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index 92d16e2b29..ecdf04a268 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -93,13 +93,6 @@ .full-size; } -.editor-container #editor { - top: @editor-toolbar-height; -} -.editor-container.has-source-toolbar #editor { - top: 0; -} - .pdf-empty, .no-history-available, .no-file-selection, @@ -276,104 +269,6 @@ } } -/************************************** -Ace -***************************************/ - -// The internal components of the aceEditor directive -.ace-editor-wrapper { - .full-size; - .undo-conflict-warning { - position: absolute; - top: 0; - right: 0; - left: 0; - z-index: 10; - } - .ace-editor-body { - width: 100%; - height: 100%; - } - .spelling-highlight { - z-index: 3; - position: absolute; - background-image: url(../../../public/img/spellcheck-underline.png); - @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - background-image: url(../../../public/img/spellcheck-underline@2x.png); - background-size: 5px 4px; - } - background-repeat: repeat-x; - background-position: bottom left; - } - .remote-cursor { - position: absolute; - border-left: 2px solid transparent; - // Adds "nubbin" top right of cursor, which inherits the injected color - &::before { - content: ''; - position: absolute; - left: -2px; - top: -5px; - height: 5px; - width: 5px; - border-top-width: 3px; - border-right-width: 3px; - border-bottom-width: 2px; - border-left-width: 2px; - border-style: solid; - border-color: inherit; - } - } - .annotation-label { - padding: (@line-height-computed / 4) (@line-height-computed / 2); - font-size: 0.8rem; - z-index: 100; - font-family: @font-family-sans-serif; - color: white; - font-weight: 700; - white-space: nowrap; - } - .annotation { - position: absolute; - z-index: 2; - } - .highlights-before-label, - .highlights-after-label { - position: absolute; - right: @line-height-computed; - z-index: 1; - } - .highlights-before-label { - top: @line-height-computed / 2; - } - .highlights-after-label { - bottom: @line-height-computed / 2; - } -} - -.strike-through-foreground::after { - content: ''; - position: absolute; - width: 100%; - top: 50%; - margin-top: -1px; - height: 2px; - background: currentColor; -} - -// Hack to solve an issue where scrollbars aren't visible in Safari. -// Safari seems to clip part of the scrollbar element. By giving the -// element a background, we're telling Safari that it *really* needs to -// paint the whole area. See https://github.com/ajaxorg/ace/issues/2872 -.ace_scrollbar-inner { - background-color: #fff; - opacity: 0.01; - - .ace_dark & { - background-color: #000; - } -} - /************************************** CodeMirror ***************************************/ diff --git a/services/web/frontend/stylesheets/app/editor/review-panel.less b/services/web/frontend/stylesheets/app/editor/review-panel.less index c86bde1e76..3e8d075b96 100644 --- a/services/web/frontend/stylesheets/app/editor/review-panel.less +++ b/services/web/frontend/stylesheets/app/editor/review-panel.less @@ -82,7 +82,7 @@ } position: absolute; - top: 32px; + top: 0px; bottom: 0px; right: 0px; background-color: @rp-bg-blue; @@ -90,10 +90,6 @@ font-size: @rp-base-font-size; color: @rp-type-blue; z-index: 6; - - .has-source-toolbar & { - top: 0; - } } .loading-panel { @@ -1128,10 +1124,6 @@ button when (@is-overleaf-light = true) { .rp-unsupported & { display: none; } - - .has-source-toolbar & { - top: 32px; - } } .rp-track-changes-indicator { @@ -1376,8 +1368,4 @@ button when (@is-overleaf-light = true) { .rp-track-changes-indicator { border: 0; } - - .has-source-toolbar & { - top: 0; - } } diff --git a/services/web/package.json b/services/web/package.json index 469267ea88..4e2ee14611 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -239,7 +239,6 @@ "@uppy/utils": "^4.0.7", "@uppy/xhr-upload": "^1.6.8", "abort-controller": "^3.0.0", - "ace-builds": "overleaf/ace-builds#v1.4.12-69aace50e6796d42116f8f96e19d2468d8a88af9", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "algoliasearch": "^3.35.1", diff --git a/services/web/test/frontend/components/editor-left-menu/scope.tsx b/services/web/test/frontend/components/editor-left-menu/scope.tsx index 76649d5ee9..0de8ad9fe5 100644 --- a/services/web/test/frontend/components/editor-left-menu/scope.tsx +++ b/services/web/test/frontend/components/editor-left-menu/scope.tsx @@ -10,7 +10,6 @@ type Scope = { doc_id?: string getSnapshot?: () => string } - newSourceEditor?: boolean } hasLintingError?: boolean ui?: { @@ -45,7 +44,6 @@ export const mockScope = (scope?: Scope) => ({ doc_id: 'test-doc', getSnapshot: () => 'some doc content', }, - newSourceEditor: true, }, hasLintingError: false, ui: { diff --git a/services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx b/services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx index 113e6aab42..2376e056cd 100644 --- a/services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx +++ b/services/web/test/frontend/features/editor-left-menu/components/help-show-hotkeys.test.jsx @@ -15,7 +15,6 @@ describe('', function () { expect(screen.queryByRole('dialog')).to.equal(null) fireEvent.click(screen.getByRole('button', { name: 'Show Hotkeys' })) const modal = screen.getAllByRole('dialog')[0] - within(modal).getByText('Hotkeys (Legacy source editor)') within(modal).getByText('Common') }) }) diff --git a/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.jsx b/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.jsx index e68e1e03c4..615f589a33 100644 --- a/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.jsx +++ b/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.jsx @@ -11,25 +11,13 @@ const modalProps = { describe('', function () { it('renders the translated modal title on cm6', async function () { - const { baseElement } = render( - - ) + const { baseElement } = render() expect(baseElement.querySelector('.modal-title').textContent).to.equal( 'Hotkeys (Source editor)' ) }) - it('renders the translated modal title on ace', async function () { - const { baseElement } = render( - - ) - - expect(baseElement.querySelector('.modal-title').textContent).to.equal( - 'Hotkeys (Legacy source editor)' - ) - }) - it('renders translated heading with embedded code', function () { const { baseElement } = render() diff --git a/services/web/test/frontend/features/outline/outline-parser.test.js b/services/web/test/frontend/features/outline/outline-parser.test.js deleted file mode 100644 index 7148675195..0000000000 --- a/services/web/test/frontend/features/outline/outline-parser.test.js +++ /dev/null @@ -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, - }, - ], - }, - ], - }, - ], - }, - ]) - }) - }) -}) diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 3becce740d..ebfe0afc7b 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -61,18 +61,11 @@ function getModuleDirectory(moduleName) { const mathjaxDir = getModuleDirectory('mathjax') const mathjax3Dir = getModuleDirectory('mathjax-3') -const aceDir = getModuleDirectory('ace-builds') const pdfjsVersions = ['pdfjs-dist213', 'pdfjs-dist401'] const vendorDir = path.join(__dirname, 'frontend/js/vendor') -const ACE_VERSION = require('ace-builds/version') -if (ACE_VERSION !== PackageVersions.version.ace) { - throw new Error( - '"ace-builds" version de-synced, update services/web/app/src/infrastructure/PackageVersions.js' - ) -} const MATHJAX_VERSION = require('mathjax/package.json').version if (MATHJAX_VERSION !== PackageVersions.version.mathjax) { throw new Error( @@ -236,11 +229,6 @@ module.exports = { }, resolve: { alias: { - // Aliases for AMD modules - - // Enables ace/ace shortcut - ace: 'ace-builds/src-noconflict', - // custom prefixes for import paths '@': path.resolve(__dirname, './frontend/js/'), }, @@ -335,11 +323,6 @@ module.exports = { to: 'js/libs/mathjax', context: mathjaxDir, }, - { - from: 'src-min-noconflict', - to: `js/ace-${PackageVersions.version.ace}/`, - context: aceDir, - }, ...pdfjsVersions.flatMap(version => { const dir = getModuleDirectory(version)