line-numbers.ts 2.5 KB

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