line-numbers.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { EditorSelection, Extension } from '@codemirror/state'
  2. import {
  3. BlockInfo,
  4. EditorView,
  5. lineNumbers as _lineNumbers,
  6. } from '@codemirror/view'
  7. import { DebouncedFunc, throttle } from 'lodash'
  8. /**
  9. * The built-in extension which displays line numbers in the gutter,
  10. * configured with a mousedown/mouseup handler that selects lines of the document
  11. * when dragging a selection in the gutter.
  12. */
  13. export function lineNumbers(): Extension {
  14. let listener: DebouncedFunc<(event: MouseEvent) => boolean> | null
  15. function disableListener() {
  16. if (listener) {
  17. document.removeEventListener('mousemove', listener)
  18. listener = null
  19. }
  20. }
  21. // Creates a selection range capped within the document bounds. The range is
  22. // anchored at the beginning so that it is a full line that is selected
  23. function selection(view: EditorView, start: BlockInfo, end: BlockInfo) {
  24. const clamp = (num: number) =>
  25. Math.max(0, Math.min(view.state.doc.length, num))
  26. let startPos = start.from
  27. let endPos = end.to + 1
  28. if (start.from === end.from) {
  29. // Selecting one line
  30. startPos = end.to + 1
  31. endPos = start.from
  32. } else if (end.from < start.from) {
  33. // End is prior to start
  34. endPos = end.from
  35. startPos = start.to + 1
  36. }
  37. return EditorSelection.range(clamp(startPos), clamp(endPos))
  38. }
  39. // Wrapper around the built-in codemirror lineNumbers() extension
  40. return _lineNumbers({
  41. domEventHandlers: {
  42. mousedown: (view, line, event) => {
  43. // Disable default focusing of line number
  44. event.preventDefault()
  45. // If we already have a listener, disable it
  46. disableListener()
  47. view.dispatch({
  48. selection: selection(view, line, line),
  49. })
  50. // Focus the editor
  51. view.contentDOM.focus()
  52. // Set up new listener to track the mouse position
  53. listener = throttle((event: MouseEvent) => {
  54. // Check if we've missed a mouseup event by validating that the
  55. // primary mouse button is still being held
  56. if (event.buttons !== 1) {
  57. disableListener()
  58. return false
  59. }
  60. // Map the mouse cursor to the document, and select the lines matched
  61. const documentPosition = view.posAtCoords({
  62. x: event.pageX,
  63. y: event.pageY,
  64. })
  65. if (documentPosition) {
  66. const endLine = view.lineBlockAt(documentPosition)
  67. view.dispatch({
  68. selection: selection(view, line, endLine),
  69. })
  70. }
  71. }, 50)
  72. document.addEventListener('mousemove', listener)
  73. return false
  74. },
  75. mouseup: () => {
  76. disableListener()
  77. return false
  78. },
  79. },
  80. })
  81. }