aceEditor.js 33 KB

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