org.tvl.goworks.editor.go.formatting.GoIndentTask.java Source code

Java tutorial

Introduction

Here is the source code for org.tvl.goworks.editor.go.formatting.GoIndentTask.java

Source

/*
 *  Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
 *  All rights reserved.
 *
 *  The source code of this document is proprietary work, and is not licensed for
 *  distribution. For information about licensing, contact Sam Harwell at:
 *      sam@tunnelvisionlabs.com
 */
package org.tvl.goworks.editor.go.formatting;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.classification.TokenTag;
import org.antlr.netbeans.editor.completion.Anchor;
import org.antlr.netbeans.editor.tagging.Tagger;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
import org.antlr.netbeans.editor.text.OffsetRegion;
import org.antlr.netbeans.editor.text.SnapshotPosition;
import org.antlr.netbeans.editor.text.SnapshotPositionRegion;
import org.antlr.netbeans.editor.text.VersionedDocument;
import org.antlr.netbeans.editor.text.VersionedDocumentUtilities;
import org.antlr.netbeans.parsing.spi.ParserData;
import org.antlr.netbeans.parsing.spi.ParserDataOptions;
import org.antlr.netbeans.parsing.spi.ParserTaskManager;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.RuleDependencies;
import org.antlr.v4.runtime.RuleDependency;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenSource;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.works.editor.antlr4.classification.TaggerTokenSource;
import org.antlr.works.editor.antlr4.completion.CaretReachedException;
import org.antlr.works.editor.antlr4.completion.CaretToken;
import org.antlr.works.editor.antlr4.completion.CodeCompletionErrorStrategy;
import org.antlr.works.editor.antlr4.completion.CodeCompletionTokenSource;
import org.antlr.works.editor.antlr4.parsing.ParseTrees;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.modules.editor.indent.spi.Context;
import org.netbeans.modules.editor.indent.spi.ExtraLock;
import org.netbeans.modules.editor.indent.spi.IndentTask;
import org.openide.text.NbDocument;
import org.openide.util.Lookup;
import org.openide.util.NotImplementedException;
import org.tvl.goworks.editor.GoEditorKit;
import org.tvl.goworks.editor.go.GoParserDataDefinitions;
import org.tvl.goworks.editor.go.codemodel.FileModel;
import org.tvl.goworks.editor.go.codemodel.ImportDeclarationModel;
import org.tvl.goworks.editor.go.completion.CodeCompletionGoParser;
import org.tvl.goworks.editor.go.completion.GoForestParser;
import org.tvl.goworks.editor.go.completion.ParserFactory;
import org.tvl.goworks.editor.go.parser.GoParser;

/**
 *
 * @author Sam Harwell
 */
public class GoIndentTask implements IndentTask {
    private static final Logger LOGGER = Logger.getLogger(GoIndentTask.class.getName());

    private final Context context;

    private ParserTaskManager taskManager;
    private DocumentSnapshot snapshot;
    private FileModel fileModel;
    private boolean fileModelDataFailed;

    private GoCodeStyle codeStyle;

    public GoIndentTask(Context context) {
        this.context = context;
    }

    @Override
    public void reindent() throws BadLocationException {
        if (!smartReindent()) {
            fallbackReindent();
        }
    }

