Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Overlaying StyleSpans causes the edited line to move up #724

Closed
liz3 opened this issue Apr 9, 2018 · 21 comments
Closed

Bug: Overlaying StyleSpans causes the edited line to move up #724

liz3 opened this issue Apr 9, 2018 · 21 comments

Comments

@liz3
Copy link

liz3 commented Apr 9, 2018

So this is a bit more specific and not easy to explain, ill try my best though.

I had the question #715 which was answered successfully.
Now i had to discover a (probably) bug related to this.

Let me try to Explain:
When overlaying a paragraph with other StyleSpans, there are cases, where after it applied the changes to the style, you than press a key which inserts a char, moves the Caret or anything else, moves the line/paragraph you just edited to the top of the area viewport.
This only happens once, so you have to edit the spans again to re create this Effect.
Another requirement, at least in my case is, that the spans of the paragraph are actually changed, when the old styleSpans from the paragraph get applied again to the same paragraph without changes, the bug will not appear.
So the creator of this bug is: area.setStyleSpans with edited style spans.

Could this be a bug, or am i doing a mistake?

Kind Regards,
Liz3

@liz3 liz3 changed the title Bug: Overlaying StyleSpans causes the edited line to move up to Bug: Overlaying StyleSpans causes the edited line to move up Apr 9, 2018
@JordanMartinez
Copy link
Contributor

Please provide a reproducible demo. It's often easier to understand what you're talking about when I can reproduce it myself. Sometimes, when pursuing that end, you'll realize what's causing the mistake yourself.

@liz3
Copy link
Author

liz3 commented Apr 10, 2018

Hey, i was not able to reproduce the error.
And i found ways to fix this, so the bug is depending on my Project.
Its just super Hard to debug this, because the Project/its gui layer has become quite big and there could be so many sources for this.

I opened this issue though, because i was able to clearly identify that the bug at the end comes from area.setStyleSpans.

This Issue is not closed but "put on hold".
Once i found the bug i want to provide the answer here.
(The Project) :)
Regards
Liz3

@liz3
Copy link
Author

liz3 commented Apr 10, 2018

EDIT:
i can actually tell that this method: https://github.com/FXMisc/RichTextFX/blob/master/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java#L1479-L1485
is somehow related to this issue, i added org.fxmisc.richtext into the project rather than as a dependency, and stopping that method, also solves the bug.
(also with the demo)

ORIGINAL:

Update, i have a reproducible demo.

Just as information, turned out its related to a lot of things, but most important it seams to be related to the VirtualizedScrollPane.

I added the Complete file.

To reproduce:
Just start the file(the css file is also included). Go somewhere in the Editor and edit one char to trigger the subscription.
Now we want to focus on line 26.
Now for the Bug, the size of the window plays a important role, but the basic is:
when line 26 is in the upper half of the area viewport and the width of the window is below a certain width, as soon as a key gets pressed, which moves the caret, line 26 will jump to the top of the editor.
If either line 26 is in the lower half of the area viewport or the width of the window is higher than that magic amount, the bug will take effect.
I also recorded a little video, if i did not explained enough here.

Any ideas from what this is coming?

DemoCode.txt
NeededCss.txt
demo-video.zip

Kind Regards
Liz3

@NickAcPT
Copy link

Jordan, have you got any update on this issue?

@JordanMartinez
Copy link
Contributor

I could not reproduce the bug with that code. However, I'm not sure if I'm following your directions correctly, even with the video. Here's what I did:

  • I changed the "/HighlightingLight.css" to this.getClass().getResource("demo-resource.css").toExternalForm()
  • I added a <> after new VirtualizedScrollPane(area);
  • Then I actually tried out the demo with the following steps:
    • Start the demo
    • Scroll to the top
    • add a space at the end of line 2 (options:), which triggers the highlighting
    • Move my caret down to line 26
    • press the space bar (as I saw you do in the video): no bug
    • make the window smaller/larger and put line 26 at the top/bottom of the viewport and press space bar again: no bug

I added org.fxmisc.richtext into the project rather than as a dependency

Why did you do that?

It seems there is something else going on in your codebase (perhaps a modification to RichTextFX inside your project).

@NickAcPT
Copy link

NickAcPT commented Apr 12, 2018

Hello, first of all, I want to thank you for the fast reply.
I won't be talking in the behalf of liz3, but I'm also related to the project.
Here's a plain word explanation of the glitch:
When typing, the followCaret() method is invoked to make sure the caret stays inside the view.
However, under some special cases, that causes some erratic movement.
I've seen it scroll to the top(scrolling the line being edited to the top of the view).

@JordanMartinez
Copy link
Contributor

You mean this project?

Right, and it's those 'special circumstances' that remain unclear to me.

@NickAcPT
Copy link

NickAcPT commented Apr 12, 2018

Yes, that one.
If you have the time, please try reproducing the bug inside the app.
It seems to happen when the terminal sub-window is opened. Maybe it could be a layout issue in our side? (We would be grateful if you pointed us in the right direction)

Thank you anyway for your time.

@JordanMartinez
Copy link
Contributor

If you have the time, please try reproducing the bug inside the app

Sorry, but that's outside the scope of this project. If you guys wrote the bug, you can fix it, too.

@liz3
Copy link
Author

liz3 commented Apr 13, 2018

Jordan, thats not true.
The demo is completely unrelated to the Project it self, its even using a own Entry Point method.
Second, no i created the demo with having RichtextFX 0.8.2 as a gradle dependency.
im using a JDK: 1.8.0_162
The only difference is that i use a hidpi(4k) screen.
@NickAcPT the target should not be produced in the project, because that could mean possible relations to the project.

@liz3
Copy link
Author

liz3 commented Apr 13, 2018

@JordanMartinez asking for a external repoducable demo, is the least he can expect from me.
And i cannot explain my self, why he is not able to recreate the bug

@liz3
Copy link
Author

liz3 commented Apr 13, 2018

@JordanMartinez Your changes should not affect the bug.
Though i highly doubt this affects this, i added the css file with: primaryStage.getScene().getStylesheets().add("/HighlightingLight.css")

@JordanMartinez
Copy link
Contributor

