aceEditor.js 32 KB

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