/* * Copyright 2011-2017 Kay Stenschke * * 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.kstenschke.shifter.models; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.kstenschke.shifter.ShifterPreferences; import com.kstenschke.shifter.models.shiftableTypes.CssUnit; import com.kstenschke.shifter.models.shiftableTypes.JsDoc; import com.kstenschke.shifter.models.shiftableTypes.NumericValue; import com.kstenschke.shifter.utils.UtilsEnvironment; import com.kstenschke.shifter.utils.UtilsFile; import com.kstenschke.shifter.utils.UtilsTextual; import org.jetbrains.annotations.Nullable; import java.util.regex.Pattern; /** * Shiftable word */ public class ShiftableWord { private final ShiftableTypesManager shiftingShiftableTypesManager; private final String word; private final String filename; // "more" count, starting w/ 1. If non-more shift: null private final Integer moreCount; private final int wordType; private final boolean isShiftable; private final CharSequence editorText; private final int caretOffset; /** * Constructor * * @param word Shiftable word * @param prefixChar Char before the word, "#"? * @param postfixChar Char after the word, "#"? * @param line Whole line to possibly guess the context * @param editorText Whole text currently in editor * @param caretOffset Caret offset in document * @param filename Filename of the edited file * @param moreCount Current "more" count, starting w/ 1. If non-more shift: null */ public ShiftableWord( String word, String prefixChar, String postfixChar, String line, CharSequence editorText, int caretOffset, String filename, @Nullable Integer moreCount ) { this.shiftingShiftableTypesManager = new ShiftableTypesManager(); this.editorText = editorText; this.caretOffset = caretOffset; this.filename = filename; this.moreCount = moreCount; // Detect word type this.wordType = shiftingShiftableTypesManager.getWordType(word, prefixChar, postfixChar, false, line, filename); // Comprehend negative values of numeric shiftableTypes this.word = ( (this.wordType == ShiftableTypesManager.TYPE_CSS_UNIT || this.wordType == ShiftableTypesManager.TYPE_NUMERIC_VALUE) && "-".equals(prefixChar) ) ? "-" + word : word; // Can the word be shifted? this.isShiftable = this.wordType != ShiftableTypesManager.TYPE_UNKNOWN; } /** * Get shifted up/down word * * @param isUp Shift up or down? * @param editor Nullable (required to retrieve offset for positioning info-balloon which isn't shown if editor == null) * @return String Next upper/lower word */ public String getShifted(boolean isUp, @Nullable Editor editor) { if (!this.isShiftable) { return this.word; } String shiftedWord = shiftingShiftableTypesManager.getShiftedWord(word, wordType, isUp, editorText, caretOffset, moreCount, filename, editor); return this.word.equals(shiftedWord) ? word : maintainCasingOnShiftedWord(shiftedWord); } private String maintainCasingOnShiftedWord(String shiftedWord) { if ( this.wordType != ShiftableTypesManager.TYPE_PHP_VARIABLE && this.wordType != ShiftableTypesManager.TYPE_QUOTED_STRING && ShifterPreferences.getIsActivePreserveCase() ) { if (UtilsTextual.isAllUppercase(this.word)) { return shiftedWord.toUpperCase(); } if (UtilsTextual.isUcFirst(this.word)) { return UtilsTextual.toUcFirst(shiftedWord, false); } if (UtilsTextual.isLcFirst(this.word)) { return UtilsTextual.toLcFirst(shiftedWord); } } return shiftedWord; } /** * Post-process: do additional modifications on word after it has been shifted * * @param word * @param postfix * @return String Post-processed word */ private String postProcess(String word, String postfix) { if (UtilsFile.isCssFile(this.filename)) { switch (this.wordType) { // "0" was shifted to a different numeric value, inside a CSS file, so we can add a measure unit case ShiftableTypesManager.TYPE_NUMERIC_VALUE: if (!CssUnit.isCssUnit(postfix)) { return word + CssUnit.determineMostProminentUnit(this.editorText.toString()); } break; case ShiftableTypesManager.TYPE_CSS_UNIT: // Correct "0px" (or other unit) to "0" if (word.startsWith("0")) { return "0"; } break; default: return word; } } return word; } /** * Get shifted word (and replace if possible) * * @param shiftUp * @param line * @param moreCount Current "more" count, starting w/ 1. If non-more shift: null * @return boolean */ public static boolean shiftWordAtCaretInDocument(Editor editor, Integer caretOffset, boolean shiftUp, String line, @Nullable Integer moreCount) { Document document = editor.getDocument(); CharSequence editorText = document.getCharsSequence(); String filename = UtilsEnvironment.getDocumentFilename(document); String fileExtension = UtilsFile.extractFileExtension(filename, true); boolean isOperator = false; String word = UtilsTextual.getOperatorAtOffset(editorText, caretOffset); if (word == null) { boolean isCSS = fileExtension != null && fileExtension.endsWith("css"); word = UtilsTextual.getWordAtOffset(editorText, caretOffset, isCSS); } else { isOperator = true; } if (word == null || word.isEmpty()) { return false; } if (fileExtension.endsWith("js") && shiftWordAtCaretInJsDocument(document, caretOffset, line, word)) { return true; } boolean isWordShifted = !getShiftedWordInDocument(editor, shiftUp, filename, word, line, null, true, isOperator, moreCount).equals(word); if (!isWordShifted) { // Shifting failed, try shifting lower-cased string String wordLower = word.toLowerCase(); isWordShifted = !getShiftedWordInDocument(editor, shiftUp, filename, wordLower, line, null, true, false, moreCount).equals(wordLower); } return isWordShifted; } @Nullable private static Boolean shiftWordAtCaretInJsDocument(Document document, int caretOffset, String line, String word) { if (JsDoc.isAtParamLine(line) && !JsDoc.containsCompounds(line) && JsDoc.isDataType(word)) { // Add missing curly brackets around data type at caret in jsDoc @param line return JsDoc.addCompoundsAroundDataTypeAtCaretInDocument(word, document, caretOffset); } if (JsDoc.isInvalidAtReturnsLine(line)) { return JsDoc.correctInvalidReturnsCommentInDocument(document, caretOffset); } return false; } /** * @param shiftUp shiftUp ...or down? * @param filename * @param word * @param line * @param wordOffset null = calculate from word at offset * @param replaceInDocument * @param isOperator * @param moreCount current "more" count, starting w/ 1. If non-more shift: null * @return String resulting shifted or original word if no shift-ability was found */ public static String getShiftedWordInDocument( Editor editor, boolean shiftUp, String filename, String word, String line, @Nullable Integer wordOffset, Boolean replaceInDocument, boolean isOperator, @Nullable Integer moreCount ) { Document document = editor.getDocument(); CharSequence editorText = document.getCharsSequence(); int caretOffset = editor.getCaretModel().getOffset(); if (wordOffset == null) { // Extract offset of word at caret wordOffset = isOperator ? UtilsTextual.getStartOfOperatorAtOffset(editorText, caretOffset) : UtilsTextual.getStartOfWordAtOffset(editorText, caretOffset); } String prefixChar = UtilsTextual.getCharBeforeOffset(editorText, wordOffset); String postfixChar = UtilsTextual.getCharAfterOffset(editorText, wordOffset + word.length() - 1); // Identify word type and shift it accordingly ShiftableWord shiftableShiftableWord = new ShiftableWord(word, prefixChar, postfixChar, line, editorText, caretOffset, filename, moreCount); if (!isOperator && (NumericValue.isNumericValue(word) || CssUnit.isCssUnitValue(word)) && "-".equals(prefixChar)) { word = "-" + word; wordOffset--; } String newWord = shiftableShiftableWord.getShifted(shiftUp, editor); if (newWord != null && newWord.length() > 0 && !newWord.matches(Pattern.quote(word)) && wordOffset != null) { newWord = shiftableShiftableWord.postProcess(newWord, postfixChar); if (replaceInDocument) { // Replace word at caret by shifted one (if any) document.replaceString(wordOffset, wordOffset + word.length(), newWord); } return newWord; } return word; } }