6

TL;DR

I have function that replace text, a string and cursor position (a number) and I need to get corrected position (a number) for new string that is created with replace function if the length of the string changes:

input and cursor position:  foo ba|r text
replacement: foo -> baz_text, bar -> quux_text
result: baz_text qu|ux_text text

input and cursor position:  foo bar| text
replacement: foo -> baz_text, bar -> quux_text
result: baz_text quux_text| text

input and cursor position:  foo bar| text
replacement: foo -> f, bar -> b
result: f b| text

input and cursor position:  foo b|ar text
replacement: foo -> f, bar -> b
result: f b| text

the problem is that I can use substring on original text but then the replacement will not match whole word so it need to be done for whole text but then substring will not match the replacement.

I'm also fine with solution that cursor is always at the end of the word when original cursor is in the middle of the replaced word.

and now my implementation, in jQuery Terminal I have a array of formatters functions in:

$.terminal.defaults.formatters

they accept a string and it should return new string it work fine except this case:

when I have formatter that change length if break the command line, for instance this formatter:

$.terminal.defaults.formatters.push(function(string) {
   return string.replace(/:smile:/g, 'a')
                .replace(/(foo|bar|baz)/g, 'text_$1');
});

then the cursor position was wrong when command line get new string.

I've try to fix this but it don't work as expected, the internal of the terminal look like this,

when I change position I'm crating another variable formatted_position that's use in command line to display the cursor. to get that value I use this:

formatted_position = position;
var string = formatting(command);
var len = $.terminal.length(string);
var command_len = $.terminal.length(command);
if (len !== command_len) {
    var orig_sub = $.terminal.substring(command, 0, position);
    var orig_len = $.terminal.length(orig_sub);
    var formatted = formatting(orig_sub);
    var formatted_len = $.terminal.length(formatted);
    if (orig_len > formatted_len) {
        // if formatting make substring - (text before cursor)
        // shorter then subtract the difference
        formatted_position -= orig_len - formatted_len;
    } else if (orig_len < formatted_len) {
        // if the formatted string is longer add difference
        formatted_position += formatted_len - orig_len;
    }
}

if (formatted_position > len) {
    formatted_position = len;
} else if (formatted_position < 0) {
    formatted_position = 0;
}