@liz3 The Hi-DPI screen could be the real culprit. I'm not sure which OS you're using, but I've read that there have been some JavaFX issues related to that on Windows. I ran your reproducible demo on Linux without such a screen. If you're running it on Windows, that would explain why you're able to reproduce the bug and I cannot.

Also, if you followed all of the guidelines I specified in the Issue template (the text that a new issue already has when one first opens it), it would have helped me tremendously in troubleshooting this potential bug. You did use a clear title, but it took a while to get the reproducible demo and I still don't know which OS you are using. For your reference, I will put them here:


Guidelines For Bug Reports

  • Check whether this issue has already been reported
  • Use the first set of headers to structure what you write (expected behavior, actual behavior, reproducible demo, and environment info) and delete the other parts of this issue template
  • Use a clear title that summarizes the problem

Expected Behavior

Describe what should be occurring when you use some method or the end-user does some behavior

Actual Behavior

Describe what actually occurs when you use some method or the end-user does some behavior

Reproducible Demo

Provide a demo that maintainers of this project can copy, paste, and run to reproduce it immediately.

Use the following template to get started.

public class Bug extends Application {

 public void start(Stage primaryStage) {

  primaryStage.show();
 }

}

Environment info:

  • RichTextFX Version: <version>
  • Operating System: <my OS>
  • Java version: <version>

That being said, I also wondered about something else in your demo. I noticed in your code that you created an EventStream for the caret bounds that does nothing in its subscription. Can you reproduce the issue if that code is removed? Please test out the following code but reinsert the rest of the String content for the testcontent variable before running the test:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.reactfx.EventStream;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DemoTest extends Application{

    private CodeArea area = new CodeArea();
    private Vector<Integer> marked = new Vector<>();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        EventStream<PlainTextChange> stream = area.plainTextChanges().filter(x -> !x.isIdentity());
        stream.subscribe(e  -> runHighlighting());
        VirtualizedScrollPane scrollPane = new VirtualizedScrollPane<>(area);
        BorderPane pane = new BorderPane();
        pane.setCenter(scrollPane);
        primaryStage.setScene(new Scene(pane, 800, 600));
        primaryStage.centerOnScreen();
        primaryStage.getScene().getStylesheets().add("/HighlightingLight.css");
        primaryStage.show();

        area.setParagraphGraphicFactory(LineNumberFactory.get(area));
        area.replaceText(0,0, testcontent);

          // If this code is removed, does the issue still occur?
//        EventStream<Optional<Bounds>> caretBounds = EventStreams.nonNullValuesOf(area.caretBoundsProperty());
//
//        Subscription caretPopupSub = EventStreams.combine(caretBounds, Var.newSimpleVar(true).values()).subscribe(tuple3 -> {
//            Optional<Bounds> opt = tuple3._1;
//            if(opt.isPresent()) {
//                Bounds b = opt.get();
//            }
//        });
//        caretPopupSub.and(caretBounds.subscribe(x -> {}));
        marked.add(5);
        marked.add(25);
        marked.add(56);
    }

    private void runHighlighting() {
        area.setStyleSpans(0, computHighlighting(area.getText()));
        mappedMarked();

    }
    private void mappedMarked() {

        for(int line : marked) {

            int len = area.getParagraphLength(line);
            StyleSpans<Collection<String>> spans = area.getStyleSpans(line);
            area.setStyleSpans(line, 0, merge(spans, len, "marked"));
        }
    }

    private StyleSpans<Collection<String>> computHighlighting(String text) {

        int lastKwEnd = 0;

        StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();

        Matcher matcher = patternCompilerStatic.matcher(text);

        while (matcher.find()) {
            String styleClass =
            matcher.group("SECTION")   != null ? "section" :
            matcher.group("NUMBERS")   != null ? "numbers" :
            matcher.group("OPERATORS") != null ? "operators" :
            matcher.group("COMMAND")   != null ? "command" :
            matcher.group("PAREN")     != null ? "paren" :
            matcher.group("BRACKET")   != null ? "bracket" :
            matcher.group("STRING")    != null ? "string" :
            matcher.group("COMMENT")   != null ? "comment" :
            matcher.group("VARS")      != null ? "vars" :
            null; /* never happens */ assert styleClass != null;

            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();

        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

    private static StyleSpans<Collection<String>> merge(StyleSpans<Collection<String>> spans, int lineLength, 
                                                        String cssClass) {
        if (spans != null) {
            spans = spans.overlay(
                    StyleSpans.singleton(Collections.singletonList(cssClass), lineLength), 
                    (bottomSpan, list) -> {
                List<String> l = new ArrayList<>(bottomSpan.size() + list.size());
                l.addAll(bottomSpan);
                l.addAll(list);
                return l;
            });
        }

        return spans;
    }

    private List<String> KEYWORDS = Arrays.asList("set", "if", "stop", "loop", "return", "function", "options", "true", "false", "else", "else if", "trigger", "on", "while", "is");
    private String NUMBERS_PATTERN = "[0-9]";
    private String SECTION_PATTERN = "(?<=\\n)\\s*usage:|executable by:|aliases:|permission:|permission message:|description:|cooldown:|cooldown message:|cooldown bypass:|cooldown storage:";
    private String COMMAND_PATTERN = "(?<=\\G|\\n)command(?=\\s)";
    private String COMMENT_PATTERN = "#[^\n]*";
    private String VAR_PATTERN = "\\{\\S*}";
    private String PAREN_PATTERN = "\\(|\\)";
    private String BRACKET_PATTERN = "\\[|\\]";
    private String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
    private String  joinBoundaryPattern(List<String> items) {
        return "\\b(" + String.join("|", items) + ")\\b";
    }
    private Pattern patternCompilerStatic = Pattern.compile(
            "(?<SECTION>" + SECTION_PATTERN + ")"
                    + "|(?<NUMBERS>" + NUMBERS_PATTERN + ")"
                    + "|(?<OPERATORS>" + joinBoundaryPattern(KEYWORDS) + ")"
                    + "|(?<COMMAND>" + COMMAND_PATTERN + ")"
                    + "|(?<PAREN>" + PAREN_PATTERN + ")"
                    + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
                    + "|(?<STRING>" + STRING_PATTERN + ")"
                    + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
                    + "|(?<VARS>" + VAR_PATTERN + ")");

    // TODO: This part of the demo needs to be replaced with the actual content from your demo
    private String testcontent = "# do whatever you want with this, just don't post it elsewhere as your own unless you've changed ~80% of the skript\n";
}

@liz3
Copy link
Author

liz3 commented Apr 14, 2018

Hey @JordanMartinez
So, here my status.
First no, deactivating what you mentioned does not fix the issue, i added it back then when trying to reproduce the issue.
And i have to correct that its related to the new VirtualizedScrollPane<>(area), it is not, still happens even without that.
Second, if you cant reproduce it, try around a bit, like scroll up a bit, then try it or scroll down a bit.
I don´t have the exact conditions for that, the two i know are:

  1. The line that is been edited has to be on the upper half of the area viewport.
  2. If the Area width is smaller than the text(so horizontal scroll bar is visible(assuming that new VirtualizedScrollPane<>(area) has been used)).

Third:
I tested on:
My Pc:
- Windows 10
- JDK: 1.8.0_162
- RichTextFX: 0.8.2
My MacBook:
- macOS high sierra 10.13.2
- JDK: 1.8.0_162
- RichTextFX: 0.8.2
My other Laptop:
- Elementary OS 0.4.1 loki(Ubuntu base, gnome Desktop env).
- JDK: 1.8.0_162
- RichTextFX: 0.8.2

The bug was reproducible on all of them.

@JordanMartinez
Copy link
Contributor

Thanks for the clarification and update.

You're using Java 8u162. I'm using 8u161. If you can't reproduce it on that version, there's probably a regression in your update. Still, looking at the update 162 release notes, nothing in particular stands out as a change that would cause the issue...

Second, if you cant reproduce it, try around a bit, like scroll up a bit, then try it or scroll down a bit.

I have already done that. If you can reproduce it in 8u161, then there's a number of hooks we can add to your code to determine what the conditions are for the viewport that causes the bug to appear (e.g. something that prints the scroll value or prints a stack trace to see where the bug's origin is). Ideally, if you could create a TestFX test that reproduces the issue every time, that would also be very beneficial.

