Problems with my Math extension


#1

Hi,

I am trying to create a math extension for FoldingText, but I am getting some funny results at times. If you load a file with all the math but nothing for the results, then the results are not shown and none of the text area acts like it should (no math ever gets evaluated). I can not duplicate it with the debugger.

Also, I can not get my basic spec to run. It runs fine in the debugger in command console, but the spec does not run in the spec runner.

You can see my extension here: https://github.com/raguay/math.ftplugin

I have the basic version in an article about FoldingText. My editor is already evaluating it. I will let you know when it is published.


#2

test.ft (263 Bytes)

This is an example file. When you load the extension from GitHub above, open this file. Each place there is a => an answer should be calculated. If you select on of the lines, delete the >, and then type it back, it should produce the answer. But, it will not. It treats that whole area as normal text area. I have to completely delete everything, and retype it to get the math to show up.

If you load a file that has answers, you can delete the answer and the space before the => and the answer will be properly recalculated. It works fine.

I can’t get the debugger to duplicate this error state at all. Can anyone help me see what I am doing wrong? Thanks.


#3

Well, I fixed it. The GitHub has the latest version of the extension and seems to be fairly solid. I added a compute action on additions and not just changes. The load issue doesn’t happen anymore. I had to refactor some also.

Please give it a try and let me know if you find any issues. The only standing issue is that I can not seem to get the spec to run. If someone could show me the right way to create the spec or fix the issue, I would be grateful.


#4

I’ve been away from computer for last week and just now checked this out… nice!

