aceEditor.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. import _ from 'lodash'
  2. /* eslint-disable
  3. camelcase,
  4. max-len
  5. */
  6. import App from '../../../base'
  7. import UndoManager from './aceEditor/undo/UndoManager'
  8. import AutoCompleteManager from './aceEditor/auto-complete/AutoCompleteManager'
  9. import SpellCheckManager from './aceEditor/spell-check/SpellCheckManager'
  10. import SpellCheckAdapter from './aceEditor/spell-check/SpellCheckAdapter'
  11. import HighlightsManager from './aceEditor/highlights/HighlightsManager'
  12. import CursorPositionManager from './aceEditor/cursor-position/CursorPositionManager'
  13. import CursorPositionAdapter from './aceEditor/cursor-position/CursorPositionAdapter'
  14. import TrackChangesManager from './aceEditor/track-changes/TrackChangesManager'
  15. import TrackChangesAdapter from './aceEditor/track-changes/TrackChangesAdapter'
  16. import MetadataManager from './aceEditor/metadata/MetadataManager'
  17. import 'ace/ace'
  18. import 'ace/ext-searchbox'
  19. import 'ace/ext-modelist'
  20. import 'ace/keybinding-vim'
  21. import '../../metadata/services/metadata'
  22. import '../../graphics/services/graphics'
  23. import '../../preamble/services/preamble'
  24. import '../../files/services/files'
  25. let syntaxValidationEnabled
  26. const { EditSession } = ace.require('ace/edit_session')
  27. const ModeList = ace.require('ace/ext/modelist')
  28. const { Vim } = ace.require('ace/keyboard/vim')
  29. const SearchBox = ace.require('ace/ext/searchbox')
  30. // Set the base path that ace will fetch modes/snippets/workers from
  31. if (window.aceBasePath !== '') {
  32. syntaxValidationEnabled = true
  33. ace.config.set('basePath', `${window.aceBasePath}`)
  34. } else {
  35. syntaxValidationEnabled = false
  36. }
  37. // By default, don't use workers - enable them per-session as required
  38. ace.config.setDefaultValue('session', 'useWorker', false)
  39. // Ace loads its script itself, so we need to hook in to be able to clear
  40. // the cache.
  41. if (ace.config._moduleUrl == null) {
  42. ace.config._moduleUrl = ace.config.moduleUrl
  43. ace.config.moduleUrl = function(...args) {
  44. const url = ace.config._moduleUrl(...Array.from(args || []))
  45. return url
  46. }
  47. }
  48. App.directive('aceEditor', function(
  49. ide,
  50. $timeout,
  51. $compile,
  52. $rootScope,
  53. eventTracking,
  54. localStorage,
  55. $cacheFactory,
  56. metadata,
  57. graphics,
  58. preamble,
  59. files,
  60. $http,
  61. $q,
  62. $window
  63. ) {
  64. monkeyPatchSearch($rootScope, $compile)
  65. return {
  66. scope: {
  67. theme: '=',
  68. showPrintMargin: '=',
  69. keybindings: '=',
  70. fontSize: '=',
  71. autoComplete: '=',
  72. autoPairDelimiters: '=',
  73. sharejsDoc: '=',
  74. spellCheck: '=',
  75. spellCheckLanguage: '=',
  76. highlights: '=',
  77. text: '=',
  78. readOnly: '=',
  79. annotations: '=',
  80. navigateHighlights: '=',
  81. fileName: '=',
  82. onCtrlEnter: '=', // Compile
  83. onCtrlJ: '=', // Toggle the review panel
  84. onCtrlShiftC: '=', // Add a new comment
  85. onCtrlShiftA: '=', // Toggle track-changes on/off
  86. onSave: '=', // Cmd/Ctrl-S or :w in Vim
  87. syntaxValidation: '=',
  88. reviewPanel: '=',
  89. eventsBridge: '=',
  90. trackChanges: '=',
  91. docId: '=',
  92. rendererData: '=',
  93. lineHeight: '=',
  94. fontFamily: '='
  95. },
  96. link(scope, element, attrs) {
  97. // Don't freak out if we're already in an apply callback
  98. let spellCheckManager
  99. scope.$originalApply = scope.$apply
  100. scope.$apply = function(fn) {
  101. if (fn == null) {
  102. fn = function() {}
  103. }
  104. const phase = this.$root.$$phase
  105. if (phase === '$apply' || phase === '$digest') {
  106. return fn()
  107. } else {
  108. return this.$originalApply(fn)
  109. }
  110. }
  111. const editor = ace.edit(element.find('.ace-editor-body')[0])
  112. editor.$blockScrolling = Infinity
  113. // Besides the main editor, other elements will re-use this directive
  114. // for displaying read-only content -- e.g. the history panes.
  115. const editorAcceptsChanges = attrs.aceEditor === 'editor'
  116. if (editorAcceptsChanges) {
  117. // end-to-end check for edits -> acks, globally on any doc
  118. // This may catch a missing attached ShareJsDoc that in turn bails out
  119. // on missing acks.
  120. ide.globalEditorWatchdogManager.attachToEditor('Ace', editor)
  121. }
  122. // auto-insertion of braces, brackets, dollars
  123. editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false)
  124. editor.setOption('wrapBehavioursEnabled', false)
  125. scope.$watch('autoPairDelimiters', autoPairDelimiters => {
  126. if (autoPairDelimiters) {
  127. return editor.setOption('behavioursEnabled', true)
  128. } else {
  129. return editor.setOption('behavioursEnabled', false)
  130. }
  131. })
  132. if (!window._debug_editors) {
  133. window._debug_editors = []
  134. }
  135. window._debug_editors.push(editor)
  136. scope.name = attrs.aceEditor
  137. if (scope.spellCheck) {
  138. // only enable spellcheck when explicitly required
  139. spellCheckManager = new SpellCheckManager(
  140. scope,
  141. $cacheFactory,
  142. $http,
  143. $q,
  144. new SpellCheckAdapter(editor)
  145. )
  146. }
  147. /* eslint-disable no-unused-vars */
  148. const undoManager = new UndoManager(editor)
  149. const highlightsManager = new HighlightsManager(scope, editor, element)
  150. const cursorPositionManager = new CursorPositionManager(
  151. scope,
  152. new CursorPositionAdapter(editor),
  153. localStorage
  154. )
  155. const trackChangesManager = new TrackChangesManager(
  156. scope,
  157. editor,
  158. element,
  159. new TrackChangesAdapter(editor)
  160. )
  161. const metadataManager = new MetadataManager(
  162. scope,
  163. editor,
  164. element,
  165. metadata
  166. )
  167. const autoCompleteManager = new AutoCompleteManager(
  168. scope,
  169. editor,
  170. element,
  171. metadataManager,
  172. graphics,
  173. preamble,
  174. files
  175. )
  176. // prevent user entering null and non-BMP unicode characters in Ace
  177. const BAD_CHARS_REGEXP = /[\0\uD800-\uDFFF]/g
  178. const BAD_CHARS_REPLACEMENT_CHAR = '\uFFFD'
  179. // the 'exec' event fires for ace functions before they are executed.
  180. // you can modify the input or reject the event with e.preventDefault()
  181. editor.commands.on('exec', function(e) {
  182. // replace bad characters in paste content
  183. if (e.command && e.command.name === 'paste') {
  184. BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage
  185. if (e.args && BAD_CHARS_REGEXP.test(e.args.text)) {
  186. e.args.text = e.args.text.replace(
  187. BAD_CHARS_REGEXP,
  188. BAD_CHARS_REPLACEMENT_CHAR
  189. )
  190. }
  191. }
  192. // replace bad characters in keyboard input
  193. if (e.command && e.command.name === 'insertstring') {
  194. BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage
  195. if (e.args && BAD_CHARS_REGEXP.test(e.args)) {
  196. e.args = e.args.replace(
  197. BAD_CHARS_REGEXP,
  198. BAD_CHARS_REPLACEMENT_CHAR
  199. )
  200. }
  201. }
  202. })
  203. /* eslint-enable no-unused-vars */
  204. scope.$watch('onSave', function(callback) {
  205. if (callback != null) {
  206. Vim.defineEx('write', 'w', callback)
  207. editor.commands.addCommand({
  208. name: 'save',
  209. bindKey: {
  210. win: 'Ctrl-S',
  211. mac: 'Command-S'
  212. },
  213. exec: callback,
  214. readOnly: true
  215. })
  216. // Not technically 'save', but Ctrl-. recompiles in OL v1
  217. // so maintain compatibility
  218. return editor.commands.addCommand({
  219. name: 'recompile_v1',
  220. bindKey: {
  221. win: 'Ctrl-.',
  222. mac: 'Ctrl-.'
  223. },
  224. exec: callback,
  225. readOnly: true
  226. })
  227. }
  228. })
  229. editor.commands.removeCommand('transposeletters')
  230. editor.commands.removeCommand('showSettingsMenu')
  231. editor.commands.removeCommand('foldall')
  232. // For European keyboards, the / is above 7 so needs Shift pressing.
  233. // This comes through as Command-Shift-/ on OS X, which is mapped to
  234. // toggleBlockComment.
  235. // This doesn't do anything for LaTeX, so remap this to togglecomment to
  236. // work for European keyboards as normal.
  237. // On Windows, the key combo comes as Ctrl-Shift-7.
  238. editor.commands.removeCommand('toggleBlockComment')
  239. editor.commands.removeCommand('togglecomment')
  240. editor.commands.addCommand({
  241. name: 'togglecomment',
  242. bindKey: {
  243. win: 'Ctrl-/|Ctrl-Shift-7',
  244. mac: 'Command-/|Command-Shift-/'
  245. },
  246. exec(editor) {
  247. return editor.toggleCommentLines()
  248. },
  249. multiSelectAction: 'forEachLine',
  250. scrollIntoView: 'selectionPart'
  251. })
  252. // Trigger search AND replace on CMD+F
  253. editor.commands.addCommand({
  254. name: 'find',
  255. bindKey: {
  256. win: 'Ctrl-F',
  257. mac: 'Command-F'
  258. },
  259. exec(editor) {
  260. return SearchBox.Search(editor, true)
  261. },
  262. readOnly: true
  263. })
  264. // Bold text on CMD+B
  265. editor.commands.addCommand({
  266. name: 'bold',
  267. bindKey: {
  268. win: 'Ctrl-B',
  269. mac: 'Command-B'
  270. },
  271. exec(editor) {
  272. const selection = editor.getSelection()
  273. if (selection.isEmpty()) {
  274. editor.insert('\\textbf{}')
  275. return editor.navigateLeft(1)
  276. } else {
  277. const text = editor.getCopyText()
  278. return editor.insert(`\\textbf{${text}}`)
  279. }
  280. },
  281. readOnly: false
  282. })
  283. // Italicise text on CMD+I
  284. editor.commands.addCommand({
  285. name: 'italics',
  286. bindKey: {
  287. win: 'Ctrl-I',
  288. mac: 'Command-I'
  289. },
  290. exec(editor) {
  291. const selection = editor.getSelection()
  292. if (selection.isEmpty()) {
  293. editor.insert('\\textit{}')
  294. return editor.navigateLeft(1)
  295. } else {
  296. const text = editor.getCopyText()
  297. return editor.insert(`\\textit{${text}}`)
  298. }
  299. },
  300. readOnly: false
  301. })
  302. scope.$watch('onCtrlEnter', function(callback) {
  303. if (callback != null) {
  304. return editor.commands.addCommand({
  305. name: 'compile',
  306. bindKey: {
  307. win: 'Ctrl-Enter',
  308. mac: 'Command-Enter'
  309. },
  310. exec: editor => {
  311. return callback()
  312. },
  313. readOnly: true
  314. })
  315. }
  316. })
  317. scope.$watch('onCtrlJ', function(callback) {
  318. if (callback != null) {
  319. return editor.commands.addCommand({
  320. name: 'toggle-review-panel',
  321. bindKey: {
  322. win: 'Ctrl-J',
  323. mac: 'Command-J'
  324. },
  325. exec: editor => {
  326. return callback()
  327. },
  328. readOnly: true
  329. })
  330. }
  331. })
  332. scope.$watch('onCtrlShiftC', function(callback) {
  333. if (callback != null) {
  334. return editor.commands.addCommand({
  335. name: 'add-new-comment',
  336. bindKey: {
  337. win: 'Ctrl-Shift-C',
  338. mac: 'Command-Shift-C'
  339. },
  340. exec: editor => {
  341. return callback()
  342. },
  343. readOnly: true
  344. })
  345. }
  346. })
  347. scope.$watch('onCtrlShiftA', function(callback) {
  348. if (callback != null) {
  349. return editor.commands.addCommand({
  350. name: 'toggle-track-changes',
  351. bindKey: {
  352. win: 'Ctrl-Shift-A',
  353. mac: 'Command-Shift-A'
  354. },
  355. exec: editor => {
  356. return callback()
  357. },
  358. readOnly: true
  359. })
  360. }
  361. })
  362. // Make '/' work for search in vim mode.
  363. editor.showCommandLine = arg => {
  364. if (arg === '/') {
  365. return SearchBox.Search(editor, true)
  366. }
  367. }
  368. const getCursorScreenPosition = function() {
  369. const session = editor.getSession()
  370. const cursorPosition = session.selection.getCursor()
  371. const sessionPos = session.documentToScreenPosition(
  372. cursorPosition.row,
  373. cursorPosition.column
  374. )
  375. return (
  376. sessionPos.row * editor.renderer.lineHeight - session.getScrollTop()
  377. )
  378. }
  379. if (attrs.resizeOn != null) {
  380. for (let event of Array.from(attrs.resizeOn.split(','))) {
  381. scope.$on(event, function() {
  382. scope.$applyAsync(() => {
  383. const previousScreenPosition = getCursorScreenPosition()
  384. editor.resize()
  385. // Put cursor back to same vertical position on screen
  386. const newScreenPosition = getCursorScreenPosition()
  387. const session = editor.getSession()
  388. return session.setScrollTop(
  389. session.getScrollTop() +
  390. newScreenPosition -
  391. previousScreenPosition
  392. )
  393. })
  394. })
  395. }
  396. }
  397. scope.$on(`${scope.name}:set-scroll-size`, function(e, size) {
  398. // Make sure that the editor has enough scroll margin above and below
  399. // to scroll the review panel with the given size
  400. const marginTop = size.overflowTop
  401. const { maxHeight } = editor.renderer.layerConfig
  402. const marginBottom = Math.max(size.height - maxHeight, 0)
  403. return setScrollMargins(marginTop, marginBottom)
  404. })
  405. var setScrollMargins = function(marginTop, marginBottom) {
  406. let marginChanged = false
  407. if (editor.renderer.scrollMargin.top !== marginTop) {
  408. editor.renderer.scrollMargin.top = marginTop
  409. marginChanged = true
  410. }
  411. if (editor.renderer.scrollMargin.bottom !== marginBottom) {
  412. editor.renderer.scrollMargin.bottom = marginBottom
  413. marginChanged = true
  414. }
  415. if (marginChanged) {
  416. return editor.renderer.updateFull()
  417. }
  418. }
  419. const resetScrollMargins = () => setScrollMargins(0, 0)
  420. scope.$watch('theme', value => editor.setTheme(`ace/theme/${value}`))
  421. scope.$watch('showPrintMargin', value => editor.setShowPrintMargin(value))
  422. scope.$watch('keybindings', function(value) {
  423. if (['vim', 'emacs'].includes(value)) {
  424. return editor.setKeyboardHandler(`ace/keyboard/${value}`)
  425. } else {
  426. return editor.setKeyboardHandler(null)
  427. }
  428. })
  429. scope.$watch('fontSize', value =>
  430. element.find('.ace_editor, .ace_content').css({
  431. 'font-size': value + 'px'
  432. })
  433. )
  434. scope.$watch('fontFamily', function(value) {
  435. const monospaceFamilies = [
  436. 'Monaco',
  437. 'Menlo',
  438. 'Ubuntu Mono',
  439. 'Consolas',
  440. 'monospace'
  441. ]
  442. if (value != null) {
  443. switch (value) {
  444. case 'monaco':
  445. return editor.setOption(
  446. 'fontFamily',
  447. monospaceFamilies.join(', ')
  448. )
  449. case 'lucida':
  450. return editor.setOption(
  451. 'fontFamily',
  452. '"Lucida Console", "Source Code Pro", monospace'
  453. )
  454. default:
  455. return editor.setOption('fontFamily', null)
  456. }
  457. }
  458. })
  459. scope.$watch('lineHeight', function(value) {
  460. if (value != null) {
  461. switch (value) {
  462. case 'compact':
  463. editor.container.style.lineHeight = 1.33
  464. break
  465. case 'normal':
  466. editor.container.style.lineHeight = 1.6
  467. break
  468. case 'wide':
  469. editor.container.style.lineHeight = 2
  470. break
  471. default:
  472. editor.container.style.lineHeight = 1.6
  473. }
  474. return editor.renderer.updateFontSize()
  475. }
  476. })
  477. scope.$watch('sharejsDoc', function(sharejs_doc, old_sharejs_doc) {
  478. if (old_sharejs_doc != null) {
  479. scope.$broadcast('beforeChangeDocument')
  480. detachFromAce(old_sharejs_doc)
  481. }
  482. if (sharejs_doc != null) {
  483. attachToAce(sharejs_doc)
  484. }
  485. if (sharejs_doc != null && old_sharejs_doc != null) {
  486. return scope.$broadcast('afterChangeDocument')
  487. }
  488. })
  489. scope.$watch('text', function(text) {
  490. if (text != null) {
  491. editor.setValue(text, -1)
  492. const session = editor.getSession()
  493. return session.setUseWrapMode(true)
  494. }
  495. })
  496. scope.$watch('annotations', function(annotations) {
  497. const session = editor.getSession()
  498. return session.setAnnotations(annotations)
  499. })
  500. scope.$watch('readOnly', value => editor.setReadOnly(!!value))
  501. scope.$watch('syntaxValidation', function(value) {
  502. // ignore undefined settings here
  503. // only instances of ace with an explicit value should set useWorker
  504. // the history instance will have syntaxValidation undefined
  505. if (value != null && syntaxValidationEnabled) {
  506. const session = editor.getSession()
  507. return session.setOption('useWorker', value)
  508. }
  509. })
  510. editor.setOption('scrollPastEnd', true)
  511. let updateCount = 0
  512. const onChange = function() {
  513. updateCount++
  514. if (updateCount === 100) {
  515. eventTracking.send('editor-interaction', 'multi-doc-update')
  516. }
  517. return scope.$emit(`${scope.name}:change`)
  518. }
  519. let currentFirstVisibleRow = null
  520. const emitMiddleVisibleRowChanged = () => {
  521. const firstVisibleRow = editor.getFirstVisibleRow()
  522. if (firstVisibleRow === currentFirstVisibleRow) return
  523. currentFirstVisibleRow = firstVisibleRow
  524. const lastVisibleRow = editor.getLastVisibleRow()
  525. scope.$emit(
  526. `scroll:editor:update`,
  527. Math.floor((firstVisibleRow + lastVisibleRow) / 2)
  528. )
  529. }
  530. const onScroll = function(scrollTop) {
  531. if (scope.eventsBridge == null) {
  532. return
  533. }
  534. const height = editor.renderer.layerConfig.maxHeight
  535. emitMiddleVisibleRowChanged()
  536. return scope.eventsBridge.emit('aceScroll', scrollTop, height)
  537. }
  538. const onScrollbarVisibilityChanged = function(event, vRenderer) {
  539. if (scope.eventsBridge == null) {
  540. return
  541. }
  542. return scope.eventsBridge.emit(
  543. 'aceScrollbarVisibilityChanged',
  544. vRenderer.scrollBarV.isVisible,
  545. vRenderer.scrollBarV.width
  546. )
  547. }
  548. if (scope.eventsBridge != null) {
  549. editor.renderer.on(
  550. 'scrollbarVisibilityChanged',
  551. onScrollbarVisibilityChanged
  552. )
  553. scope.eventsBridge.on('externalScroll', position =>
  554. editor.getSession().setScrollTop(position)
  555. )
  556. scope.eventsBridge.on('refreshScrollPosition', function() {
  557. const session = editor.getSession()
  558. session.setScrollTop(session.getScrollTop() + 1)
  559. return session.setScrollTop(session.getScrollTop() - 1)
  560. })
  561. }
  562. const onSessionChangeForSpellCheck = function(e) {
  563. spellCheckManager.onSessionChange()
  564. if (e.oldSession != null) {
  565. e.oldSession.getDocument().off('change', spellCheckManager.onChange)
  566. }
  567. e.session.getDocument().on('change', spellCheckManager.onChange)
  568. if (e.oldSession != null) {
  569. e.oldSession.off('changeScrollTop', spellCheckManager.onScroll)
  570. }
  571. return e.session.on('changeScrollTop', spellCheckManager.onScroll)
  572. }
  573. const initSpellCheck = function() {
  574. if (!spellCheckManager) return
  575. spellCheckManager.init()
  576. editor.on('changeSession', onSessionChangeForSpellCheck)
  577. onSessionChangeForSpellCheck({
  578. session: editor.getSession()
  579. }) // Force initial setup
  580. return editor.on('nativecontextmenu', spellCheckManager.onContextMenu)
  581. }
  582. const tearDownSpellCheck = function() {
  583. if (!spellCheckManager) return
  584. editor.off('changeSession', onSessionChangeForSpellCheck)
  585. return editor.off('nativecontextmenu', spellCheckManager.onContextMenu)
  586. }
  587. const initTrackChanges = function() {
  588. trackChangesManager.rangesTracker = scope.sharejsDoc.ranges
  589. // Force onChangeSession in order to set up highlights etc.
  590. trackChangesManager.onChangeSession()
  591. if (!trackChangesManager) return
  592. editor.on('changeSelection', trackChangesManager.onChangeSelection)
  593. // Selection also moves with updates elsewhere in the document
  594. editor.on('change', trackChangesManager.onChangeSelection)
  595. editor.on('changeSession', trackChangesManager.onChangeSession)
  596. editor.on('cut', trackChangesManager.onCut)
  597. editor.on('paste', trackChangesManager.onPaste)
  598. editor.renderer.on('resize', trackChangesManager.onResize)
  599. }
  600. const tearDownTrackChanges = function() {
  601. if (!trackChangesManager) return
  602. trackChangesManager.tearDown()
  603. editor.off('changeSelection', trackChangesManager.onChangeSelection)
  604. editor.off('change', trackChangesManager.onChangeSelection)
  605. editor.off('changeSession', trackChangesManager.onChangeSession)
  606. editor.off('cut', trackChangesManager.onCut)
  607. editor.off('paste', trackChangesManager.onPaste)
  608. editor.renderer.off('resize', trackChangesManager.onResize)
  609. }
  610. const initUndo = function() {
  611. // Emulate onChangeSession event. Note: listening to changeSession
  612. // event is unnecessary since this method is called when we switch
  613. // sessions (via ShareJS changing) anyway
  614. undoManager.onChangeSession(editor.getSession())
  615. editor.on('change', undoManager.onChange)
  616. }
  617. const tearDownUndo = function() {
  618. editor.off('change', undoManager.onChange)
  619. }
  620. const onSessionChangeForCursorPosition = function(e) {
  621. if (e.oldSession != null) {
  622. e.oldSession.selection.off(
  623. 'changeCursor',
  624. cursorPositionManager.onCursorChange
  625. )
  626. }
  627. return e.session.selection.on(
  628. 'changeCursor',
  629. cursorPositionManager.onCursorChange
  630. )
  631. }
  632. const onUnloadForCursorPosition = () =>
  633. cursorPositionManager.onUnload(editor.getSession())
  634. const initCursorPosition = function() {
  635. editor.on('changeSession', onSessionChangeForCursorPosition)
  636. // Force initial setup
  637. onSessionChangeForCursorPosition({ session: editor.getSession() })
  638. return $(window).on('unload', onUnloadForCursorPosition)
  639. }
  640. const tearDownCursorPosition = function() {
  641. editor.off('changeSession', onSessionChangeForCursorPosition)
  642. return $(window).off('unload', onUnloadForCursorPosition)
  643. }
  644. initCursorPosition()
  645. // Trigger the event once *only* - this is called after Ace is connected
  646. // to the ShareJs instance but this event should only be triggered the
  647. // first time the editor is opened. Not every time the docs opened
  648. const triggerEditorInitEvent = _.once(() =>
  649. scope.$broadcast('editorInit')
  650. )
  651. var attachToAce = function(sharejs_doc) {
  652. let mode
  653. const lines = sharejs_doc.getSnapshot().split('\n')
  654. let session = editor.getSession()
  655. if (session != null) {
  656. session.destroy()
  657. }
  658. // see if we can lookup a suitable mode from ace
  659. // but fall back to text by default
  660. try {
  661. if (/\.(Rtex|bbl|tikz)$/i.test(scope.fileName)) {
  662. // recognise Rtex and bbl as latex
  663. mode = 'ace/mode/latex'
  664. } else if (/\.(sty|cls|clo)$/.test(scope.fileName)) {
  665. // recognise some common files as tex
  666. mode = 'ace/mode/tex'
  667. } else {
  668. ;({ mode } = ModeList.getModeForPath(scope.fileName))
  669. // we prefer plain_text mode over text mode because ace's
  670. // text mode is actually for code and has unwanted
  671. // indenting (see wrapMethod in ace edit_session.js)
  672. if (mode === 'ace/mode/text') {
  673. mode = 'ace/mode/plain_text'
  674. }
  675. }
  676. } catch (error) {
  677. mode = 'ace/mode/plain_text'
  678. }
  679. // create our new session
  680. session = new EditSession(lines, mode)
  681. session.setUseWrapMode(true)
  682. // use syntax validation only when explicitly set
  683. if (
  684. scope.syntaxValidation != null &&
  685. syntaxValidationEnabled &&
  686. !/\.bib$/.test(scope.fileName)
  687. ) {
  688. session.setOption('useWorker', scope.syntaxValidation)
  689. }
  690. // set to readonly until document change handlers are attached
  691. editor.setReadOnly(true)
  692. // now attach session to editor
  693. editor.setSession(session)
  694. const doc = session.getDocument()
  695. doc.on('change', onChange)
  696. editor.initing = true
  697. sharejs_doc.attachToAce(editor)
  698. editor.initing = false
  699. // now ready to edit document
  700. // respect the readOnly setting, normally false
  701. editor.setReadOnly(scope.readOnly)
  702. triggerEditorInitEvent()
  703. if (!scope.readOnly) {
  704. initSpellCheck()
  705. }
  706. initTrackChanges()
  707. initUndo()
  708. resetScrollMargins()
  709. // need to set annotations after attaching because attaching
  710. // deletes and then inserts document content
  711. session.setAnnotations(scope.annotations)
  712. session.on('changeScrollTop', eventTracking.editingSessionHeartbeat)
  713. angular
  714. .element($window)
  715. .on('click', eventTracking.editingSessionHeartbeat)
  716. scope.$on('$destroy', () =>
  717. angular
  718. .element($window)
  719. .off('click', eventTracking.editingSessionHeartbeat)
  720. )
  721. if (scope.eventsBridge != null) {
  722. session.on('changeScrollTop', onScroll)
  723. }
  724. $rootScope.hasLintingError = false
  725. session.on('changeAnnotation', function() {
  726. // Both linter errors and compile logs are set as error annotations,
  727. // however when the user types something, the compile logs are
  728. // replaced with linter errors. When we check for lint errors before
  729. // autocompile we are guaranteed to get linter errors
  730. const hasErrors =
  731. session
  732. .getAnnotations()
  733. .filter(annotation => annotation.type !== 'info').length > 0
  734. if ($rootScope.hasLintingError !== hasErrors) {
  735. return ($rootScope.hasLintingError = hasErrors)
  736. }
  737. })
  738. setTimeout(() =>
  739. // Let any listeners init themselves
  740. onScroll(editor.renderer.getScrollTop())
  741. )
  742. return editor.focus()
  743. }
  744. var detachFromAce = function(sharejs_doc) {
  745. tearDownSpellCheck()
  746. tearDownTrackChanges()
  747. tearDownUndo()
  748. sharejs_doc.detachFromAce()
  749. sharejs_doc.off('remoteop.recordRemote')
  750. const session = editor.getSession()
  751. session.off('changeScrollTop')
  752. const doc = session.getDocument()
  753. return doc.off('change', onChange)
  754. }
  755. if (scope.rendererData != null) {
  756. editor.renderer.on('changeCharacterSize', () => {
  757. scope.$apply(
  758. () => (scope.rendererData.lineHeight = editor.renderer.lineHeight)
  759. )
  760. })
  761. }
  762. scope.$watch('rendererData', function(rendererData) {
  763. if (rendererData != null) {
  764. return (rendererData.lineHeight = editor.renderer.lineHeight)
  765. }
  766. })
  767. scope.$on('$destroy', function() {
  768. if (scope.sharejsDoc != null) {
  769. scope.$broadcast('changeEditor')
  770. tearDownSpellCheck()
  771. tearDownCursorPosition()
  772. tearDownUndo()
  773. detachFromAce(scope.sharejsDoc)
  774. const session = editor.getSession()
  775. if (session != null) {
  776. session.destroy()
  777. }
  778. return scope.eventsBridge.emit(
  779. 'aceScrollbarVisibilityChanged',
  780. false,
  781. 0
  782. )
  783. }
  784. })
  785. return scope.$emit(`${scope.name}:inited`, editor)
  786. },
  787. template: `\
  788. <div class="ace-editor-wrapper">
  789. <div
  790. class="undo-conflict-warning alert alert-danger small"
  791. ng-show="undo.show_remote_warning"
  792. >
  793. <strong>Watch out!</strong>
  794. We had to undo some of your collaborators changes before we could undo yours.
  795. <a
  796. href="#"
  797. class="pull-right"
  798. ng-click="undo.show_remote_warning = false"
  799. >Dismiss</a>
  800. </div>
  801. <div class="ace-editor-body"></div>
  802. <spell-menu
  803. open="spellMenu.open"
  804. top="spellMenu.top"
  805. left="spellMenu.left"
  806. layout-from-bottom="spellMenu.layoutFromBottom"
  807. highlight="spellMenu.highlight"
  808. replace-word="replaceWord(highlight, suggestion)"
  809. learn-word="learnWord(highlight)"
  810. ></spell-menu>
  811. <div
  812. class="annotation-label"
  813. ng-show="annotationLabel.show"
  814. ng-style="{
  815. position: 'absolute',
  816. left: annotationLabel.left,
  817. right: annotationLabel.right,
  818. bottom: annotationLabel.bottom,
  819. top: annotationLabel.top,
  820. 'background-color': annotationLabel.backgroundColor
  821. }"
  822. >
  823. {{ annotationLabel.text }}
  824. </div>
  825. <a
  826. href
  827. class="highlights-before-label btn btn-info btn-xs"
  828. ng-show="updateLabels.highlightsBefore > 0"
  829. ng-click="gotoHighlightAbove()"
  830. >
  831. <i class="fa fa-fw fa-arrow-up"></i>
  832. {{ updateLabels.highlightsBefore }} more update{{ updateLabels.highlightsBefore > 1 && "" || "s" }} above
  833. </a>
  834. <a
  835. href
  836. class="highlights-after-label btn btn-info btn-xs"
  837. ng-show="updateLabels.highlightsAfter > 0"
  838. ng-click="gotoHighlightBelow()"
  839. >
  840. <i class="fa fa-fw fa-arrow-down"></i>
  841. {{ updateLabels.highlightsAfter }} more update{{ updateLabels.highlightsAfter > 1 && "" || "s" }} below
  842. </a>
  843. </div>\
  844. `
  845. }
  846. })
  847. function monkeyPatchSearch($rootScope, $compile) {
  848. const searchHtml = `\
  849. <div class="ace_search right">
  850. <a href type="button" action="hide" class="ace_searchbtn_close">
  851. <i class="fa fa-fw fa-times"></i>
  852. </a>
  853. <div class="ace_search_form">
  854. <input class="ace_search_field form-control input-sm" placeholder="Search for" spellcheck="false"></input>
  855. <div class="btn-group">
  856. <button type="button" action="findNext" class="ace_searchbtn next btn btn-default btn-sm">
  857. <i class="fa fa-chevron-down fa-fw"></i>
  858. </button>
  859. <button type="button" action="findPrev" class="ace_searchbtn prev btn btn-default btn-sm">
  860. <i class="fa fa-chevron-up fa-fw"></i>
  861. </button>
  862. </div>
  863. </div>
  864. <div class="ace_replace_form">
  865. <input class="ace_search_field form-control input-sm" placeholder="Replace with" spellcheck="false"></input>
  866. <div class="btn-group">
  867. <button type="button" action="replaceAndFindNext" class="ace_replacebtn btn btn-default btn-sm">Replace</button>
  868. <button type="button" action="replaceAll" class="ace_replacebtn btn btn-default btn-sm">All</button>
  869. </div>
  870. </div>
  871. <div class="ace_search_options">
  872. <div class="btn-group">
  873. <button action="toggleRegexpMode" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="RegExp Search">.*</button>
  874. <button action="toggleCaseSensitive" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="CaseSensitive Search">Aa</button>
  875. <button action="toggleWholeWords" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Whole Word Search">"..."</button>
  876. <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>
  877. </div>
  878. <span class="ace_search_counter"></span>
  879. </div>
  880. <div action="toggleReplace" class="hidden"></div>
  881. </div>\
  882. `
  883. // Remove Ace CSS
  884. $('#ace_searchbox').remove()
  885. const SB = SearchBox.SearchBox
  886. const { $init } = SB.prototype
  887. SB.prototype.$init = function() {
  888. this.element = $compile(searchHtml)($rootScope.$new())[0]
  889. return $init.apply(this)
  890. }
  891. }