@liz3
Copy link
Author

liz3 commented Apr 15, 2018

Hey, so sadly the JDK does not make the difference, the bug was producible also on 1.8.0_161
Further, yes i created a TestFX test(have never used it before though).
I also recorded another video which runs the test.
Also here some more things:

  • I only added the RichTextFX base to the project, to be able to easily modify it, i went to the Releases and downloaded 0.8.2 as .zip.
  • Since until now, you were unable to repoduce it, may there be other things i forgot, at this stage i can only tell by 100% that, this bug is unrelated to the projects Codebase and the JDK version.

The Code:

import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.Subscription;
import org.reactfx.value.Var;
import org.testfx.framework.junit.ApplicationTest;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BugDemo extends ApplicationTest {

    private CodeArea area = new CodeArea();
    private Vector<Integer> marked = new Vector<>();


    @Test
    public void recreate_bug() {

        moveTo(area);
        area.moveTo(area.getAbsolutePosition(15, area.getParagraphLength(15)));
        press(KeyCode.SPACE);
        sleep(5000);
        area.moveTo(area.getAbsolutePosition(25, area.getParagraphLength(25)));
        moveTo(area);
        sleep(5000);
        press(KeyCode.X);
        sleep(14000);


    }


    @Override
    public void start(Stage primaryStage) throws Exception {
        EventStream<PlainTextChange> stream = area.plainTextChanges().filter(x -> !x.isIdentity());
        stream.subscribe(e -> runHighlighting());
        BorderPane pane = new BorderPane();
        pane.setCenter(area);
        primaryStage.setScene(new Scene(pane, 800, 600));
        primaryStage.centerOnScreen();
        primaryStage.getScene().getStylesheets().add("/HighlightingLight.css");
        primaryStage.show();

        area.setParagraphGraphicFactory(LineNumberFactory.get(area));
        area.replaceText(0, 0, testcontent);

        // If this code is removed, does the issue still occur?
//        EventStream<Optional<Bounds>> caretBounds = EventStreams.nonNullValuesOf(area.caretBoundsProperty());
//
//        Subscription caretPopupSub = EventStreams.combine(caretBounds, Var.newSimpleVar(true).values()).subscribe(tuple3 -> {
//            Optional<Bounds> opt = tuple3._1;
//            if(opt.isPresent()) {
//                Bounds b = opt.get();
//            }
//        });
//        caretPopupSub.and(caretBounds.subscribe(x -> {}));
        marked.add(5);
        marked.add(25);
        marked.add(56);

        area.moveTo(0);
    }

    private void runHighlighting() {
        area.setStyleSpans(0, computHighlighting(area.getText()));
        mappedMarked();

    }

    private void mappedMarked() {

        for (int line : marked) {

            int len = area.getParagraphLength(line);
            StyleSpans<Collection<String>> spans = area.getStyleSpans(line);
            area.setStyleSpans(line, 0, merge(spans, len, "marked"));
        }
    }

    private StyleSpans<Collection<String>> computHighlighting(String text) {

        int lastKwEnd = 0;

        StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();

        Matcher matcher = patternCompilerStatic.matcher(text);

        while (matcher.find()) {
            String styleClass =
                    matcher.group("SECTION") != null ? "section" :
                            matcher.group("NUMBERS") != null ? "numbers" :
                                    matcher.group("OPERATORS") != null ? "operators" :
                                            matcher.group("COMMAND") != null ? "command" :
                                                    matcher.group("PAREN") != null ? "paren" :
                                                            matcher.group("BRACKET") != null ? "bracket" :
                                                                    matcher.group("STRING") != null ? "string" :
                                                                            matcher.group("COMMENT") != null ? "comment" :
                                                                                    matcher.group("VARS") != null ? "vars" :
                                                                                            null; /* never happens */
            assert styleClass != null;

            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();

        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

    private static StyleSpans<Collection<String>> merge(StyleSpans<Collection<String>> spans, int lineLength,
                                                        String cssClass) {
        if (spans != null) {
            spans = spans.overlay(
                    StyleSpans.singleton(Collections.singletonList(cssClass), lineLength),
                    (bottomSpan, list) -> {
                        List<String> l = new ArrayList<>(bottomSpan.size() + list.size());
                        l.addAll(bottomSpan);
                        l.addAll(list);
                        return l;
                    });
        }

        return spans;
    }

    private List<String> KEYWORDS = Arrays.asList("set", "if", "stop", "loop", "return", "function", "options", "true", "false", "else", "else if", "trigger", "on", "while", "is");
    private String NUMBERS_PATTERN = "[0-9]";
    private String SECTION_PATTERN = "(?<=\\n)\\s*usage:|executable by:|aliases:|permission:|permission message:|description:|cooldown:|cooldown message:|cooldown bypass:|cooldown storage:";
    private String COMMAND_PATTERN = "(?<=\\G|\\n)command(?=\\s)";
    private String COMMENT_PATTERN = "#[^\n]*";
    private String VAR_PATTERN = "\\{\\S*}";
    private String PAREN_PATTERN = "\\(|\\)";
    private String BRACKET_PATTERN = "\\[|\\]";
    private String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";

    private String joinBoundaryPattern(List<String> items) {
        return "\\b(" + String.join("|", items) + ")\\b";
    }

    private Pattern patternCompilerStatic = Pattern.compile(
            "(?<SECTION>" + SECTION_PATTERN + ")"
                    + "|(?<NUMBERS>" + NUMBERS_PATTERN + ")"
                    + "|(?<OPERATORS>" + joinBoundaryPattern(KEYWORDS) + ")"
                    + "|(?<COMMAND>" + COMMAND_PATTERN + ")"
                    + "|(?<PAREN>" + PAREN_PATTERN + ")"
                    + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
                    + "|(?<STRING>" + STRING_PATTERN + ")"
                    + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
                    + "|(?<VARS>" + VAR_PATTERN + ")");

    private String testcontent = "# do whatever you want with this, just don't post it elsewhere as your own unless you've changed ~80% of the skript\n" +
            "options:\n" +
            "\t# messages\n" +
            "\tprefix: <orange>Login <dark grey>ª<light grey>\n" +
            "\tspecifyapassword: You must specify a password\n" +
            "\tmustregister: You have to register with \"\"/register <password>\"\" before you can login # remember to double quotes\n" +
            "\talreadyloggedin: You are already logged in\n" +
            "\talreadyhavepass: You already have a password\n" +
            "\tpasswordset: Your password has been set\n" +
            "\tincorrectpass: You entered an incorrect password\n" +
            "\thavetologin: Login with \"\"/login <password>\"\" # remember to double quotes\n" +
            "\tnowloggedin: You are now logged in\n" +
            "\tnopassword: Please create a password with \"\"/register <password>\"\" # remember to double quotes\n" +
            "\ttoomanytries: You were kicked for entering an incorrect password too many times\n" +
            "\tnoperm: You do not have permission to use this\n" +
            "\tpasswordreset: Your password has been reset\n" +
            "\tresetmsg: %arg-1%'s password was reset\n" +
            "\t# settings\n" +
            "\tipchange: true # whether or not to log someone out if their ip changes between logins\n" +
            "\tkickfortries: true # whether or not to kick for incorrectly entering a password x amount of times\n" +
            "\tmaxtries: 5 # the amount of tries the player gets to enter the correct password before being kicked, if kickfortries is enabled\n" +
            "\tresetperm: reset.password\n" +
            "\tgravity: false # whether or not the armour stand should be effected by gravity. Potentially abusable to stop fall damage, but I reccomend keeping it on. True = gravity on, false = gravity off\n" +
            "\t# slighty more \"advanced\" options below here\n" +
            "\ttimeout: 0 #The time, in seconds, a user has to be logged off before they have to log in again when they rejoin (set to 0 for none)\n" +
            "\tlist: logindetails #name of the list variable\n" +
            "\t#custom event names\n" +
            "\tevent-register: register\n" +
            "\tevent-login: login\n" +
            "\tevent-incorrect: incorrectpass\n" +
            "\tevent-kick: incorrectkick\n" +
            "\t#probably shouldn't touch these ones honestly\n" +
            "\tsalts: 3\n" +
            "\tpeppers: afjfg\\]nri14753\n" +
            "\talgorithim: SHA-256\n" +
            "#the actual skript\n" +
            "function loginResetAll(i: integer = 0):\n" +
            "\tdelete {{@list}::*}\n" +
            "function loginVehicle(p: player):\n" +
            "\tmake {_p} dismount\n" +
            "\tspawn 1 armor stand at location of {_p}\n" +
            "\tset {_s} to last spawned entity\n" +
            "\tadd \"{Invisible:1}\" to nbt of {_s}\n" +
            "\tif {@gravity} is false:\n" +
            "\t\tadd \"{NoGravity:1}\" to nbt of {_s}\n" +
            "\tmake {_p} ride {_s}\n" +
            "\tset metadata value \"loginStand\" of {_s} to true\n" +
            "function loginstartsWith(s: string, t: string) :: boolean:\n" +
            "\treturn check [subtext of {_s} from characters 1 to length of {_t} is {_t}]\n" +
            "command /resetpass <offline player>:\n" +
            "\ttrigger:\n" +
            "\t\tif player has permission \"{@resetperm}\":\n" +
            "\t\t\tset {_u} to arg-1's uuid\n" +
            "\t\t\tdelete {{@list}::%{_u}%::*}\n" +
            "\t\t\tmessage \"{@prefix} {@resetmsg}\" to player\n" +
            "\t\t\tkick arg-1 due to \"{@prefix} {@passwordreset}\"\n" +
            "\t\telse:\n" +
            "\t\t\tmessage \"{@prefix} {@noperm}\" to player\n" +
            "on command:\n" +
            "\tif command executor is player:\n" +
            "\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on packet:\n" +
            "\tif event-string is \"PacketPlayInChat\":\n" +
            "\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "\t\tset {_m} to \"%packet field \"\"a\"\"%\"\n" +
            "\t\tif \"%loginstartsWith({_m}, \"\"/login\"\")% %loginstartsWith({_m}, \"\"/login \"\")%\" contains \"true\":\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tif loginstartsWith({_m}, \"/login \"):\n" +
            "\t\t\t\treplace all \"/login \" in {_m} with \"\"\n" +
            "\t\t\t\tif {{@list}::%player's uuid%::password} is set:\n" +
            "\t\t\t\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\t\t\t\tloop {{@list}::%player's uuid%::salt::*}:\n" +
            "\t\t\t\t\t\t\tloop split \"{@peppers}\" at \"\":\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::password} is hashed \"%loop-value-1%%loop-value-2%%{_m}%\" using \"{@algorithim}\":\n" +
            "\t\t\t\t\t\t\t\t\tset {{@list}::%player's uuid%::status} to true\n" +
            "\t\t\t\t\t\t\t\t\tdelete player's vehicle\n" +
            "\t\t\t\t\t\t\t\t\tdelete {{@list}::%player's uuid%::tries}\n" +
            "\t\t\t\t\t\t\t\t\tsync:\n" +
            "\t\t\t\t\t\t\t\t\t\tset yaw of {{@list}::%player's uuid%::location} to player's yaw\n" +
            "\t\t\t\t\t\t\t\t\t\tset pitch of {{@list}::%player's uuid%::location} to player's pitch\n" +
            "\t\t\t\t\t\t\t\t\t\tteleport player to {{@list}::%player's uuid%::location}\n" +
            "\t\t\t\t\t\t\t\t\tmessage \"{@prefix} {@nowloggedin}\" to player\n" +
            "\t\t\t\t\t\t\t\t\tcall custom event \"{@event-login}\" to details player\n" +
            "\t\t\t\t\t\t\t\t\tstop loop\n" +
            "\t\t\t\t\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\t\t\t\t\tmessage \"{@prefix} {@incorrectpass}\" to player\n" +
            "\t\t\t\t\t\t\tcall custom event \"{@event-incorrect}\" to details player\n" +
            "\t\t\t\t\t\t\tif {@kickfortries} is true:\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::tries} is not set:\n" +
            "\t\t\t\t\t\t\t\t\tset {{@list}::%player's uuid%::tries} to 1\n" +
            "\t\t\t\t\t\t\t\telse:\n" +
            "\t\t\t\t\t\t\t\t\tadd 1 to {{@list}::%player's uuid%::tries}\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::tries} is greater than or equal to {@maxtries}:\n" +
            "\t\t\t\t\t\t\t\t\tsync:\n" +
            "\t\t\t\t\t\t\t\t\t\tkick player due to \"{@prefix} {@toomanytries}\"\n" +
            "\t\t\t\t\t\t\t\t\t\tcall custom event \"{@event-kick}\" to details player\n" +
            "\t\t\t\t\telse:\n" +
            "\t\t\t\t\t\tmessage \"{@prefix} {@alreadyloggedin}\"\n" +
            "\t\t\t\telse:\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@mustregister}\"\n" +
            "\t\t\telse:\n" +
            "\t\t\t\tmessage \"{@prefix} {@specifyapassword}\"\n" +
            "\t\telse if \"%loginstartsWith({_m}, \"\"/register\"\")% %loginstartsWith({_m}, \"\"/register \"\")%\" contains \"true\":\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tif loginstartsWith({_m}, \"/register \"):\n" +
            "\t\t\t\tif {{@list}::%player's uuid%::password} is not set:\n" +
            "\t\t\t\t\treplace all \"/register \" in {_m} with \"\"\n" +
            "\t\t\t\t\tloop {@salts} times:\n" +
            "\t\t\t\t\t\tadd random 20 char string from `a-zA-Z0-9` to {{@list}::%player's uuid%::salt::*}\n" +
            "\t\t\t\t\tset {_ss} to a random element out of {{@list}::%player's uuid%::salt::*}\n" +
            "\t\t\t\t\tset {{@list}::%player's uuid%::password} to hashed \"%{_ss}%%a random element out of split \"\"{@peppers}\"\" at \"\"\"\"%%{_m}%\" using \"{@algorithim}\"\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@passwordset}\"\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@nowloggedin}\"\n" +
            "\t\t\t\t\tset {{@list}::%player's uuid%::status} to true\n" +
            "\t\t\t\t\tdelete player's vehicle\n" +
            "\t\t\t\t\tcall custom event \"{@event-register}\" to details player\n" +
            "\t\t\t\telse:\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@alreadyhavepass}\"\n" +
            "\t\t\telse:\n" +
            "\t\t\t\tmessage \"{@prefix} {@specifyapassword}\"\n" +
            "on quit:\n" +
            "\tdelete {{@list}::%player's uuid%::tries}\n" +
            "\tset {{@list}::%player's uuid%::lastlogin} to now\n" +
            "\tset {{@list}::%player's uuid%::lastip} to hashed player's ip\n" +
            "\tif metadata value \"loginStand\" of player's vehicle is true:\n" +
            "\t\tdelete player's vehicle\n" +
            "on join:\n" +
            "\tif difference between {{@list}::%player's uuid%::lastlogin} and now is greater than or equal to {@timeout} seconds:\n" +
            "\t\tdelete {{@list}::%player's uuid%::status}\n" +
            "\telse if hashed player's ip is not {{@list}::%player's uuid%::lastip}:\n" +
            "\t\tif {@ipchange} is true:\n" +
            "\t\t\tdelete {{@list}::%player's uuid%::status}\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tset {{@list}::%player's uuid%::location} to location of player\n" +
            "\t\twait 20 ticks\n" +
            "\t\tloginVehicle(player)\n" +
            "\t\tif {{@list}::%player's uuid%::password} is not set:\n" +
            "\t\t\tmessage \"{@prefix} {@nopassword}\" to player\n" +
            "\t\telse:\n" +
            "\t\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on click:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on drop:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on pick up:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on damage:\n" +
            "\tif attacker is a player:\n" +
            "\t\tif {{@list}::%attacker's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "on damage of player:\n" +
            "\tif {{@list}::%victim's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "on entity target:\n" +
            "\tif targeted entity is a player:\n" +
            "\t\tif {{@list}::%targeted entity's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "on packet:\n" +
            "\tif event-string is \"PacketPlayInSteerVehicle\" where [metadata value \"loginStand\" of player's vehicle is true]:\n" +
            "\t\tcancel the event\n";
}

vid.mp4.zip

@JordanMartinez
Copy link
Contributor

Excellent! I can finally reproduce it on my end.

I'd say this is a bug in Flowless, one of RichTextFX's dependencies. It looks like calling setStyleSpans causes the viewport to update its scrollY value multiple times before it finally stops at the wrong value. However, I'm not sure why it's scrolling in the first place since you're just updating the styles and not the actual text of the document.

@JordanMartinez
Copy link
Contributor

I believe the issue is arising because you are calling area.setStyleSpans multiple times. I'm not yet sure why that's an issue, but when I only call it once (using the code below), the issue seems to disappear via the following code (please check the code. If it's not doing exactly what you originally intended, then I haven't found a workaround to the bug yet):

import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.junit.Test;
import org.testfx.framework.junit.ApplicationTest;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.Assert.assertTrue;

public class BugDemo extends ApplicationTest {

    private CodeArea area = new CodeArea();
    private Vector<Integer> marked = new Vector<>();

    private boolean sleepThread = false;

    @Test
    public void recreate_bug() {
        System.out.println("Moving caret to line 15, col 15");
        sleepThread(4000);
        interact(() -> area.moveTo(area.getAbsolutePosition(15, area.getParagraphLength(15))));

        System.out.println("Typing Space");
        sleepThread(4000);
        type(KeyCode.SPACE);

        System.out.println("Moving caret to line 25, col 25");
        sleepThread(4000);
        interact(() -> area.moveTo(area.getAbsolutePosition(25, area.getParagraphLength(25))));

        double scrollY = area.getEstimatedScrollY();
        System.out.println("Before typing x, scroll y is: " + scrollY);
        interact(() -> area.estimatedScrollYProperty().addListener((obs, ov, nv) -> System.out.println("Scroll y is now: " + nv)));
        System.out.println("Typing 'X'");
        sleepThread(4000);
        type(KeyCode.X);

        double actualScrollY = area.getEstimatedScrollY();
        System.out.println("After typing 'x', scroll Y is now: " + actualScrollY);
        assertTrue(scrollY == actualScrollY);
    }


    @Override
    public void start(Stage primaryStage) {
        area.plainTextChanges()
                .filter(x -> !x.isIdentity())
                .subscribe(e -> runHighlighting());

        BorderPane pane = new BorderPane();
        pane.setCenter(area);
        primaryStage.setScene(new Scene(pane, 800, 600));
        primaryStage.centerOnScreen();
        primaryStage.getScene().getStylesheets().add(this.getClass().getResource("bug-demo.css").toExternalForm());
        primaryStage.show();

        area.setParagraphGraphicFactory(LineNumberFactory.get(area));
        area.replaceText(0, 0, testcontent);

        marked.add(5);
        marked.add(25);
        marked.add(26);

        // show top of viewport with caret at top
        area.moveTo(0);
        area.requestFollowCaret();

        // request focus so don't need to in test body
        area.requestFocus();
    }

    private void runHighlighting() {
        // these two computations could be run on a background thread each...
        StyleSpans<Collection<String>> markedSpans = computeMarkedSpans("marked");
        StyleSpans<Collection<String>> highlightedSpans = computeHighlighting(area.getText());

        // now overlay them together so only need to set style spans once
        StyleSpans<Collection<String>> spans = highlightedSpans.overlay(
                markedSpans,
                (originalList, addedList) -> {
                    if (addedList.isEmpty()) {
                        // no need to create a new list if it just stores the original list's contents
                        return originalList;
                    }

                    List<String> l = new ArrayList<>(originalList.size() + addedList.size());
                    l.addAll(originalList);
                    l.addAll(addedList);
                    return l;
                }
        );
        area.setStyleSpans(0, spans);
    }

    private StyleSpans<Collection<String>> computeMarkedSpans(String cssClass) {
        // avoid StyleSpansBuilder#create throwing IllegalStateException: No spans have been added
        if (marked.size() == 0) {
            return StyleSpans.singleton(Collections.emptyList(), 0);
        }

        StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>();
        int endOfPrevStyle = 0;
        for (int line : marked) {
            int startStyle = area.getAbsolutePosition(line, 0);

            builder.add(Collections.emptyList(), startStyle - endOfPrevStyle);
            int lineLength = area.getParagraphLength(line);
            builder.add(Collections.singletonList(cssClass), lineLength);
            endOfPrevStyle = startStyle + lineLength;
        }
        return builder.create();
    }

    private StyleSpans<Collection<String>> computeHighlighting(String text) {
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
        Matcher matcher = patternCompilerStatic.matcher(text);
        while (matcher.find()) {
            String styleClass =
                    matcher.group("SECTION") != null ? "section" :
                    matcher.group("NUMBERS") != null ? "numbers" :
                    matcher.group("OPERATORS") != null ? "operators" :
                    matcher.group("COMMAND") != null ? "command" :
                    matcher.group("PAREN") != null ? "paren" :
                    matcher.group("BRACKET") != null ? "bracket" :
                    matcher.group("STRING") != null ? "string" :
                    matcher.group("COMMENT") != null ? "comment" :
                    matcher.group("VARS") != null ? "vars" : null; /* never happens */
            assert styleClass != null;

            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();

        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

    private void sleepThread(long amount) {
        if (sleepThread) { sleep(amount); }
    }

    private List<String> KEYWORDS = Arrays.asList("set", "if", "stop", "loop", "return", "function", "options", "true", "false", "else", "else if", "trigger", "on", "while", "is");
    private String NUMBERS_PATTERN = "[0-9]";
    private String SECTION_PATTERN = "(?<=\\n)\\s*usage:|executable by:|aliases:|permission:|permission message:|description:|cooldown:|cooldown message:|cooldown bypass:|cooldown storage:";
    private String COMMAND_PATTERN = "(?<=\\G|\\n)command(?=\\s)";
    private String COMMENT_PATTERN = "#[^\n]*";
    private String VAR_PATTERN = "\\{\\S*}";
    private String PAREN_PATTERN = "\\(|\\)";
    private String BRACKET_PATTERN = "\\[|\\]";
    private String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";

    private String joinBoundaryPattern(List<String> items) {
        return "\\b(" + String.join("|", items) + ")\\b";
    }

    private Pattern patternCompilerStatic = Pattern.compile(
            "(?<SECTION>" + SECTION_PATTERN + ")"
                    + "|(?<NUMBERS>" + NUMBERS_PATTERN + ")"
                    + "|(?<OPERATORS>" + joinBoundaryPattern(KEYWORDS) + ")"
                    + "|(?<COMMAND>" + COMMAND_PATTERN + ")"
                    + "|(?<PAREN>" + PAREN_PATTERN + ")"
                    + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
                    + "|(?<STRING>" + STRING_PATTERN + ")"
                    + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
                    + "|(?<VARS>" + VAR_PATTERN + ")");

    private String testcontent = "# do whatever you want with this, just don't post it elsewhere as your own unless you've changed ~80% of the skript\n" +
            "options:\n" +
            "\t# messages\n" +
            "\tprefix: <orange>Login <dark grey>ª<light grey>\n" +
            "\tspecifyapassword: You must specify a password\n" +
            "\tmustregister: You have to register with \"\"/register <password>\"\" before you can login # remember to double quotes\n" +
            "\talreadyloggedin: You are already logged in\n" +
            "\talreadyhavepass: You already have a password\n" +
            "\tpasswordset: Your password has been set\n" +
            "\tincorrectpass: You entered an incorrect password\n" +
            "\thavetologin: Login with \"\"/login <password>\"\" # remember to double quotes\n" +
            "\tnowloggedin: You are now logged in\n" +
            "\tnopassword: Please create a password with \"\"/register <password>\"\" # remember to double quotes\n" +
            "\ttoomanytries: You were kicked for entering an incorrect password too many times\n" +
            "\tnoperm: You do not have permission to use this\n" +
            "\tpasswordreset: Your password has been reset\n" +
            "\tresetmsg: %arg-1%'s password was reset\n" +
            "\t# settings\n" +
            "\tipchange: true # whether or not to log someone out if their ip changes between logins\n" +
            "\tkickfortries: true # whether or not to kick for incorrectly entering a password x amount of times\n" +
            "\tmaxtries: 5 # the amount of tries the player gets to enter the correct password before being kicked, if kickfortries is enabled\n" +
            "\tresetperm: reset.password\n" +
            "\tgravity: false # whether or not the armour stand should be effected by gravity. Potentially abusable to stop fall damage, but I reccomend keeping it on. True = gravity on, false = gravity off\n" +
            "\t# slighty more \"advanced\" options below here\n" +
            "\ttimeout: 0 #The time, in seconds, a user has to be logged off before they have to log in again when they rejoin (set to 0 for none)\n" +
            "\tlist: logindetails #name of the list variable\n" +
            "\t#custom event names\n" +
            "\tevent-register: register\n" +
            "\tevent-login: login\n" +
            "\tevent-incorrect: incorrectpass\n" +
            "\tevent-kick: incorrectkick\n" +
            "\t#probably shouldn't touch these ones honestly\n" +
            "\tsalts: 3\n" +
            "\tpeppers: afjfg\\]nri14753\n" +
            "\talgorithim: SHA-256\n" +
            "#the actual skript\n" +
            "function loginResetAll(i: integer = 0):\n" +
            "\tdelete {{@list}::*}\n" +
            "function loginVehicle(p: player):\n" +
            "\tmake {_p} dismount\n" +
            "\tspawn 1 armor stand at location of {_p}\n" +
            "\tset {_s} to last spawned entity\n" +
            "\tadd \"{Invisible:1}\" to nbt of {_s}\n" +
            "\tif {@gravity} is false:\n" +
            "\t\tadd \"{NoGravity:1}\" to nbt of {_s}\n" +
            "\tmake {_p} ride {_s}\n" +
            "\tset metadata value \"loginStand\" of {_s} to true\n" +
            "function loginstartsWith(s: string, t: string) :: boolean:\n" +
            "\treturn check [subtext of {_s} from characters 1 to length of {_t} is {_t}]\n" +
            "command /resetpass <offline player>:\n" +
            "\ttrigger:\n" +
            "\t\tif player has permission \"{@resetperm}\":\n" +
            "\t\t\tset {_u} to arg-1's uuid\n" +
            "\t\t\tdelete {{@list}::%{_u}%::*}\n" +
            "\t\t\tmessage \"{@prefix} {@resetmsg}\" to player\n" +
            "\t\t\tkick arg-1 due to \"{@prefix} {@passwordreset}\"\n" +
            "\t\telse:\n" +
            "\t\t\tmessage \"{@prefix} {@noperm}\" to player\n" +
            "on command:\n" +
            "\tif command executor is player:\n" +
            "\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on packet:\n" +
            "\tif event-string is \"PacketPlayInChat\":\n" +
            "\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "\t\tset {_m} to \"%packet field \"\"a\"\"%\"\n" +
            "\t\tif \"%loginstartsWith({_m}, \"\"/login\"\")% %loginstartsWith({_m}, \"\"/login \"\")%\" contains \"true\":\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tif loginstartsWith({_m}, \"/login \"):\n" +
            "\t\t\t\treplace all \"/login \" in {_m} with \"\"\n" +
            "\t\t\t\tif {{@list}::%player's uuid%::password} is set:\n" +
            "\t\t\t\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\t\t\t\tloop {{@list}::%player's uuid%::salt::*}:\n" +
            "\t\t\t\t\t\t\tloop split \"{@peppers}\" at \"\":\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::password} is hashed \"%loop-value-1%%loop-value-2%%{_m}%\" using \"{@algorithim}\":\n" +
            "\t\t\t\t\t\t\t\t\tset {{@list}::%player's uuid%::status} to true\n" +
            "\t\t\t\t\t\t\t\t\tdelete player's vehicle\n" +
            "\t\t\t\t\t\t\t\t\tdelete {{@list}::%player's uuid%::tries}\n" +
            "\t\t\t\t\t\t\t\t\tsync:\n" +
            "\t\t\t\t\t\t\t\t\t\tset yaw of {{@list}::%player's uuid%::location} to player's yaw\n" +
            "\t\t\t\t\t\t\t\t\t\tset pitch of {{@list}::%player's uuid%::location} to player's pitch\n" +
            "\t\t\t\t\t\t\t\t\t\tteleport player to {{@list}::%player's uuid%::location}\n" +
            "\t\t\t\t\t\t\t\t\tmessage \"{@prefix} {@nowloggedin}\" to player\n" +
            "\t\t\t\t\t\t\t\t\tcall custom event \"{@event-login}\" to details player\n" +
            "\t\t\t\t\t\t\t\t\tstop loop\n" +
            "\t\t\t\t\t\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\t\t\t\t\t\tmessage \"{@prefix} {@incorrectpass}\" to player\n" +
            "\t\t\t\t\t\t\tcall custom event \"{@event-incorrect}\" to details player\n" +
            "\t\t\t\t\t\t\tif {@kickfortries} is true:\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::tries} is not set:\n" +
            "\t\t\t\t\t\t\t\t\tset {{@list}::%player's uuid%::tries} to 1\n" +
            "\t\t\t\t\t\t\t\telse:\n" +
            "\t\t\t\t\t\t\t\t\tadd 1 to {{@list}::%player's uuid%::tries}\n" +
            "\t\t\t\t\t\t\t\tif {{@list}::%player's uuid%::tries} is greater than or equal to {@maxtries}:\n" +
            "\t\t\t\t\t\t\t\t\tsync:\n" +
            "\t\t\t\t\t\t\t\t\t\tkick player due to \"{@prefix} {@toomanytries}\"\n" +
            "\t\t\t\t\t\t\t\t\t\tcall custom event \"{@event-kick}\" to details player\n" +
            "\t\t\t\t\telse:\n" +
            "\t\t\t\t\t\tmessage \"{@prefix} {@alreadyloggedin}\"\n" +
            "\t\t\t\telse:\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@mustregister}\"\n" +
            "\t\t\telse:\n" +
            "\t\t\t\tmessage \"{@prefix} {@specifyapassword}\"\n" +
            "\t\telse if \"%loginstartsWith({_m}, \"\"/register\"\")% %loginstartsWith({_m}, \"\"/register \"\")%\" contains \"true\":\n" +
            "\t\t\tcancel the event\n" +
            "\t\t\tif loginstartsWith({_m}, \"/register \"):\n" +
            "\t\t\t\tif {{@list}::%player's uuid%::password} is not set:\n" +
            "\t\t\t\t\treplace all \"/register \" in {_m} with \"\"\n" +
            "\t\t\t\t\tloop {@salts} times:\n" +
            "\t\t\t\t\t\tadd random 20 char string from `a-zA-Z0-9` to {{@list}::%player's uuid%::salt::*}\n" +
            "\t\t\t\t\tset {_ss} to a random element out of {{@list}::%player's uuid%::salt::*}\n" +
            "\t\t\t\t\tset {{@list}::%player's uuid%::password} to hashed \"%{_ss}%%a random element out of split \"\"{@peppers}\"\" at \"\"\"\"%%{_m}%\" using \"{@algorithim}\"\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@passwordset}\"\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@nowloggedin}\"\n" +
            "\t\t\t\t\tset {{@list}::%player's uuid%::status} to true\n" +
            "\t\t\t\t\tdelete player's vehicle\n" +
            "\t\t\t\t\tcall custom event \"{@event-register}\" to details player\n" +
            "\t\t\t\telse:\n" +
            "\t\t\t\t\tmessage \"{@prefix} {@alreadyhavepass}\"\n" +
            "\t\t\telse:\n" +
            "\t\t\t\tmessage \"{@prefix} {@specifyapassword}\"\n" +
            "on quit:\n" +
            "\tdelete {{@list}::%player's uuid%::tries}\n" +
            "\tset {{@list}::%player's uuid%::lastlogin} to now\n" +
            "\tset {{@list}::%player's uuid%::lastip} to hashed player's ip\n" +
            "\tif metadata value \"loginStand\" of player's vehicle is true:\n" +
            "\t\tdelete player's vehicle\n" +
            "on join:\n" +
            "\tif difference between {{@list}::%player's uuid%::lastlogin} and now is greater than or equal to {@timeout} seconds:\n" +
            "\t\tdelete {{@list}::%player's uuid%::status}\n" +
            "\telse if hashed player's ip is not {{@list}::%player's uuid%::lastip}:\n" +
            "\t\tif {@ipchange} is true:\n" +
            "\t\t\tdelete {{@list}::%player's uuid%::status}\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tset {{@list}::%player's uuid%::location} to location of player\n" +
            "\t\twait 20 ticks\n" +
            "\t\tloginVehicle(player)\n" +
            "\t\tif {{@list}::%player's uuid%::password} is not set:\n" +
            "\t\t\tmessage \"{@prefix} {@nopassword}\" to player\n" +
            "\t\telse:\n" +
            "\t\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on click:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on drop:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on pick up:\n" +
            "\tif {{@list}::%player's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "\t\tmessage \"{@prefix} {@havetologin}\" to player\n" +
            "on damage:\n" +
            "\tif attacker is a player:\n" +
            "\t\tif {{@list}::%attacker's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "on damage of player:\n" +
            "\tif {{@list}::%victim's uuid%::status} is not set:\n" +
            "\t\tcancel the event\n" +
            "on entity target:\n" +
            "\tif targeted entity is a player:\n" +
            "\t\tif {{@list}::%targeted entity's uuid%::status} is not set:\n" +
            "\t\t\tcancel the event\n" +
            "on packet:\n" +
            "\tif event-string is \"PacketPlayInSteerVehicle\" where [metadata value \"loginStand\" of player's vehicle is true]:\n" +
            "\t\tcancel the event\n";
}

@liz3
Copy link
Author

liz3 commented Apr 16, 2018

Yes, thank you very very much, the workaround for the bug works fine!!!!!

Best Regards,
Liz3 Yann HN

@JordanMartinez
Copy link
Contributor

JordanMartinez commented Apr 26, 2018

I'm pretty confident that the underlying issue here is Flowless. Here are my reasons:

  • I don't understand why calling setStyleSpans twice would be a problem. The way the code is set up, it doesn't cause an infinite loop (where a setStyleSpans call triggers another runHightlighting call, which calls setStyleSpans).
  • Changing the height in your test can make it pass (237) or fail (236).
  • Calling requestFollowCaret even when using a failing height will make the test pass.
  • the area's estimatedScrollY value, when a change listener is added to it in the start method and requestFollowCaret is not called, will oscillate between three values: a constant A value, a constant B value, and a value that is continuously getting closer to 0, probably with the height amount of a regular line. This situation stops once the test method actually starts.

@JordanMartinez
Copy link
Contributor

I also suspect it may have something to do with FXMisc/Flowless#60

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants