match-highlighter.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: http://codemirror.net/LICENSE
  3. // Highlighting text that matches the selection
  4. //
  5. // Defines an option highlightSelectionMatches, which, when enabled,
  6. // will style strings that match the selection throughout the
  7. // document.
  8. //
  9. // The option can be set to true to simply enable it, or to a
  10. // {minChars, style, wordsOnly, showToken, delay} object to explicitly
  11. // configure it. minChars is the minimum amount of characters that should be
  12. // selected for the behavior to occur, and style is the token style to
  13. // apply to the matches. This will be prefixed by "cm-" to create an
  14. // actual CSS class name. If wordsOnly is enabled, the matches will be
  15. // highlighted only if the selected text is a word. showToken, when enabled,
  16. // will cause the current token to be highlighted when nothing is selected.
  17. // delay is used to specify how much time to wait, in milliseconds, before
  18. // highlighting the matches.
  19. (function(mod) {
  20. if (typeof exports == "object" && typeof module == "object") // CommonJS
  21. mod(require("../../lib/codemirror"));
  22. else if (typeof define == "function" && define.amd) // AMD
  23. define(["../../lib/codemirror"], mod);
  24. else // Plain browser env
  25. mod(CodeMirror);
  26. })(function(CodeMirror) {
  27. "use strict";
  28. var DEFAULT_MIN_CHARS = 2;
  29. var DEFAULT_TOKEN_STYLE = "matchhighlight";
  30. var DEFAULT_DELAY = 100;
  31. var DEFAULT_WORDS_ONLY = false;
  32. function State(options) {
  33. if (typeof options == "object") {
  34. this.minChars = options.minChars;
  35. this.style = options.style;
  36. this.showToken = options.showToken;
  37. this.delay = options.delay;
  38. this.wordsOnly = options.wordsOnly;
  39. }
  40. if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
  41. if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
  42. if (this.delay == null) this.delay = DEFAULT_DELAY;
  43. if (this.wordsOnly == null) this.wordsOnly = DEFAULT_WORDS_ONLY;
  44. this.overlay = this.timeout = null;
  45. }
  46. CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
  47. if (old && old != CodeMirror.Init) {
  48. var over = cm.state.matchHighlighter.overlay;
  49. if (over) cm.removeOverlay(over);
  50. clearTimeout(cm.state.matchHighlighter.timeout);
  51. cm.state.matchHighlighter = null;
  52. cm.off("cursorActivity", cursorActivity);
  53. }
  54. if (val) {
  55. cm.state.matchHighlighter = new State(val);
  56. highlightMatches(cm);
  57. cm.on("cursorActivity", cursorActivity);
  58. }
  59. });
  60. function cursorActivity(cm) {
  61. var state = cm.state.matchHighlighter;
  62. clearTimeout(state.timeout);
  63. state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
  64. }
  65. function highlightMatches(cm) {
  66. cm.operation(function() {
  67. var state = cm.state.matchHighlighter;
  68. if (state.overlay) {
  69. cm.removeOverlay(state.overlay);
  70. state.overlay = null;
  71. }
  72. if (!cm.somethingSelected() && state.showToken) {
  73. var re = state.showToken === true ? /[\w$]/ : state.showToken;
  74. var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
  75. while (start && re.test(line.charAt(start - 1))) --start;
  76. while (end < line.length && re.test(line.charAt(end))) ++end;
  77. if (start < end)
  78. cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style));
  79. return;
  80. }
  81. var from = cm.getCursor("from"), to = cm.getCursor("to");
  82. if (from.line != to.line) return;
  83. if (state.wordsOnly && !isWord(cm, from, to)) return;
  84. var selection = cm.getRange(from, to).replace(/^\s+|\s+$/g, "");
  85. if (selection.length >= state.minChars)
  86. cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style));
  87. });
  88. }
  89. function isWord(cm, from, to) {
  90. var str = cm.getRange(from, to);
  91. if (str.match(/^\w+$/) !== null) {
  92. if (from.ch > 0) {
  93. var pos = {line: from.line, ch: from.ch - 1};
  94. var chr = cm.getRange(pos, from);
  95. if (chr.match(/\W/) === null) return false;
  96. }
  97. if (to.ch < cm.getLine(from.line).length) {
  98. var pos = {line: to.line, ch: to.ch + 1};
  99. var chr = cm.getRange(to, pos);
  100. if (chr.match(/\W/) === null) return false;
  101. }
  102. return true;
  103. } else return false;
  104. }
  105. function boundariesAround(stream, re) {
  106. return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
  107. (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
  108. }
  109. function makeOverlay(query, hasBoundary, style) {
  110. return {token: function(stream) {
  111. if (stream.match(query) &&
  112. (!hasBoundary || boundariesAround(stream, hasBoundary)))
  113. return style;
  114. stream.next();
  115. stream.skipTo(query.charAt(0)) || stream.skipToEnd();
  116. }};
  117. }
  118. });