/* * 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); } } }