aceEditor.js 33 KB

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