aceEditor.js 34 KB

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