FXMisc / RichTextFX

Rich-text area for JavaFX
BSD 2-Clause "Simplified" License
1.2k stars 237 forks source link

How can I implement some code folding between brackets? #1150

Open garawaa opened 1 year ago

garawaa commented 1 year ago

I just want to implement code folding just like other ide-s : intellij, eclipse, netbeans. I reviewed java keyboard sample. The folding is controlled from context menu. I want indicate fold unfold icon on the line indicator? How to do it with codearea? Can you give me some code hints? Thank you.

Jugen commented 1 year ago

If you are using either CodeArea, InlineCssTextArea, or StyleClassedTextArea then just use the following API:

foldText( int start, int end )
foldSelectedParagraphs()  // paragraph in this context is a line/block of text ending with a line break.
foldParagraphs( int startPar, int endPar )
unfoldParagraphs​( int startingFromPar )
unfoldText​( int startingFromPos )

You can also look at JavaKeywordsDemo that uses a context menu to demo folding.

Jugen commented 1 year ago

I want indicate fold unfold icon on the line indicator?

I think you'll have to provide your own custom LineNumberFactory and then add it to the area with codeArea.setParagraphGraphicFactory( .... )

garawaa commented 1 year ago

Can you give me some example implementation of custom LineNumberFactory class?

garawaa commented 1 year ago

If you are using either CodeArea, InlineCssTextArea, or StyleClassedTextArea then just use the following API:

foldText( int start, int end )
foldSelectedParagraphs()  // paragraph in this context is a line/block of text ending with a line break.
foldParagraphs( int startPar, int endPar )
unfoldParagraphs​( int startingFromPar )
unfoldText​( int startingFromPos )

You can also look at JavaKeywordsDemo that uses a context menu to demo folding.

I know this example. This works just for selected text. But I need to similar functionality for a method folding or unfolding

Jugen commented 1 year ago

Here is a custom LineNumberFactory implementation for you:

import java.util.function.IntFunction;
import java.util.function.Predicate;

import org.fxmisc.richtext.InlineCssTextArea;
import org.reactfx.collection.LiveList;
import org.reactfx.value.Val;

import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;

/**
 * Graphic factory that produces labels containing line numbers and a "+" to indicate folded paragraphs.
 * To customize appearance, use {@code .lineno} and {@code .fold-indicator} style classes in CSS stylesheets.
 */
public class InlineCssLineNumberFactory<PS> implements IntFunction<Node> {

    private static final Insets DEFAULT_INSETS = new Insets(0.0, 5.0, 0.0, 5.0);
    private static final Paint DEFAULT_TEXT_FILL = Color.web("#666");
    private static final Font DEFAULT_FONT = Font.font("monospace", FontPosture.ITALIC, 13);
    private static final Font DEFAULT_FOLD_FONT = Font.font("monospace", FontWeight.BOLD, 13);
    private static final Background DEFAULT_BACKGROUND = new Background(new BackgroundFill(Color.web("#ddd"), null, null));

    public static IntFunction<Node> get(InlineCssTextArea area) {
        return get(area, digits -> "%1$" + digits + "s");
    }

    /**
     * @param <PS> The paragraph style type being used by the text area
     * @param format Given an int convert to a String for the line number.
     */
    public static <PS> IntFunction<Node> get( InlineCssTextArea area, IntFunction<String> format )
    {
        return new InlineCssLineNumberFactory<>( area, format );
    }

    private final Val<Integer> nParagraphs;
    private final IntFunction<String> format;
    private final InlineCssTextArea area;
    private final Predicate<String> isFoldedCheck = pstyle -> pstyle != null && pstyle.contains( "collapse" );

    private InlineCssLineNumberFactory( InlineCssTextArea area, IntFunction<String> format )
    {
        nParagraphs = LiveList.sizeOf(area.getParagraphs());
        this.format = format;
        this.area = area;

        area.getParagraphs().sizeProperty().addListener( (ob,ov,nv) -> {
            if ( nv <= ov ) Platform.runLater( () -> deleteParagraphCheck() );
            else  Platform.runLater( () -> insertParagraphCheck() );
        });
    }

