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

Paragraph Folding #965

Merged
merged 3 commits into from
Oct 5, 2020
Merged

Paragraph Folding #965

merged 3 commits into from
Oct 5, 2020

Conversation

Jugen
Copy link
Collaborator

@Jugen Jugen commented Sep 25, 2020

This PR implements Paragraph Folding, resolves #107 and closes #955

Folded/collapsed paragraphs can be copied, pasted, and saved. A section of text containing a fold can also be folded without losing the contained fold, and each layer only unfolds itself. Any folded/collapsed text is still part of the document and can be manipulated as usual. The LineNumberFactory has also been modified to show a clickable "+" symbol on the line before a fold.

For historical purposes or for whoever cares to scrutinize how this was implemented and to defend any WTF thoughts, some explanatory notes are needed. Rest assured that careful consideration has been given and although not necessarily the best way of accomplishing folding it wasn't just arbitrarily or haphazardly slapped on even though it may appear that way.

If you are familiar with the RichTextFX architecture then you may have thought that the natural place for this feature to be implemented would have been through the Paragraph class. The problem however with adding extra state to Paragraph is that it would then become incompatible with any RichTextFX documents previously saved via the inbuilt Codecs and loading those documents would fail. So in order to maintain maximum backward compatibility and not have the added complexity to also modify the codecs to handle multiple save files an alternative, less elegant and unconventional approach had to be taken.

The idea is to leverage RichTextFX's ability to have/apply paragraph level styling to accomplish folding. The problem with this approach though is that a paragraph style object is generic and not a predefined object of any sort, there's not even an interface. This means that any methods created for folding/unfolding needs to have some function/operator passed to it to manipulate whatever paragraph style object is being used. The four generic methods added to GenericStyledArea are all really just convenience methods doing the heavy-lifting and technically one doesn't have to use them at all (see last paragraph). They are really just API to say "Hey paragraph folding is possible if you're interested".

These four methods in GenericStyledArea are marked as protected because of their generic signatures and have simpler public representations in InlineCssTextArea and StyleClassedTextArea. Any DRY purists/idealists should be aware that code duplication was kept intentionally (:gasp:) in order to aid context (i.e. not have related code separated) and also not to expose methods that have no business being public (which would have had to be the case if an interface with default methods had been used). The duplication resulted as a natural code evolution of enabling LineNumberFactory to support folding.

If you use InlineCssTextArea, StyleClassedTextArea, or CodeArea then you are good to go and can just use the API to fold/collapse paragraphs of text. However if you have extended GenericStyledArea and would like to use folding then you'll need to do some additional coding. First read the JavaDocs for the fold/unfold methods in GenericStyledArea and then see how InlineCssTextArea, StyleClassedTextArea, or RichTextDemo have implemented the necessary function/operators and use that to guide your own implementation. (Also look at LineNumberFactory and demo.richtext.BulletFactory if applicable to your use case.)

You can also just do your own thing from scratch if you want by taking note of the following. Historically paragraph styles are applied to TextFlow objects via the applyParagraphStyle BiConsumer (supplied in the constructor to GenericStyledArea). So essentially the way you get a paragraph to collapse is simply by setting its visibility to false, through the applyParagraphStyle BiConsumer, when a paragraph style indicates to do so. There is a preexisting JavaFX CSS tag for this that has a valid value of "collapse" (equivalent to false). So txtFlow.setStyle("visibility: collapse;") or txtFlow.setStyle("visibility: false;"), or txtFlow.getStyleClass().add("collapse") where .collapse { visibility: false; } would trigger the paragraph to be folded by RichTextFX.

@Col-E
Copy link
Contributor

Col-E commented Sep 25, 2020

Whipped up this quick demo for curly-brace folding in JavaKeywordsDemo, works great 👍

