aceEditor.js 33 KB

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