    public boolean smartReindent() throws BadLocationException {
        if (!(context.document() instanceof StyledDocument)) {
            return false;
        }

        StyledDocument document = (StyledDocument) context.document();

        taskManager = Lookup.getDefault().lookup(ParserTaskManager.class);
        if (taskManager == null) {
            return false;
        }

        VersionedDocument versionedDocument = VersionedDocumentUtilities.getVersionedDocument(context.document());
        snapshot = versionedDocument.getCurrentSnapshot();
        List<Anchor> anchors = getDynamicAnchorPoints();
        if (anchors == null) {
            return false;
        }

        SnapshotPosition contextEndPosition = new SnapshotPosition(snapshot, context.endOffset());
        SnapshotPosition endPosition = contextEndPosition.getContainingLine().getEndIncludingLineBreak();
        SnapshotPosition endPositionOnLine = contextEndPosition.getContainingLine().getEnd();

        Anchor enclosing = null;
        Anchor previous = null;
        Anchor next = null;

        /*
         * parse the current rule
         */
        for (Anchor anchor : anchors) {
            // TODO: support more anchors
            if (anchor.getRule() != GoParser.RULE_topLevelDecl) {
                continue;
            }

            if (anchor.getSpan().getStartPosition(snapshot).getOffset() <= endPosition.getOffset()) {
                previous = anchor;
                if (anchor.getSpan().getEndPosition(snapshot).getOffset() > endPosition.getOffset()) {
                    enclosing = anchor;
                }
            } else {
                next = anchor;
                break;
            }
        }

        if (previous == null) {
            return false;
        }

        Future<ParserData<Tagger<TokenTag<Token>>>> futureTokensData = taskManager.getData(snapshot,
                GoParserDataDefinitions.LEXER_TOKENS, EnumSet.of(ParserDataOptions.SYNCHRONOUS));
        Tagger<TokenTag<Token>> tagger = null;
        try {
            tagger = futureTokensData != null ? futureTokensData.get().getData() : null;
        } catch (InterruptedException | ExecutionException ex) {
            LOGGER.log(Level.WARNING, "An exception occurred while getting token data.", ex);
        }

        int regionEnd = Math.min(snapshot.length(), endPosition.getOffset() + 1);
        OffsetRegion region;
        if (enclosing != null) {
            region = OffsetRegion.fromBounds(enclosing.getSpan().getStartPosition(snapshot).getOffset(), regionEnd);
        } else {
            // at least for now, include the previous span due to the way error handling places bounds on an anchor
            region = OffsetRegion.fromBounds(previous.getSpan().getStartPosition(snapshot).getOffset(), regionEnd);
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Reindent from anchor region: {0}.", region);
        }

        TaggerTokenSource taggerTokenSource = new TaggerTokenSource(tagger,
                new SnapshotPositionRegion(snapshot, region));
        TokenSource tokenSource = new CodeCompletionTokenSource(endPosition.getOffset(), taggerTokenSource);
        CommonTokenStream tokens = new CommonTokenStream(tokenSource);

        Map<RuleContext, CaretReachedException> parseTrees;
        CodeCompletionGoParser parser = ParserFactory.DEFAULT.getParser(tokens);
        parser.setBuildParseTree(true);
        parser.setErrorHandler(new CodeCompletionErrorStrategy());
        @SuppressWarnings("LocalVariableHidesMemberVariable")
        FileModel fileModel = getFileModel();
        if (fileModel != null) {
            Set<String> packageNames = new HashSet<>();
            for (ImportDeclarationModel model : fileModel.getImportDeclarations()) {
                String name = model.getName();
                if (!name.isEmpty() && !name.equals(".")) {
                    packageNames.add(name);
                }
            }

            parser.setCheckPackageNames(true);
            parser.setPackageNames(packageNames);
        }

        switch (previous.getRule()) {
        case GoParser.RULE_topLevelDecl:
            parseTrees = GoForestParser.INSTANCE.getParseTrees(parser);
            break;

        default:
            parseTrees = null;
            break;
        }

        if (parseTrees == null) {
            return false;
        }

        NavigableMap<Integer, List<Map.Entry<RuleContext, CaretReachedException>>> indentLevels = new TreeMap<>();
        for (Map.Entry<RuleContext, CaretReachedException> parseTree : parseTrees.entrySet()) {
            if (parseTree.getValue() == null) {
                continue;
            }

            ParseTree firstNodeOnLine = findFirstNodeAfterOffset(parseTree.getKey(),
                    endPositionOnLine.getContainingLine().getStart().getOffset());
            if (firstNodeOnLine == null) {
                firstNodeOnLine = parseTree.getValue().getFinalContext();
            }

            if (firstNodeOnLine == null) {
                continue;
            }

            int indentationLevel = getIndent(firstNodeOnLine);

            //                TerminalNode startNode = ParseTrees.getStartNode(parseTree.getKey());
            //                //int startLine = new SnapshotPosition(snapshot, startNode.getSymbol().getStartIndex()).getContainingLine();
            //                int lineStartOffset = context.lineStartOffset(startNode.getSymbol().getStartIndex());
            //                int outerIndent = context.lineIndent(lineStartOffset);

            List<Map.Entry<RuleContext, CaretReachedException>> indentList = indentLevels.get(indentationLevel);
            if (indentList == null) {
                indentList = new ArrayList<>();
                indentLevels.put(indentationLevel, indentList);
            }

            indentList.add(parseTree);
        }

        int indentLevel = !indentLevels.isEmpty() ? indentLevels.firstKey() : 0;
        if (indentLevels.size() > 1) {
            // TODO: resolve multiple possibilities (appears at least with case statements)
        }

        int startLine = NbDocument.findLineNumber(document, context.startOffset());
        int endLine;
        if (context.endOffset() <= context.startOffset()) {
            endLine = startLine;
        } else {
            endLine = NbDocument.findLineNumber(document, context.endOffset() - 1);
        }

        for (int line = startLine; line <= endLine; line++) {
            int currentOffset = NbDocument.findLineOffset(document, startLine);
            context.modifyIndent(currentOffset, indentLevel);
        }

        return true;
    }

    private FileModel getFileModel() {
        if (fileModel == null && !fileModelDataFailed) {
            Future<ParserData<FileModel>> futureFileModelData = taskManager.getData(snapshot,
                    GoParserDataDefinitions.FILE_MODEL,
                    EnumSet.of(ParserDataOptions.ALLOW_STALE, ParserDataOptions.SYNCHRONOUS));
            try {
                fileModel = futureFileModelData != null ? futureFileModelData.get().getData() : null;
                fileModelDataFailed = fileModel != null;
            } catch (InterruptedException | ExecutionException ex) {
                LOGGER.log(Level.WARNING, "An exception occurred while getting the file model.", ex);
                fileModelDataFailed = true;
            }
        }

        return fileModel;
    }

    private List<Anchor> getDynamicAnchorPoints() {
        List<Anchor> anchors;
        Future<ParserData<List<Anchor>>> result = taskManager.getData(snapshot,
                GoParserDataDefinitions.DYNAMIC_ANCHOR_POINTS, EnumSet.of(ParserDataOptions.SYNCHRONOUS));
        try {
            anchors = result != null ? result.get().getData() : null;
        } catch (InterruptedException ex) {
            anchors = null;
        } catch (ExecutionException ex) {
            LOGGER.log(Level.WARNING, "An exception occurred while getting the dynamic anchor points.", ex);
            anchors = null;
        }

        return anchors;
    }

    private void fallbackReindent() throws BadLocationException {
        if (!(context.document() instanceof StyledDocument)) {
            return;
        }

        StyledDocument document = (StyledDocument) context.document();
        int startLine = NbDocument.findLineNumber(document, context.startOffset());

        int endLine;
        if (context.endOffset() <= context.startOffset()) {
            endLine = startLine;
        } else {
            endLine = NbDocument.findLineNumber(document, context.endOffset() - 1);
        }

        int previousIndent;
        if (startLine == 0) {
            previousIndent = 0;
        } else {
            int previousLineOffset = NbDocument.findLineOffset(document, startLine - 1);
            previousIndent = context.lineIndent(previousLineOffset);
        }

        for (int line = startLine; line <= endLine; line++) {
            int currentOffset = NbDocument.findLineOffset(document, startLine);
            int currentIndent = context.lineIndent(currentOffset);
            if (currentIndent == 0 && previousIndent > 0) {
                context.modifyIndent(currentOffset, previousIndent);
            }

            previousIndent = currentIndent;
        }
    }

    @Override
    public ExtraLock indentLock() {
        return null;
    }

    private TerminalNode findFirstNodeAfterOffset(ParseTree tree, int offset) {
        TerminalNode lastNode = ParseTrees.getStopNode(tree);
        if (lastNode == null) {
            return null;
        }

        if (lastNode.getSymbol() instanceof CaretToken) {
            throw new NotImplementedException();
        } else if (lastNode.getSymbol().getStartIndex() < offset) {
            return null;
        }

        if (tree instanceof TerminalNode) {
            return (TerminalNode) tree;
        }

        for (int i = 0; i < tree.getChildCount(); i++) {
            TerminalNode node = findFirstNodeAfterOffset(tree.getChild(i), offset);
            if (node != null) {
                return node;
            }
        }

        return null;
    }