    @Override
    public Node apply(int idx) {
        Val<String> formatted = nParagraphs.map(n -> format(idx+1, n));

        Label lineNo = new Label();
        lineNo.setFont(DEFAULT_FONT);
        lineNo.setBackground(DEFAULT_BACKGROUND);
        lineNo.setTextFill(DEFAULT_TEXT_FILL);
        lineNo.setPadding(DEFAULT_INSETS);
        lineNo.setAlignment(Pos.TOP_RIGHT);
        lineNo.getStyleClass().add("lineno");

        // bind label's text to a Val that stops observing area's paragraphs
        // when lineNo is removed from scene
        lineNo.textProperty().bind(formatted.conditionOnShowing(lineNo));

        Label foldIndicator = new Label( " " );
        foldIndicator.setTextFill( Color.BLUE ); // Prevents CSS errors
        foldIndicator.setFont( DEFAULT_FOLD_FONT );

        lineNo.setContentDisplay( ContentDisplay.RIGHT );
        lineNo.setGraphic( foldIndicator );

        if ( area.getParagraphs().size() > idx+1 ) {
            if ( isFoldedCheck.test( area.getParagraph( idx+1 ).getParagraphStyle() )
            && ! isFoldedCheck.test( area.getParagraph( idx ).getParagraphStyle() ) ) {
                foldIndicator.setOnMouseClicked( ME -> area.unfoldParagraphs( idx ) );
                foldIndicator.getStyleClass().add( "fold-indicator" );
                foldIndicator.setCursor( Cursor.HAND );
                foldIndicator.setText( "+" );
            }
        }

        return lineNo;
    }

    private String format(int x, int max) {
        int digits = (int) Math.floor(Math.log10(max)) + 1;
        return String.format(format.apply(digits), x);
    }

    private void deleteParagraphCheck()
    {
        int p = area.getCurrentParagraph();
        // Was the deleted paragraph in the viewport ?
        if ( p >= area.firstVisibleParToAllParIndex() && p <= area.lastVisibleParToAllParIndex() )
        {
            int col = area.getCaretColumn();
            // Delete was pressed on an empty paragraph, and so the cursor is now at the start of the next paragraph.
            if ( col == 0 ) {
                // Check if the now current paragraph is folded.
                if ( isFoldedCheck.test( area.getParagraph( p ).getParagraphStyle() ) ) {
                    p = Math.max( p-1, 0 );              // Adjust to previous paragraph.
                    area.recreateParagraphGraphic( p );  // Show fold/unfold indicator on previous paragraph.
                    area.moveTo( p, 0 );                 // Move cursor to previous paragraph.
                }
            }
            // Backspace was pressed on an empty paragraph, and so the cursor is now at the end of the previous paragraph.
            else if ( col == area.getParagraph( p ).length() ) {
                area.recreateParagraphGraphic( p ); // Shows fold/unfold indicator on current paragraph if needed.
            }
            // In all other cases the paragraph graphic is created/updated automatically.
        }
    }

    private void insertParagraphCheck()
    {
        int p = area.getCurrentParagraph();
        // Is the inserted paragraph in the viewport ?
        if ( p > area.firstVisibleParToAllParIndex() && p <= area.lastVisibleParToAllParIndex() ) {
            // Check limits, as p-1 and p+1 are accessed ...
            if ( p > 0 && p+1 < area.getParagraphs().size() ) {
                // Now check if the inserted paragraph is before a folded block ?
                if ( isFoldedCheck.test( area.getParagraph( p+1 ).getParagraphStyle() ) ) {
                    area.recreateParagraphGraphic( p-1 ); // Remove the unfold indicator.
                }
            }
        }
    }

}
credmond commented 3 months ago

The "java keywords" foldable demo is all a bit misleading. It has nothing to do with Java or Java code structure, it's just folding/unfolding any randomly selected text.

What the OP was looking for was a way to show in the line number bar (in a CodeArea, not a InlineCssTextArea), collapsible areas, such as methods, etc. Basically like a tree but without the ugly-ness of a standard tree control.