/** * Copyright (C) 2015 drrb * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package com.github.drrb.rust.netbeans.formatting; import com.github.drrb.rust.netbeans.parsing.NetbeansRustParser.NetbeansRustParserResult; import com.github.drrb.rust.netbeans.parsing.RustTokenId; import java.util.LinkedList; import java.util.List; import javax.swing.text.BadLocationException; import javax.swing.text.Position; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.editor.BaseDocument; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.modules.editor.indent.spi.Context; import org.netbeans.modules.parsing.api.Snapshot; import org.openide.util.Exceptions; /** * */ public class RustDocumentFormatter { private final RustFormatter formatter; private final NetbeansRustParserResult parseResult; private final BaseDocument document; private final Context context; RustDocumentFormatter(RustFormatter formatter, NetbeansRustParserResult parseResult, BaseDocument document, Context context) { this.formatter = formatter; this.parseResult = parseResult; this.document = document; this.context = context; } public void format() { final Snapshot snapshot = parseResult.getSnapshot(); try { List<Delimiter> delimiters = new LinkedList<>(); TokenHierarchy<?> tokenHierarchy = snapshot.getTokenHierarchy(); TokenSequence<RustTokenId> tokenSequence = tokenHierarchy.tokenSequence(RustTokenId.language()); tokenSequence.move(0); while (tokenSequence.moveNext()) { Token<RustTokenId> token = tokenSequence.token(); int tokenOffset = tokenSequence.offset(); if (token.id() == RustTokenId.OPEN_BRACE) { delimiters.add(new Delimiter(DelimiterType.OPEN_BRACE, tokenOffset)); } else if (token.id() == RustTokenId.CLOSE_BRACE) { delimiters.add(new Delimiter(DelimiterType.CLOSE_BRACE, tokenOffset)); } else if (token.id() == RustTokenId.SEMI) { delimiters.add(new Delimiter(DelimiterType.SEMICOLON, tokenOffset)); } } int depth = 0; // We need these, because context.endOffset() doesn't update if we modify the document directly (i.e. not through the context object) Position startPosition = document.createPosition(context.startOffset()); Position endPosition = document.createPosition(context.endOffset()); for (Delimiter delimiter : delimiters) { depth += delimiter.type.depthChangeBefore; int nextDepth = depth + delimiter.type.depthChangeAfter; if (delimiter.offset() >= startPosition.getOffset() && delimiter.offset() < endPosition.getOffset()) { delimiter.adjustSurroundings(); delimiter.modifyIndentDepth(depth); //TODO: we wouldn't need this if we added a placeholding delimiter for lines before closing braces with no semicolon if (delimiter.startOfNextLine() >= startPosition.getOffset() && delimiter.startOfNextLine() < endPosition.getOffset()) { delimiter.modifyIndentDepthOfNextLine(nextDepth); } } depth = nextDepth; } } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } public int indentForDepth(int depth) { //TODO: return 0 if depth negative return depth * formatter.indentSize(); } private enum DelimiterType { OPEN_BRACE(" ", "\n", 0, 1), CLOSE_BRACE("\n", "\n", -1, 0), SEMICOLON("", "\n", 0, 0); final String prefix; final String suffix; final int depthChangeBefore; final int depthChangeAfter; DelimiterType(String prefix, String suffix, int depthChangeBefore, int depthChangeAfter) { this.prefix = prefix; this.suffix = suffix; this.depthChangeBefore = depthChangeBefore; this.depthChangeAfter = depthChangeAfter; } } private class Delimiter { private static final int TOKEN_WIDTH = 1; final DelimiterType type; final Position position; private Delimiter(DelimiterType type, int offset) throws BadLocationException { this.type = type; this.position = document.createPosition(offset); } int offset() { return position.getOffset(); } void adjustSurroundings() throws BadLocationException { setSurrounding(type.prefix, type.suffix); } void setSurrounding(String before, String after) throws BadLocationException { replaceLeadingWhitespace(before); replaceTrailingWhitespace(after); } void modifyIndentDepth(int newDepth) throws BadLocationException { context.modifyIndent(context.lineStartOffset(offset()), indentForDepth(newDepth)); } void modifyIndentDepthOfNextLine(int depth) throws BadLocationException { context.modifyIndent(startOfNextLine(), indentForDepth(depth)); } int startOfNextLine() throws BadLocationException { assert type.suffix.equals("\n") : "This only works for delimiter types that are followed by newlines"; assert type.suffix.equals(getText(offset() + TOKEN_WIDTH, type.suffix.length())) : "Trying to find where the next line starts, but doing it dodgily!"; return offset() + TOKEN_WIDTH + type.suffix.length(); } private void replaceLeadingWhitespace(String prefix) throws BadLocationException { final int endOfGap = offset(); int previousCharacterPosition = endOfGap; char nextChar = charAt(previousCharacterPosition - 1); while (Character.isWhitespace(nextChar)) { previousCharacterPosition--; nextChar = charAt(previousCharacterPosition - 1); } final int startOfGap = previousCharacterPosition; final int lengthOfGap = endOfGap - startOfGap; document.replace(startOfGap, lengthOfGap, prefix, null); } private void replaceTrailingWhitespace(String suffix) throws BadLocationException { final int startOfGap = offset() + TOKEN_WIDTH; int endOfGap = startOfGap; for (int i = startOfGap; i < document.getLength() && Character.isWhitespace(charAt(i)); i++) { endOfGap = i + 1; } final int lengthOfGap = endOfGap - startOfGap; document.replace(startOfGap, lengthOfGap, suffix, null); } private char charAt(int nextCharPosition) { return DocumentUtilities.getText(document).charAt(nextCharPosition); } private String getText(int start, int length) throws BadLocationException { return DocumentUtilities.getText(document, start, length).toString(); } } }