blob: 044280c1c2c26f963833f69b45f4dd4add41c761 [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.idea.blaze.base.lang.buildfile.editor;
import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
import com.google.idea.blaze.base.lang.buildfile.psi.*;
import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
import com.intellij.ide.DataManager;
import com.intellij.injected.editor.EditorWindow;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actions.SplitLineAction;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.util.text.CharArrayUtil;
import javax.annotation.Nullable;
/**
* Inserts indents as appropriate when enter is pressed.<br>
* This is a substitute for implementing a full FormattingModel for the BUILD language.
* If we ever decide to do that, this code should be removed.
*/
public class BuildEnterHandler extends EnterHandlerDelegateAdapter {
@Override
public Result preprocessEnter(PsiFile file,
Editor editor,
Ref<Integer> caretOffset,
Ref<Integer> caretAdvance,
DataContext dataContext,
EditorActionHandler originalHandler) {
int offset = caretOffset.get();
if (editor instanceof EditorWindow) {
file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
editor = InjectedLanguageUtil.getTopLevelEditor(editor);
offset = editor.getCaretModel().getOffset();
}
if (!isApplicable(file, dataContext)) {
return Result.Continue;
}
// Previous enter handler's (e.g. EnterBetweenBracesHandler) can introduce a mismatch
// between the editor's caret model and the offset we've been provided with.
editor.getCaretModel().moveToOffset(offset);
Document doc = editor.getDocument();
PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
CodeStyleSettings currentSettings = CodeStyleSettingsManager.getSettings(file.getProject());
IndentOptions indentOptions = currentSettings.getIndentOptions(file.getFileType());
Integer indent = determineIndent(file, editor, offset, indentOptions);
if (indent == null) {
return Result.Continue;
}
removeTrailingWhitespace(doc, file, offset);
originalHandler.execute(editor, editor.getCaretModel().getCurrentCaret(), dataContext);
LogicalPosition position = editor.getCaretModel().getLogicalPosition();
if (position.column == indent) {
return Result.Stop;
}
if (position.column > indent) {
//default enter handler has added too many spaces -- remove them
int excess = position.column - indent;
doc.deleteString(editor.getCaretModel().getOffset() - excess, editor.getCaretModel().getOffset());
} else if (position.column < indent) {
String spaces = StringUtil.repeatSymbol(' ', indent - position.column);
doc.insertString(editor.getCaretModel().getOffset(), spaces);
}
editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(position.line, indent));
return Result.Stop;
}
private static void removeTrailingWhitespace(Document doc, PsiFile file, int offset) {
CharSequence chars = doc.getCharsSequence();
int start = offset;
while (offset < chars.length() && chars.charAt(offset) == ' ') {
PsiElement element = file.findElementAt(offset);
if (element == null || !(element instanceof PsiWhiteSpace)) {
break;
}
offset++;
}
if (start != offset) {
doc.deleteString(start, offset);
}
}
private static boolean isApplicable(PsiFile file, DataContext dataContext) {
if (!(file instanceof BuildFile)) {
return false;
}
Boolean isSplitLine = DataManager.getInstance().loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY);
if (isSplitLine != null) {
return false;
}
return true;
}
/**
* Returns null if an appropriate indent cannot be found. In that case we do nothing,
* and pass it along to the next EnterHandler.
*/
@Nullable
private static Integer determineIndent(PsiFile file, Editor editor, int offset, IndentOptions indentOptions) {
if (offset == 0) {
return null;
}
Document doc = editor.getDocument();
PsiElement element = getRelevantElement(file, doc, offset);
PsiElement parent = element != null ? element.getParent() : null;
if (parent == null) {
return null;
}
if (endsBlock(element)) {
// current line indent subtract block indent
return Math.max(0, getIndent(doc, element) - indentOptions.INDENT_SIZE);
}
if (parent instanceof BuildListType) {
BuildListType list = (BuildListType) parent;
int listOffset = list.getStartOffset();
LogicalPosition caretPosition = editor.getCaretModel().getLogicalPosition();
LogicalPosition listStart = editor.offsetToLogicalPosition(listOffset);
if (listStart.line != caretPosition.line) {
// take the minimum of the current line's indent and the current caret position
return indentOfLineUpToCaret(doc, caretPosition.line, offset);
}
BuildElement firstChild = ((BuildListType) parent).getFirstElement();
if (firstChild != null && firstChild.getNode().getStartOffset() < offset) {
return getIndent(doc, firstChild);
}
return lineIndent(doc, listStart.line) + additionalIndent(parent, indentOptions);
}
if (parent instanceof StatementListContainer && afterColon(doc, offset)) {
return getIndent(doc, parent) + additionalIndent(parent, indentOptions);
}
return null;
}
private static int additionalIndent(PsiElement parent, IndentOptions indentOptions) {
return parent instanceof StatementListContainer
? indentOptions.INDENT_SIZE : indentOptions.CONTINUATION_INDENT_SIZE;
}
private static int lineIndent(Document doc, int line) {
int startOffset = doc.getLineStartOffset(line);
int indentOffset = CharArrayUtil.shiftForward(doc.getCharsSequence(), startOffset, " \t");
return indentOffset - startOffset;
}
private static int getIndent(Document doc, PsiElement element) {
int offset = element.getNode().getStartOffset();
int lineNumber = doc.getLineNumber(offset);
return offset - doc.getLineStartOffset(lineNumber);
}
private static int indentOfLineUpToCaret(Document doc, int line, int caretOffset) {
int startOffset = doc.getLineStartOffset(line);
int indentOffset = CharArrayUtil.shiftForward(doc.getCharsSequence(), startOffset, " \t");
return Math.min(indentOffset, caretOffset) - startOffset;
}
private static boolean endsBlock(PsiElement element) {
return element instanceof ReturnStatement
|| element instanceof PassStatement;
}
private static PsiElement getBlockEndingParent(PsiElement element) {
while (element != null && !(element instanceof PsiFileSystemItem)) {
if (endsBlock(element)) {
return element;
}
element = element.getParent();
}
return null;
}
@Nullable
private static PsiElement getRelevantElement(PsiFile file, Document doc, int offset) {
if (offset == 0) {
return null;
}
if (offset == doc.getTextLength()) {
offset--;
}
PsiElement element = file.findElementAt(offset);
while (element != null && isWhiteSpace(element)) {
element = PsiUtils.getPreviousNodeInTree(element);
}
PsiElement blockTerminator = getBlockEndingParent(element);
if (blockTerminator != null
&& blockTerminator.getTextRange().getEndOffset() == element.getTextRange().getEndOffset()) {
return blockTerminator;
}
while (element != null && skipElement(element, offset)) {
element = element.getParent();
}
return element;
}
private static boolean isWhiteSpace(PsiElement element) {
if (element instanceof PsiWhiteSpace) {
return true;
}
return BuildToken.WHITESPACE_AND_NEWLINE.contains(element.getNode().getElementType());
}
private static boolean skipElement(PsiElement element, int offset) {
PsiElement parent = element.getParent();
if (parent == null || parent.getNode() == null || parent instanceof PsiFileSystemItem) {
return false;
}
TextRange childRange = element.getNode().getTextRange();
return childRange.equals(parent.getNode().getTextRange())
|| childRange.getStartOffset() == offset && (parent instanceof Argument || parent instanceof Parameter);
}
private static boolean afterColon(Document doc, int offset) {
CharSequence text = doc.getCharsSequence();
int previousOffset = CharArrayUtil.shiftBackward(text, offset - 1, " \t");
return text.charAt(previousOffset) == ':';
}
}