$.terminal.substring and $.terminal.length are helper functions that are terminal formatting aware (text that look like this [[b;#fff;]hello]) if you will write solution you can use normal text and use string methods.

the problem is that when I move the cursor in the middle of the word that is changed

it kind of work when text is longer, but for shorter string the cursor jump to the right when text is in the middle of the word that got replaced.

I've try to fix this as well using this code:

function find_diff(callback) {
    var start = position === 0 ? 0 : position - 1;
    for (var i = start; i < command_len; ++i) {
        var substr = $.terminal.substring(command, 0, i);
        var next_substr = $.terminal.substring(command, 0, i + 1);
        var formatted = formatting(next_substr);
        var substr_len = $.terminal.length(substr);
        var formatted_len = $.terminal.length(formatted);
        var diff = Math.abs(substr_len - formatted_len);
        if (diff > 1) {
            return diff;
        }
    }
    return 0;
}

...

} else if (len < command_len) {
    formatted_position -= find_diff();
} else if (len > command_len) {
    formatted_position += find_diff();
}

but this I think make it even worse becuase it find diff when cursor is before or in the middle of replaced word and it should find diff only when cursor is in the middle of replaced word.

You can see the result of my attempts in this codepen https://codepen.io/jcubic/pen/qPVMPg?editors=0110 (that allow to type emoji and foo bar baz get replaced by text_$1)

UPDATE:

I've make it kind of work with this code:

    // ---------------------------------------------------------------------
    // :: functions used to calculate position of cursor when formatting
    // :: change length of output text like with emoji demo
    // ---------------------------------------------------------------------
    function split(formatted, normal) {
        function longer(str) {
            return found && length(str) > length(found) || !found;
        }
        var formatted_len = $.terminal.length(formatted);
        var normal_len = $.terminal.length(normal);
        var found;
        for (var i = normal_len; i > 1; i--) {
            var test_normal = $.terminal.substring(normal, 0, i);
            var formatted_normal = formatting(test_normal);
            for (var j = formatted_len; j > 1; j--) {
                var test_formatted = $.terminal.substring(formatted, 0, j);
                if (test_formatted === formatted_normal &&
                    longer(test_normal)) {
                    found = test_normal;
                }
            }
        }
        return found || '';
    }
    // ---------------------------------------------------------------------
    // :: return index after next word that got replaced by formatting
    // :: and change length of text
    // ---------------------------------------------------------------------
    function index_after_formatting(position) {
        var start = position === 0 ? 0 : position - 1;
        var command_len = $.terminal.length(command);
        for (var i = start; i < command_len; ++i) {
            var substr = $.terminal.substring(command, 0, i);
            var next_substr = $.terminal.substring(command, 0, i + 1);
            var formatted_substr = formatting(substr);
            var formatted_next = formatting(next_substr);
            var substr_len = length(formatted_substr);
            var next_len = length(formatted_next);
            var test_diff = Math.abs(next_len - substr_len);
            if (test_diff > 1) {
                return i;
            }
        }
    }
    // ---------------------------------------------------------------------
    // :: main function that return corrected cursor position on display
    // :: if cursor is in the middle of the word that is shorter the before
    // :: applying formatting then the corrected position is after the word
    // :: so it stay in place when you move real cursor in the middle
    // :: of the word
    // ---------------------------------------------------------------------
    function get_formatted_position(position) {
        var formatted_position = position;
        var string = formatting(command);
        var len = $.terminal.length(string);
        var command_len = $.terminal.length(command);
        if (len !== command_len) {
            var orig_sub = $.terminal.substring(command, 0, position);
            var orig_len = $.terminal.length(orig_sub);
            var sub = formatting(orig_sub);
            var sub_len = $.terminal.length(sub);
            var diff = Math.abs(orig_len - sub_len);
            if (false && orig_len > sub_len) {
                formatted_position -= diff;
            } else if (false && orig_len < sub_len) {
                formatted_position += diff;
            } else {
                var index = index_after_formatting(position);
                var to_end = $.terminal.substring(command, 0, index + 1);
                //formatted_position -= length(to_end) - orig_len;
                formatted_position -= orig_len - sub_len;
                if (orig_sub && orig_sub !== to_end) {
                    var formatted_to_end = formatting(to_end);
                    var common = split(formatted_to_end, orig_sub);
                    var re = new RegExp('^' + $.terminal.escape_regex(common));
                    var to_end_rest = to_end.replace(re, '');
                    var to_end_rest_len = length(formatting(to_end_rest));
                    if (common orig_sub !== common) {
                        var commnon_len = length(formatting(common));
                        formatted_position = commnon_len + to_end_rest_len;
                    }
                }
            }
            if (formatted_position > len) {
                formatted_position = len;
            } else if (formatted_position < 0) {
                formatted_position = 0;
            }
        }
        return formatted_position;
    }

it don't work for one case when you type emoji as first character and the cursor is in the middle of :smile: word. How to fix get_formatted_position function to have correct fixed position after replace?

UPDATE: I've ask different and simple question and got the solution using trackingReplace function that accept regex and string, so I've change the API for formatters to accept array with regex and string along the function Correct substring position after replacement

jcubic
  • 61,973
  • 54
  • 229
  • 402
  • 1
    Are you sure, moving the cursor into the replaced word is right? To me it seems quite confusing. I would look into Word, or google docs if they don't move it at the beginning or the end. – Akxe Oct 11 '17 at 17:14
  • @Akxe it's completly different case than word or google docs, because the text is replaced while you type not when you do text replace like in search/replace function. And it look very weird when you don't change position because you can be end the end of the text that have length of 10 and the replaced text is 3 and you get position of 10 it should be 3. – jcubic Oct 11 '17 at 17:41
  • I meant, if you replace the word your cursor is currently inside, then it may be better to put the cursor in front or after the new replaced word. And Word for sure have option for replace. – Akxe Oct 11 '17 at 18:28
  • @Akxe I'm fine with solution that cursor is at the end of the replaced word if is in the middle but either way I need to detect if cursor is in the middle of the word and then subtract from original position to make it at the end. – jcubic Oct 11 '17 at 18:33
  • I'll look into it tomorrow, I promise nothing, but I will give it a shot. – Akxe Oct 11 '17 at 19:22
  • What are your browser needs? – Akxe Oct 17 '17 at 13:08
  • @Akxe I should work in modern browsers and IE9 but my question got resolved but I can't close this one as duplicate because of bounty. – jcubic Oct 17 '17 at 13:28
  • You should link it... or will you link it afterwards using the duplicite? – Akxe Oct 17 '17 at 19:29
  • @Akxe will mark as duplicate when bounty ends, that comment "possible duplicate of" is added when you click close but I can't click it because I have golden badge so I can close automatically which is not allowed while bounty is on – jcubic Oct 17 '17 at 20:33

1 Answers1

1

So I was able to accomplish the given task, however I wasn't able to implement it into the library as I am not sure how to implements many things there.

I made it in vanilla javascript so there shouldn't be any hiccups while implementing into the library. The script is mostly dependant on the selectionStart and selectionEnd properties available on textarea, input or similar elements. After all replacement is done, the new selection is set to the textarea using setSelectionRange method.

// sel = [selectionStart, selectionEnd]
function updateSelection(sel, replaceStart, oldLength, newLength){
    var orig = sel.map(a => a)
    var diff = newLength - oldLength
    var replaceEnd = replaceStart + oldLength
    if(replaceEnd <= sel[0]){
        //  Replacement occurs before selection
        sel[0] += diff
        sel[1] += diff
        console.log('Replacement occurs before selection', orig, sel)
    }else if(replaceStart <= sel[0]){
        //  Replacement starts before selection
        if(replaceEnd >= sel[1]){
            //  and ends after selection
            sel[1] += diff
        }else{
            //  and ends in selection
        }
        console.log('Replacement starts before selection', orig, sel)
    }else if(replaceStart <= sel[1]){
        //  Replacement starts in selection
        if(replaceEnd < sel[1]){
            //  and ends in seledtion
        }else{
            //  and ends after selection
            sel[1] += diff
        }
        console.log('Replacement starts in selection', orig, sel)
    }
}

Here is whole demo: codepen.

PS: From my observations the format script runs way to often.

Akxe
  • 9,694
  • 3
  • 36
  • 71
  • Sorry maybe I wasn't clear enough, but I don't want to keep cursor for selected text in textarea but I need this for normal string (I don't have any selection) and cursor is just a number not real textarea cursor. – jcubic Oct 12 '17 at 07:00
  • I have textarea in terminal but it need to have original text not the replaced one, and one more thing I have a function as I mentioned that do all replacements I can't iterate over keys like in your code and also one replacement function can do multiple words not single one so your code will break when you do this `string = string.replace(new RegExp(toRelpace.replace(/([()])/g, '\\$1'), 'g'), replaceWith)` – jcubic Oct 12 '17 at 07:22
  • So you need the formated text as well as the original? The selection can be used mostly unchanged even if you don't use start & end, just imagine start and end be same number. And the regexps, you have them separate, you just concat them in your code. – Akxe Oct 12 '17 at 14:17
  • The thing is, if you don't type in the formatted input the cursor will do weird thing. For example, in you codepen, it jumps wierdly around emoji that are in the `:emotion:` format. – Akxe Oct 12 '17 at 14:19
  • I've added [two buttons to your solution](https://codepen.io/jcubic/pen/PJBBMp?editors=1010) and it simply don't work if I put cursor in the middle of :emotion: or in the middle of bar the corected position is wrong and also your code will be completely wrong when you have function that you can't access that do the replacement. My solution only don't work (I think) for case when there are no characters before first :emotion: and the cursor is in the middle, I know the function get executed a lot but I didn't figure out how to calculate corrected position with calling that function. – jcubic Oct 12 '17 at 17:39