charWidth() and non-monospace fonts

Hi

When using non-monospace fonts, the value returned by charWidth() is useless and any calculations that depend on it become incorrect. This unfortunately limits CM to monospace fonts.

For example using the indentwrap demo with a non-monospace font will give the wrong result.

The issue also occurs when a MarkedSpan changes the class of a segment inside the line so that it has a different width - for example changing to a different font, font size, bold, italics etc.

I looked into CM code trying to understand this. As far as I can see a solution requires two things:

  1. An API function using the Codemirror-measure div to calculate the position of a character in a line that takes into account any MarkedSpan in the line (different fonts etc). So the line need to be constructed into the Codemirror-measure div in the same way codemirror creates it when displayed, then chopped at the right column before it’s width measured.

  2. Any CM code that relies on charWidth() needs to be changed to use the above function, so that CM does not use charWidth() internally at all.

I realize this may be a big change but I am writing this post in hope that it can be be implemented to solve what is to me a major limitation in CM, and can open the door for more non-code-only projects building on CM.

Thanks!
Micha

It is not called charWidth, it is called defaultCharWidth to make it clear that no, you can’t rely on this as being the width of all characters. Saying that it ‘limits CM to monospace fonts’ is nonsense. CodeMirror works fine with non-monospace fonts, only some hacks like the indentwrap demo don’t. That’s part of the requiremens for these hacks – you’ll have to use a monospace font to have them work.

Hi Marjin, thanks for answering !

Looking at the code, and as far as I can tell, defaultCharWidth returns the result of charWidth(…) which is incorrect for non-monospace or mixed fonts lines etc. I now see that charWidth() is not called a lot internally, one place is in posFromMouse(), does that mean this function and anything that relies on it will have an issue with non-monospace etc ?

Indentwrap is a hack because there is no better way to achieve what it does otherwise. Likewise there is no way I can see to calculate the absolute position of a character in the line (for non monospace etc), or to calculate the place a line is wrapped in those cases. It is exactly the purpose of my post to try to find solutions for such issues (needed for more non-code projects built with codemirror).

Thanks!
Micha

which is incorrect for non-monospace or mixed fonts lines

It is not incorrect, it is an approximation, which is how it is used. So no, don’t worry, all of the core editor really does work with a variable-width font. Getting the position of a character is done with charCoords or cursorCoords.

Thanks marijn !

So if I understand correctly I should rely on cursorCoords rather than multiples of defaultCharWidth to calculate position of a column in a line.

I tried to apply this to the indentwrap sample to make it work with non-monospace fonts, I tried:

  editor.on("renderLine", function(cm, line, elt) {
    if (cm.renderLineStopRecurse===true) return;
    cm.renderLineStopRecurse=true;

    var l=line.lineNo();
    var p = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize"));

    var off=cm.cursorCoords({line: l, pos: p}, "window").left;
    console.log("renderLine: "+l+"/"+p+" -> "+off);

    elt.style.textIndent = "-" + off + "px";
    elt.style.paddingLeft = (basePadding + off) + "px";

    cm.renderLineStopRecurse=false;
  });

(notice the ugly renderLineStopRecurse to prevent an endless loop when cursorCoords calls renderLine)

However this does not work, the values I get for off are very high (600+) even when p is 1 or 2. Is there any other consideration I am missing ?

Thank you!
Micha

You’re using the left of the coords in window coordinates as offset. What you want is the difference between the start of the line and the start of the text (which is not the column, so countColumn isn’t what you want here either).

Thanks marjin,

I’ve updated my code accordingly. When the initial text displays it is indented properly which is great progress, however if I edit any line CM becomes unresponsive on that line and in the console I see:

Uncaught TypeError: Cannot read property ‘length’ of null codemirror.js:878

line 878 in codemirror.js is:

877 function updateLineForChanges(cm, lineView, lineN, dims) {
878 for (var j = 0; j < lineView.changes.length; j++) {

I don’t understand what is the issue here. Can you provide any help in why I get this error and why is CM then unresponsive on that line ?

My revised code is simply:

 editor.on("renderLine", function(cm, line, elt) {
    if (cm.renderLineStopRecurse===true) return;
    cm.renderLineStopRecurse=true;

    var col=CodeMirror.countColumn(line.text, null, cm.getOption("tabSize"));
    var off=cm.cursorCoords({line: line.lineNo(), ch: col}, "local").left;

    elt.style.textIndent = "-" + off + "px";
    elt.style.paddingLeft = (basePadding + off) + "px";

    cm.renderLineStopRecurse=false;
  });

Thanks!
Micha

Ah, yes, what you’re seeing there looks like the editor’s state getting screwed up by the recursive calls originating from the event handler. The manual states you aren’t allowed to “try to change the state of the editor” from this event, and due to the way rendering and measuring works, calling cursorCoords can change the editor’s state.

In general, you seem to be heading down a route of madness here. Measuring requires a rendered representation of a line, so measuring a given line when it is being rendered is, even if it wouldn’t crash your editor, going to duplicate all rendering work. Have you considered just measuring the size of a space, once, and using that and hoping for the best? Or are you really using widgets in leading whitespace and/or wildly varying font sizes?

Thanks marjin,

So I understand I can’t use cursorCoords in renderLine.

Yes madness for sure :slight_smile: I have now spent a lot of time trying to achieve what I thought would be simple and am back at square one. measuring the size of a space once is not reliable in a situation when the user can change fonts or have a line made up of multiple fonts (as is for example in Firepad).

Perhaps I’m asking the wrong question - I’ll try to rephrase it

Is there a reliable way to have indent wrap in CM when the text contains multiple fonts/non-monospace characters ? If yes how ?

I feel the root of the problem is that the only way to implement indentwrap is through the renderLine hack. Perhaps a more clean way can be implemented ? (for example allow defining custom wraps in the same way you can define custom folds etc)

Thank you !
Micha

I don’t think so. You might be able to approximate it with some clever hacks (inspecting the whitespace tokens at the start of the line, and picking the right space width based on their style), but I don’t know of a general solution.

measuring a given line when it is being rendered is, even if it wouldn’t crash your editor, going to duplicate all rendering work

The duplication won’t be bad with caching - many lines will have the same prefix and it rarely changes as the line is edited. IIUC the worst part is triggering browser reflow each measurement and caching helps here.

But yes, using charCoords/cursorCoords involves madness because by definition those work post-renderLine. It’s saner to do our own measurements (which is what we did in our caching proof-of-concept) but we didn’t yet get to handle fonts & tabs.
That’s where the desire to reuse codemirror’s measurement comes in - we want to reuse existing span/pre building logic (buildToken etc). A natural approach I haven’t yet tried is simply cloning part of the DOM tree passed into renderLine (easier with a mode that adds a class to the prefix to so it’s separate spans).

But it’d also be neat to somehow reuse CodeMirror’s measurement caching prowess.
A crazier (but more black-box) idea is hsaving a second invisible CodeMirror instance without the renderLine handler and only using it for measurements (adding every prefix encountered as a line)…

P.S. I must say that while renderLine is a hack, it’s a brilliantly flexible one! I’ve used it for styling that dramatically affects line breaking and heights, and CodeMirror still automagically knew where everything is…

BTW, another case where user-driven measurements would be needed is simulating “elastic tabstops” [https://github.com/codemirror/CodeMirror/issues/2510] or other kinds of aligning between lines.

But I’m not sure how that would even work API-wise. A line’s rendering depends on both previous and following lines (we might even care about lines below the viewport). I don’t see a sane way to do it perfectly on first render.
at least in some cases it should probably involve user-visible later corrections.
It’s not even a clean -> markText loop; even if change handler can do measurements (not sure why but feels not quite Right to me), it’ll be a change -> markText or record sizes elsewhere -> renderLine.

The flows I can see are:

  1. change event somehow doing measurements -> markText if column widths changed -> renderLine applying the widths.
  2. each renderLine measuring its own line -> setTimeout to run markText if column width changed -> renderLine applying the new widths.

The bottom line is that while maybe not optimal, it could be handy if CodeMirror exposed functions that take a given range of text and build a DOM tree and/or measure it. Independent of the normal rendering flow (eg. if I want I can measure stuff far outside the viewport).

A simpler request needed for any kind of custom caching:
CodeMirror knows to clean its caches in certain situations. Could you also fire an event at such moments?

I did end up coming with a solution to measure the position of a column in a line from renderLine and without relying on the defailtCharWidth. I don’t know how good it is but it works so I’ll post it here so others can use it maybe.

Basically I cut renderLine’s elt.innerHTML of at the given position preserving any tags. I then put that html in a separate measurement div and measure it (I tried to reuse cm.display.measure but it didn’t work). A caching can be added to improve performance:

  var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
    lineNumbers: true,
    lineWrapping: true,
    mode: "text/html"
  });

  function htmlOffset(html, pos) {
    
    function cutHTML(html, pos) { // cut html text at pos preserving all tags
      var tag=false, symbol=false; res='', count=0;

      for(var i=0; i<html.length; i++ ) {
          if(html[i] === '<') tag = true;
          if (tag || count<pos) res += html[i];

          if (!tag) {
            if (html[i] === '&') symbol=true;
            if (symbol && html[i] === ';') symbol=false;
            if (!symbol) count++; // note this counts the ';' of a symbol
          }

          if(html[i] === '>') tag = false;
      }
      return res;
    }

    var span=document.getElementById('htmlOffsetMeasurementSpan');
    if (!span) { // create a 'dummy' span as the last child of 'CodeMirror'
      span=document.createElement('span');
      span.setAttribute("id", "htmlOffsetMeasurementSpan");
      span.style.cssText = 'border:0;padding:0;'; // offsetWidth includes padding and border, explicitly override the style:
      document.getElementsByClassName('CodeMirror')[0].appendChild(span);
    }

    // measure offset
    span.innerHTML = cutHTML(html, pos).split(' ').join('\xA0'); // replace spaces with hard spaces
    span.style.display = 'inline'; // show
    var prefixWidth=span.offsetWidth;
    span.style.display = 'none'; // hide
    return prefixWidth;
  }

  var basePadding = 4;
  
  editor.on("renderLine", function(cm, line, elt) {
    var offstCol=CodeMirror.countColumn(line.text, null, cm.getOption("tabSize"));
    var prefixWidth=htmlOffset(elt.innerHTML, offstCol);

    elt.style.textIndent = "-" + prefixWidth + "px";
    elt.style.paddingLeft = (basePadding + prefixWidth) + "px";
  });

  editor.refresh();

That does sound like it should work. Indeed, don’t touch the editor’s own measure divs, and add your own. It is probably a good idea to filter out short lines and non-indented lines, and do nothing for them, to prevent wasting too much CPU time.

All this could be avoided if there was an event (similar to ‘beforeChange’) that fired before a line was rendered for the first time. If I had such a beforeFirstRender event I could have calculated everything in that and in beforeChange using cursorCoords in a safe clean way and save it somewhere for renderLine to use on rendering. This would also be super efficient because the heavy calculation will only be done when needed. In addition if there was a way to set a style on a line I could set the textIndent and paddingLeft styles there and not need any code in renderLine …

I just posted a separate request for such an event (change/update events the first time a line is drawn)

Thank you!
micha