I did notice one bug, in your process loop you have the loop termination hard coded to pnode.type() != "heading". The problem with this is that the user ("me in my first test! :smile:) might not be using headings for structure. For instance try this:

my.math
- 1 + 1 =>

It throws a null pointer since the structure isn’t using a heading. Instead try changing your loop to something like:

var pnode = node,
    text = '';

while (pnode && pnode.modeContext() === 'math') {
    text = pnode.text();
    if (text.search('=>') < 0) {
        result = text + '\n' + result;                    
    }
    pnode = pnode.previousBranch();
}

And I “think” that problem will go away.


#5

@raguay Hope you don’t mind… but you’ve inspired me!

I’m building my own version of math mode into the next release. It will work a little different from your take, more along the lines of http://mathnotepad.com except (for now anyway) with simpler output… just text, no plots. The big difference is that results will get displayed in widgets below each line of math, instead of inserted into the text. Generally I think inserting calculated values into text as user types is problematic. It can mess with undo and usually isn’t what the user expects.

Here’s what it looks like at the moment:


#6

Hi,

I made the changes you suggested and changed the extension to be ‘imath’ for inline math, but it still will not work without the extension line set to a header. The mode and modeContext do not get set unless the extension is in a header. You can see that here in a picture of the debugger:

But, for me it is okay. I always use a header to define it so that I can fold it out of the way also.

I like the answer inline as I want to keep the answers in the document. Just like Calca. I have problems with Calca on large documents, but FoldingText handles them better (though I still get some screen jitters while scrolling with a 2000 word tutorial). If my extension uses imath and your math, everyone can choose the one they want to use.

I have never seen mathnotepad.com. That is nice. It is using the same JS library too! That is a great library. I am still exploring the things it can do.

I updated my GitHub. Could you add a link to it for the extensions wiki page?


#7

Never mind. I just figured it out. If you do not make the extension line in a header, the lines after it have to be indented. Otherwise, they are all treated as equals and the mode is not set based on the previous line. Okay, I am starting to get a hang of this.

I like working with this a lot! It would be nice to add graphs!


#8

Hey, how are you getting functions to work. Everytime I follow the examples on mathjs website, it errors on me. What’s the trick?


#9

I don’t think I’m handling them in any way different then normal equations. I just use the same mathjs.compile on each line of math. Here’s the full logic of my code:

define(function(require, exports, module) {
  'use strict';

  var Extensions = require('ft/core/extensions').Extensions,
    Pasteboard = require('ft/system/pasteboard').Pasteboard,
    NodeSet = require('ft/core/nodeset').NodeSet,
    mathjs = require("./math.js"),
    idsToCompiledExpressions = {},
    idsToResults = {},
    idsToErrors = {};

  function refreshCalc(editor, node) {
    var each = node.firstChild,
      compiledExpression,
      scope = {},
      newResult,
      line,
      id;

    while (each) {
      id = each.id;
      line = each.line().trim();
      compiledExpression = idsToCompiledExpressions[id];

      idsToErrors[id] = false;

      if (!line || line[0] === '#') {
        idsToCompiledExpressions[id] = undefined;
        newResult = undefined;
      } else {
        try {
          if (!compiledExpression) {
            compiledExpression = mathjs.compile(line);
            idsToCompiledExpressions[id] = compiledExpression;
          }

          if (compiledExpression) {
            newResult = compiledExpression.eval(scope);
          }
        } catch (e) {
          newResult = e.message;
          idsToErrors[id] = true;
        }
      }

      var oldResult = idsToResults[id],
        oldResultString = oldResult ? oldResult.toString() : '',
        newResultString = newResult ? newResult.toString() : '';
      if (oldResultString !== newResultString) {
        idsToResults[id] = newResult;
        editor.setNeedsRender(each);
      }        

      each = each.nextSibling;
    }
  }

  Extensions.addMode({
    name: 'calc'
  });

  Extensions.addTreeChanged(function(editor, e) {
    var deltas = e.deltas,
      effectedCalcs = new NodeSet();

    for (var i = 0; i < deltas.length; i++) {
      var delta = deltas[i],
        insertedNodes = delta.insertedNodes,
        removedNodes = delta.removedNodes,
        updatedNode = delta.updatedNode,
        length,
        each,
        j;

      length = removedNodes.length;
      for (j = 0; j < length; j++) {
        each = removedNodes[j];
        delete idsToCompiledExpressions[each.id];
        var previousParent = e.previousParent(each);
        if (previousParent) {
          if (previousParent.mode() === 'calc') {
            effectedCalcs.addNode(previousParent);
          }
        }
      }

      length = insertedNodes.length;
      for (j = 0; j < length; j++) {
        each = insertedNodes[j];
        if (each.mode() === 'calc') {
          effectedCalcs.addNode(each);
        } else if (each.modeContext() === 'calc' && each.parent.mode() === 'calc') {
          effectedCalcs.addNode(each.parent);
        }
      }

      if (updatedNode) {
        delete idsToCompiledExpressions[updatedNode.id];
        if (updatedNode.modeContext() === 'calc' && updatedNode.parent.mode() === 'calc') {
          effectedCalcs.addNode(updatedNode.parent);
        }
      }
    }

    if (effectedCalcs.count > 0) {
      effectedCalcs.forEachNodeInSet(function (each) {
        if (each.mode() === 'calc') {
          refreshCalc(editor, each);
        }
      });
    }
  });

  function createWidget(className, textContent) {
    var widget = document.createElement('span');

    widget.className = className;
    widget.textContent = textContent;

    widget.addEventListener('mousedown', function (e) {
      e.preventDefault();
    });

    widget.addEventListener('mouseup', function (e) {
      e.preventDefault();
      Pasteboard.writeString(widget.textContent);
    });

    return widget;
  }

  Extensions.addRenderNode(function (editor, node, nodeRenderer) {
    if (node.modeContext() === 'calc' && node.parent.mode() === 'calc') {      
      var result = idsToResults[node.id];

      if (result !== undefined) {
        var widget;

        if (idsToErrors[node.id]) {
          widget = createWidget('cm-calc-renderwidget cm-error', result);
        } else {
          widget = createWidget('cm-calc-renderwidget', mathjs.format(result, {
            precision: 14
          }));
        }

        nodeRenderer.renderLineWidget(widget, {
          showIfHidden : false,
          handleMouseEvents: true,
          positionWidget : function (widgetWidth, widgetHeight) {
            var line = node.lineNumber(),
              leadingSpace = node.line().match(/\s*/)[0],
              coords = editor.charCoords({ line : line, ch : leadingSpace.length}, 'div'),
              left = Math.round(coords.left + (editor.defaultSpaceWidth() * 4)) + 'px';
            return {
              marginLeft: left
            };
          }
        });
      }
    }
  });

  Extensions.addCommand({
    name: 'calc',
    description: 'Evaluate selected text and replace with result.',
    performCommand: function (editor) {
      var range = editor.selectedRange(),
        text = range.textInRange(),
        parser = mathjs.parser(),
        result;

      try {
        text.split('\n').forEach(function (each) {
          result = parser.eval(each);
        });
      } catch (e) {
        result = e;
      }

      editor.replaceSelection(mathjs.format(result), 'around');      
    }
  });  
});

FoldingText Extensions Wiki
#10

I found my bug. The latest version is on my GitHub page for it

It now does functions, evaluates around imath section, and if a section is folded, it will not evaluate the expressions there. If you can think of something else to add to is, just let me know.


#11

Suggestions to improve your imath plugin. From time to time, I use Calca to make some calculation, and more.

Two features are really useful:

1 cmd-return to automatically write => and get the result;

2 Google search: type $200 in € = ? the application makes the request and gives the answer:
$200 in € = 160,468568 € #googled.

Definitely useful. Not sure it is the good place to post this.

My two cents.


#12

That and graphs would make it closer to Calca. I actually have more features to add and fix, just have not gotten around to it. I am glad you are using it and finding it helpful. I use it more than Calca since Calca has a jitter problem (large documents jump up and down while typing. Very distracting. I have told the author.).