aceEditor.js 33 KB

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