// paragraph-folding: insert folding for method-level curly braces
codeArea.addEventHandler( KeyEvent.KEY_PRESSED, KE ->
{
    if ( KE.isControlDown() && KE.getCode() == KeyCode.F ) {
        int currentParagraph = codeArea.getCurrentParagraph();
        // get position for the last character of the current paragraph
        int paragraphEndPosition = 0;
        for (int i = 0; i <= currentParagraph; i++) {
            paragraphEndPosition += codeArea.getParagraphLength(i) + 1;
        }
        // the starting brace is the last open curly brace up through the cursor's selected paragraph.
        // the ending brace position will need to be computed by balacing open/closed braces.
        int curlyStartPosition = codeArea.getText(0, paragraphEndPosition).lastIndexOf("{");
        int curlyEndPosition = -1;
        // consume the remaining text counting the balance of open-to-closed braces.
        String remainingText = codeArea.getText().substring(curlyStartPosition + 1);
        int balance = 1;
        int off = 0;
        while (true) {
            int nextOpen = remainingText.indexOf("{");
            int nextClosed = remainingText.indexOf("}");
            if ((nextOpen == -1 && nextClosed == -1) || (nextOpen > -1 && nextClosed == -1)) {
                // no folding to be done if no valid open-closed brace patterns are found.
                return;
            } else if (nextClosed > 0) {
                // update brace open-closed balance.
                if (nextOpen == -1) {
                    balance--;
                } else {
                    if (nextOpen < nextClosed) {
                        balance++;
                    } else {
                        balance--;
                    }
                }
                // if we are now balanced, this is the end position.
                // otherwise, continue searching the remaining text.
                if (balance == 0) {
                    curlyEndPosition = curlyStartPosition + off + nextClosed + 1;
                    break;
                } else {
                    int tOff = (nextOpen == -1) ? nextClosed + 1 : Math.min(nextClosed, nextOpen) + 1;
                    remainingText = remainingText.substring(tOff);
                    off += tOff;
                }
            }
        }
        // hide the start position's paragraph up until the end position's paragraph.
        // we still want to see the end position's paragraph to prevent confusion.
        if (curlyEndPosition > curlyStartPosition) {
            codeArea.foldParagraphs(
                    codeArea.offsetToPosition(curlyStartPosition, TwoDimensional.Bias.Backward).getMajor(),
                    codeArea.offsetToPosition(curlyEndPosition, TwoDimensional.Bias.Backward).getMajor() - 1);
        }
    }
});

@Jugen Jugen merged commit 37fe88c into master Oct 5, 2020
@Jugen Jugen deleted the paragraph-folding branch October 5, 2020 07:34
@buko
Copy link

buko commented Oct 7, 2020

We're new to the project but personally I find it unfortunate that complexity is being introduced to maintain backwards complexity. This isn't a 1.0 product... it's a 0.10.5. In my experience keeping complexity out of the core and moving it to the edges -- having codecs that can read/write multiple versions or simply breaking backwards compatibility and perhaps offering a migration tool to the new format -- is almost always a good idea.

@atoktoto
Copy link

atoktoto commented Oct 9, 2020

Thank you for implementing this! It enabled me to continue with my project :) It generally work as intended but I have some observations after using this:

First, there is no obvious way to unfold everything. I ended up doing area.replaceText(area.text) which does not seem to be ideal.

Second, Ctrl+A seems to be broken if there are any folded paragraphs.

Also, the required CSS markup causes Intellj to complain
obraz

@Jugen
Copy link
Collaborator Author

Jugen commented Oct 9, 2020

Yeah, unfolding everything is not built in and there's no real shortcut. Its either replace the text like you are doing or looping through all the paragraphs unfolding each one.

When you say Ctrl+A is broken can you please provide more detail. (I tried it in CodeArea and it seems to work ?)

With regards to Intellij complaining about the CSS you can safely change it to "collapse;"

@atoktoto
Copy link

atoktoto commented Oct 9, 2020

In my case, when the caret is not at the end of the text the Ctrl + A selects the text only up to the caret.
ctrla

@Jugen
Copy link
Collaborator Author

Jugen commented Oct 10, 2020

Okay, so when the very last line is also included in a fold we get this misbehavior.
Exclude the very last line and Ctrl+A works as expected.

@Jugen
Copy link
Collaborator Author

Jugen commented Nov 30, 2020

Added Ctrl+A and Ctrl+End fix.

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

Successfully merging this pull request may close these issues.

can richtextFx fold code area? Feature: Set Lines Visible / Code Folding
4 participants