    private GoCodeStyle getCodeStyle() {
        if (codeStyle == null) {
            codeStyle = GoCodeStyle.getDefault(context.document());
        }

        return codeStyle;
    }

    @RuleDependencies({ @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_constDecl, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_typeDecl, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_varDecl, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_importDecl, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_block, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_literalValue, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_structType, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_interfaceType, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_exprSwitchStmt, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_typeSwitchStmt, version = 0),
            @RuleDependency(recognizer = GoParser.class, rule = GoParser.RULE_selectStmt, version = 0), })
    private int getIndent(ParseTree node) throws BadLocationException {
        //        System.out.println(node.toStringTree(parser));
        int nodeLineStart = -1;

        for (ParseTree current = node; current != null; current = current.getParent()) {
            if (!(current instanceof RuleNode)) {
                continue;
            }

            ParserRuleContext ruleContext = (ParserRuleContext) ((RuleNode) current).getRuleContext();
            if (nodeLineStart == -1) {
                nodeLineStart = context.lineStartOffset(ruleContext.start.getStartIndex());
            }

            switch (ruleContext.getRuleIndex()) {
            case GoParser.RULE_constDecl:
            case GoParser.RULE_typeDecl:
            case GoParser.RULE_varDecl:
            case GoParser.RULE_importDecl: {
                TerminalNode leftParen = ruleContext.getToken(GoParser.LeftParen, 0);
                if (leftParen == null) {
                    continue;
                }

                // get the indent of the line where the block starts
                int blockLineOffset = context.lineStartOffset(leftParen.getSymbol().getStartIndex());
                int blockIndent = context.lineIndent(blockLineOffset);
                if (nodeLineStart == blockLineOffset) {
                    return blockIndent;
                }

                if (node instanceof TerminalNode) {
                    // no extra indent if the first node on the line is the closing brace of the block
                    if (node == ruleContext.getToken(GoParser.RightParen, 0)) {
                        return blockIndent;
                    }
                }

                return blockIndent + getCodeStyle().getIndentSize();
            }

            case GoParser.RULE_block:
            case GoParser.RULE_literalValue:
            case GoParser.RULE_structType:
            case GoParser.RULE_interfaceType:
            case GoParser.RULE_exprSwitchStmt:
            case GoParser.RULE_typeSwitchStmt:
            case GoParser.RULE_selectStmt: {
                TerminalNode leftBrace = ruleContext.getToken(GoParser.LeftBrace, 0);
                if (leftBrace == null) {
                    continue;
                }

                // get the indent of the line where the block starts
                int blockLineOffset = context.lineStartOffset(leftBrace.getSymbol().getStartIndex());
                int blockIndent = context.lineIndent(blockLineOffset);
                if (nodeLineStart == blockLineOffset) {
                    return blockIndent;
                }

                if (node instanceof TerminalNode) {
                    // no extra indent if the first node on the line is the closing brace of the block
                    if (node == ruleContext.getToken(GoParser.RightBrace, 0)) {
                        return blockIndent;
                    } else {
                        Token symbol = ((TerminalNode) node).getSymbol();
                        switch (symbol.getType()) {
                        case GoParser.Case:
                        case GoParser.Default:
                            return blockIndent;

                        default:
                            break;
                        }
                    }
                }

                return blockIndent + getCodeStyle().getIndentSize();
            }

            default:
                if (current.getParent() == null) {
                    int outerLineOffset = context.lineStartOffset(ruleContext.start.getStartIndex());
                    int outerIndent = context.lineIndent(outerLineOffset);
                    return outerIndent;
                }

                continue;
            }
        }

        return 0;
    }

    @MimeRegistration(mimeType = GoEditorKit.GO_MIME_TYPE, service = IndentTask.Factory.class)
    public static final class FactoryImpl implements Factory {

        @Override
        public IndentTask createTask(Context context) {
            return new GoIndentTask(context);
        }

    }
}