Kyle Kelley Kyle Kelley - 1 month ago 12
Javascript Question

How can I search across multiple codemirror instances?

Let's say I have the following simple page with two CodeMirror instances:

const body = document.querySelector('body')

const title = document.createElement('h1')
title.textContent = 'This is a document with multiple CodeMirrors'
body.appendChild(title);

const area1 = document.createElement('textarea')
body.appendChild(area1)
const editor1 = CodeMirror.fromTextArea(area1, {
lineNumbers: true,
})

const segway = document.createElement('h2')
segway.textContent = 'Moving on to another editor'
body.appendChild(segway)


const area2 = document.createElement('textarea')
body.appendChild(area2)
const editor2 = CodeMirror.fromTextArea(area2, {
lineNumbers: true,
})


and that I've included


  • codemirror/addon/search/search

  • codemirror/addon/search/searchcursor

  • codemirror/addon/dialog/dialog



Each CodeMirror instance now has their own search handler when focused on the editor (triggered via ctrl/cmd-f). How could I implement search/replace that works across multiple CodeMirror instances?

There's at least a way to execute a
find
on each editor:
editor.execCommand
. I'm not seeing a way to pass through to it, or to query about what results are available.

CodePen with example code and imports

GitHub issue for project wanting to use this, nteract.

In CodeMirror issue Marijn states "You'll have to code that up yourself.", which is fair -- I'm unsure about how to approach this.

Answer

The find and replace commands are linked to the dialog addons and there doesn't seem to be a way to access them through the instances, at least not with a query that is not passed through a dialog.

But you can recuperate most of what's in search.js and add it as an extension, to which you can pass a query. But then you'll need to set up a global dialog or a way to get a query that is not instance dependent, and run it on each instance. Something like that should work, this is only for searching, but replacing should be easy as well:

CodeMirror.defineExtension('search', function(query) {

  // This is all taken from search.js, pretty much as is for the first part.
  function SearchState() {
    this.posFrom = this.posTo = this.lastQuery = this.query = null;
    this.overlay = null;
  }

  function getSearchState(cm) {
    return cm.state.search || (cm.state.search = new SearchState());
  }

  function searchOverlay(query, caseInsensitive) {
    if (typeof query == "string")
      query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
    else if (!query.global)
      query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");

    return {
      token: function(stream) {
        query.lastIndex = stream.pos;
        var match = query.exec(stream.string);
        if (match && match.index == stream.pos) {
          stream.pos += match[0].length || 1;
          return "searching";
        } else if (match) {
          stream.pos = match.index;
        } else {
          stream.skipToEnd();
        }
      }
    };
  }

  function queryCaseInsensitive(query) {
    return typeof query == "string" && query == query.toLowerCase();
  }

  function parseString(string) {
    return string.replace(/\\(.)/g, function(_, ch) {
      if (ch == "n") return "\n"
      if (ch == "r") return "\r"
      return ch
    })
  }

  function parseQuery(query) {
    var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
    if (isRE) {
      try {
        query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i");
      } catch (e) {} // Not a regular expression after all, do a string search
    } else {
      query = parseString(query)
    }
    if (typeof query == "string" ? query == "" : query.test(""))
      query = /x^/;
    return query;
  }

  // From here it's still from search.js, but a bit tweaked so it applies
  // as an extension, these are basically clearSearch and startSearch.
  var state = getSearchState(this);
  state.lastQuery = state.query;
  state.query = state.queryText = null;
  this.removeOverlay(state.overlay);
  if (state.annotate) {
    state.annotate.clear();
    state.annotate = null;
  }

  state.queryText = query;
  state.query = parseQuery(query);
  this.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
  state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));

  this.addOverlay(state.overlay);
  if (this.showMatchesOnScrollbar) {
    if (state.annotate) {
      state.annotate.clear();
      state.annotate = null;
    }
    state.annotate = this.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
  }
});

// this is to have an external input, but you could have your own way of
// providing your query. Important thing is that you can run search on
// an instance with a query.
const body = document.querySelector('body')
const searchAll = document.createElement('input');
body.appendChild(searchAll);
searchAll.placeholder = 'Search All';
searchAll.addEventListener('input', function(e) {
  var query = e.target.value;
  var codeMirrorInstances = document.getElementsByClassName('CodeMirror');
  for (var i = 0; i < codeMirrorInstances.length; i++) {
    var curInst = codeMirrorInstances[i].CodeMirror;
    curInst.search(query);
  }
});

const title = document.createElement('h1')
title.textContent = 'This is a document with multiple CodeMirrors'
body.appendChild(title);

const area1 = document.createElement('textarea')
body.appendChild(area1)
const editor1 = CodeMirror.fromTextArea(area1, {
  lineNumbers: true,
})

const segway = document.createElement('h2')
segway.textContent = 'Moving on to another editor'
body.appendChild(segway)

const area2 = document.createElement('textarea')
body.appendChild(area2)
const editor2 = CodeMirror.fromTextArea(area2, {
  lineNumbers: true,
});

http://codepen.io/anon/pen/yavrRk?editors=0010

EDIT:

Other commands such as findNext will work normally once the query is applied, but of course this will be instance dependent as well. If you need to implement findNext across all instances, it gets more complicated, you need to managed different things such as current focused instance, and override certain behaviors such as findNext looping and stuff like this. It can be done, but depending on the level of precision you need it may be very complex. Something like this works, it's not very elegant, but shows how it could be done:

CodeMirror.defineExtension('findNext', function(query) {
  function SearchState() {
    this.posFrom = this.posTo = this.lastQuery = this.query = null;
    this.overlay = null;
  }

  function getSearchState(cm) {
    return cm.state.search || (cm.state.search = new SearchState());
  }

  // You tweak findNext a bit so it doesn't loop and so that it returns
  // false when at the last occurence. You could make findPrevious as well
  var state = getSearchState(this);
  var cursor = this.getSearchCursor(state.query, state.posTo, state.query.toLowerCase());
  if (!cursor.find(false)) {
    state.posTo = CodeMirror.Pos(0, 0);
    this.setSelection(CodeMirror.Pos(0, 0));
    return false;

  } else {
    this.setSelection(cursor.from(), cursor.to());
    this.scrollIntoView({
      from: cursor.from(),
      to: cursor.to()
    }, 20);
    state.posFrom = cursor.from();
    state.posTo = cursor.to();
    return true;
  }
});

// You make a find next button that will handle all instances
const findNextBtn = document.createElement('button');
body.appendChild(findNextBtn);
findNextBtn.textContent = 'Find next';
findNextBtn.addEventListener('click', function(e) {
    // Here you need to keep track of where you want to start the search
    // and iterating through all instances. 
    var curFocusIndex = -1;
    var codeMirrorInstances = Array.prototype.slice.call(document.getElementsByClassName('CodeMirror'));
    var focusedIndex = codeMirrorInstances.indexOf(lastFocused.getWrapperElement());

    // with the new return in findNext you can control when you go to
    // next instance
    var findInCurCm = lastFocused.findNext();
    while (!findInCurCm && curFocusIndex !== focusedIndex) {
      curFocusIndex = codeMirrorInstances.indexOf(lastFocused.getWrapperElement()) + 1;
      curFocusIndex = curFocusIndex === codeMirrorInstances.length ? 0 : curFocusIndex;
      codeMirrorInstances[curFocusIndex].CodeMirror.focus();
      lastFocused = codeMirrorInstances[curFocusIndex].CodeMirror;    
      var findInCurCm = lastFocused.findNext();    
    }
  });

http://codepen.io/anon/pen/ORvJpK?editors=0010

Comments