aceEditor.js 31 KB

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