| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975 |
- 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'
- 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}`)
- } 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', function(
- $timeout,
- $compile,
- $rootScope,
- eventTracking,
- localStorage,
- $cacheFactory,
- metadata,
- graphics,
- preamble,
- files,
- $http,
- $q,
- $window
- ) {
- 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
- // auto-insertion of braces, brackets, dollars
- editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false)
- editor.setOption('wrapBehavioursEnabled', false)
- 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 */
- scope.$watch('onSave', function(callback) {
- if (callback != null) {
- Vim.defineEx('write', 'w', callback)
- editor.commands.addCommand({
- name: 'save',
- bindKey: {
- win: 'Ctrl-S',
- mac: 'Command-S'
- },
- exec: callback,
- readOnly: true
- })
- // Not technically 'save', but Ctrl-. recompiles in OL v1
- // so maintain compatibility
- return editor.commands.addCommand({
- name: 'recompile_v1',
- bindKey: {
- win: 'Ctrl-.',
- mac: 'Ctrl-.'
- },
- exec: callback,
- readOnly: true
- })
- }
- })
- 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 (let 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)
- })
- var setScrollMargins = function(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',
- 'source-code-pro',
- 'monospace'
- ]
- if (value != null) {
- switch (value) {
- case 'monaco':
- return editor.setOption(
- 'fontFamily',
- monospaceFamilies.join(', ')
- )
- case 'lucida':
- return editor.setOption(
- 'fontFamily',
- '"Lucida Console", 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`)
- }
- const onScroll = function(scrollTop) {
- if (scope.eventsBridge == null) {
- return
- }
- const height = editor.renderer.layerConfig.maxHeight
- 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() {
- trackChangesManager.rangesTracker = scope.sharejsDoc.ranges
- // Force onChangeSession in order to set up highlights etc.
- trackChangesManager.onChangeSession()
- if (!trackChangesManager) return
- 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')
- )
- var attachToAce = function(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)
- 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)
- session.on('changeScrollTop', eventTracking.editingSessionHeartbeat)
- angular
- .element($window)
- .on('click', eventTracking.editingSessionHeartbeat)
- scope.$on('$destroy', () =>
- angular
- .element($window)
- .off('click', eventTracking.editingSessionHeartbeat)
- )
- 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').length > 0
- if ($rootScope.hasLintingError !== hasErrors) {
- return ($rootScope.hasLintingError = hasErrors)
- }
- })
- setTimeout(() =>
- // Let any listeners init themselves
- onScroll(editor.renderer.getScrollTop())
- )
- return editor.focus()
- }
- var detachFromAce = function(sharejs_doc) {
- tearDownSpellCheck()
- tearDownTrackChanges()
- tearDownUndo()
- 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: `\
- <div class="ace-editor-wrapper">
- <div
- class="undo-conflict-warning alert alert-danger small"
- ng-show="undo.show_remote_warning"
- >
- <strong>Watch out!</strong>
- We had to undo some of your collaborators changes before we could undo yours.
- <a
- href="#"
- class="pull-right"
- ng-click="undo.show_remote_warning = false"
- >Dismiss</a>
- </div>
- <div class="ace-editor-body"></div>
- <spell-menu
- open="spellMenu.open"
- top="spellMenu.top"
- left="spellMenu.left"
- layout-from-bottom="spellMenu.layoutFromBottom"
- highlight="spellMenu.highlight"
- replace-word="replaceWord(highlight, suggestion)"
- learn-word="learnWord(highlight)"
- ></spell-menu>
- <div
- class="annotation-label"
- ng-show="annotationLabel.show"
- ng-style="{
- position: 'absolute',
- left: annotationLabel.left,
- right: annotationLabel.right,
- bottom: annotationLabel.bottom,
- top: annotationLabel.top,
- 'background-color': annotationLabel.backgroundColor
- }"
- >
- {{ annotationLabel.text }}
- </div>
- <a
- href
- class="highlights-before-label btn btn-info btn-xs"
- ng-show="updateLabels.highlightsBefore > 0"
- ng-click="gotoHighlightAbove()"
- >
- <i class="fa fa-fw fa-arrow-up"></i>
- {{ updateLabels.highlightsBefore }} more update{{ updateLabels.highlightsBefore > 1 && "" || "s" }} above
- </a>
- <a
- href
- class="highlights-after-label btn btn-info btn-xs"
- ng-show="updateLabels.highlightsAfter > 0"
- ng-click="gotoHighlightBelow()"
- >
- <i class="fa fa-fw fa-arrow-down"></i>
- {{ updateLabels.highlightsAfter }} more update{{ updateLabels.highlightsAfter > 1 && "" || "s" }} below
- </a>
- </div>\
- `
- }
- })
- function monkeyPatchSearch($rootScope, $compile) {
- const searchHtml = `\
- <div class="ace_search right">
- <a href type="button" action="hide" class="ace_searchbtn_close">
- <i class="fa fa-fw fa-times"></i>
- </a>
- <div class="ace_search_form">
- <input class="ace_search_field form-control input-sm" placeholder="Search for" spellcheck="false"></input>
- <div class="btn-group">
- <button type="button" action="findNext" class="ace_searchbtn next btn btn-default btn-sm">
- <i class="fa fa-chevron-down fa-fw"></i>
- </button>
- <button type="button" action="findPrev" class="ace_searchbtn prev btn btn-default btn-sm">
- <i class="fa fa-chevron-up fa-fw"></i>
- </button>
- </div>
- </div>
- <div class="ace_replace_form">
- <input class="ace_search_field form-control input-sm" placeholder="Replace with" spellcheck="false"></input>
- <div class="btn-group">
- <button type="button" action="replaceAndFindNext" class="ace_replacebtn btn btn-default btn-sm">Replace</button>
- <button type="button" action="replaceAll" class="ace_replacebtn btn btn-default btn-sm">All</button>
- </div>
- </div>
- <div class="ace_search_options">
- <div class="btn-group">
- <button action="toggleRegexpMode" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="RegExp Search">.*</button>
- <button action="toggleCaseSensitive" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="CaseSensitive Search">Aa</button>
- <button action="toggleWholeWords" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Whole Word Search">"..."</button>
- <button action="searchInSelection" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Search Within Selection"><i class="fa fa-align-left"></i></button>
- </div>
- <span class="ace_search_counter"></span>
- </div>
- <div action="toggleReplace" class="hidden"></div>
- </div>\
- `
- // 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)
- }
- }
|