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