All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jetbrains.python.editor.PyJoinLinesHandler Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition python-community library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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.jetbrains.python.editor;

import com.intellij.codeInsight.editorActions.JoinRawLinesHandlerDelegate;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static com.jetbrains.python.psi.PyUtil.StringNodeInfo;

/**
 * Joins lines sanely.
 * 
    *
  • statement lines: add a semicolon;
  • *
  • list-like lines: keep one space after comma;
  • *
  • lines inside a multiline string: remove excess indentation;
  • *
  • multi-constant string like "a" "b": join into one;
  • *
  • comment and comment: remove indentation and hash sign;
  • *
  • second line is 'class' or 'def': fail.
  • *
* * @author dcheryasov */ public class PyJoinLinesHandler implements JoinRawLinesHandlerDelegate { private final static Joiner[] JOINERS = { new OpenBracketJoiner(), new CloseBracketJoiner(), new StringLiteralJoiner(), new StmtJoiner(), // strings before stmts to let doc strings join new BinaryExprJoiner(), new CommentJoiner(), new StripBackslashJoiner() }; @Override public int tryJoinLines(Document document, PsiFile file, int start, int end) { return -1; // we go for raw } @Override public int tryJoinRawLines(@NotNull Document document, PsiFile file, int start, int end) { if (!(file instanceof PyFile)) return CANNOT_JOIN; // step back the probable "\" and space before it. final CharSequence text = document.getCharsSequence(); if (start >= 0 && text.charAt(start) == '\n') start -= 1; if (start >= 0 && text.charAt(start) == '\\') start -= 1; while (start >= 0 && text.charAt(start) == ' ' || text.charAt(start) == '\t') { start -= 1; } if (start < 0) { return CANNOT_JOIN; // TODO: join with empty BOF, too } // detect elements around the join final PsiElement leftElement = file.findElementAt(start); final PsiElement rightElement = file.findElementAt(end); if (leftElement != null && rightElement != null) { final PyExpression leftExpr = PsiTreeUtil.getParentOfType(leftElement, PyExpression.class); final PyExpression rightExpr = PsiTreeUtil.getParentOfType(rightElement, PyExpression.class); final Request request = new Request(document, start, end, leftElement, leftExpr, rightElement, rightExpr); for (Joiner joiner : JOINERS) { final Result res = joiner.join(request); if (res != null) { final int cutStart = start + 1 - res.cutFromLeft; document.replaceString(cutStart, end + res.cutIntoRight, res.replacement); return cutStart + res.caretOffset; } } } return CANNOT_JOIN; } // a dumb immutable result holder private static class Result { final String replacement; final int caretOffset; final int cutFromLeft; final int cutIntoRight; /** * Result of a join operation. * * @param replacement: what string to insert at start position * @param cursorOffset: how to move cursor relative to start (0 = stand at start) */ Result(@NotNull String replacement, int cursorOffset) { this(replacement, cursorOffset, 0, 0); } /** * Result of a join operation. * * @param replacement what to insert into the cut place * @param cursorOffset where to put cursor, relative to the start cursorOffset of cutting * @param cutFromLeft how many chars to cut from the end on left string, >0 moves start cursorOffset of cutting to the left. * @param cutIntoRight how many chars to cut from the beginning on right string, >0 moves start cursorOffset of cutting to the right. */ Result(@NotNull String replacement, int cursorOffset, int cutFromLeft, int cutIntoRight) { this.cutFromLeft = cutFromLeft; this.cutIntoRight = cutIntoRight; this.replacement = replacement; caretOffset = cursorOffset; } } // a dumb immutable request items holder private static class Request { final Document document; final PsiElement leftElem; final PsiElement rightElem; final PyExpression leftExpr; final PyExpression rightExpr; final int secondLineStartOffset; final int firstLineEndOffset; private Request(@NotNull Document document, int firstLineEndOffset, int secondLineStartOffset, @NotNull PsiElement leftElem, @Nullable PyExpression leftExpr, @NotNull PsiElement rightElem, @Nullable PyExpression rightExpr) { this.document = document; this.firstLineEndOffset = firstLineEndOffset; this.secondLineStartOffset = secondLineStartOffset; this.leftElem = leftElem; this.rightElem = rightElem; this.leftExpr = leftExpr; this.rightExpr = rightExpr; } } private interface Joiner { /** * Try to join lines. * * @param req@return null if cannot join, or ("what to insert", cursor_offset). */ @Nullable Result join(@NotNull Request req); } private static class OpenBracketJoiner implements Joiner { private static final TokenSet OPENS = TokenSet.create(PyTokenTypes.LBRACKET, PyTokenTypes.LBRACE, PyTokenTypes.LPAR); @Override public Result join(@NotNull Request req) { if (OPENS.contains(req.leftElem.getNode().getElementType())) { // TODO: look at settings for space after opening paren return new Result("", 0); } return null; } } private static class CloseBracketJoiner implements Joiner { private static final TokenSet CLOSES = TokenSet.create(PyTokenTypes.RBRACKET, PyTokenTypes.RBRACE, PyTokenTypes.RPAR); @Override public Result join(@NotNull Request req) { if (CLOSES.contains(req.rightElem.getNode().getElementType())) { // TODO: look at settings for space before closing paren return new Result("", 0); } return null; } } private static class BinaryExprJoiner implements Joiner { @Override public Result join(@NotNull Request req) { if (req.leftExpr instanceof PyBinaryExpression || req.rightExpr instanceof PyBinaryExpression) { // TODO: look at settings for space around binary exprs return new Result(" ", 1); } return null; } } private static class StmtJoiner implements Joiner { @Override public Result join(@NotNull Request req) { final PyStatement leftStmt = PsiTreeUtil.getParentOfType(req.leftExpr, PyStatement.class); if (leftStmt != null) { final PyStatement rightStmt = PsiTreeUtil.getParentOfType(req.rightExpr, PyStatement.class); if (rightStmt != null && rightStmt != leftStmt) { // TODO: look at settings for space after semicolon return new Result("; ", 1); // cursor after semicolon } } return null; } } private static class StringLiteralJoiner implements Joiner { @Override public Result join(@NotNull Request req) { if (req.leftElem != req.rightElem) { final PsiElement parent = req.rightElem.getParent(); if ((req.leftElem.getParent() == parent && parent instanceof PyStringLiteralExpression) || (req.leftExpr instanceof PyStringLiteralExpression && req.rightExpr instanceof PyStringLiteralExpression)) { // two quoted strings close by final CharSequence text = req.document.getCharsSequence(); final StringNodeInfo leftNodeInfo = new StringNodeInfo(req.leftElem); final StringNodeInfo rightNodeInfo = new StringNodeInfo(req.rightElem); if (leftNodeInfo.isTerminated() && rightNodeInfo.isTerminated()) { final int rightNodeContentOffset = rightNodeInfo.getContentRange().getStartOffset(); if (leftNodeInfo.equals(rightNodeInfo)) { return new Result("", 0, leftNodeInfo.getQuote().length(), rightNodeContentOffset); } if (haveSamePrefixes(leftNodeInfo, rightNodeInfo) && !leftNodeInfo.isTripleQuoted() && !rightNodeInfo.isTripleQuoted()) { // maybe fit one literal's quotes to match other's if (!rightNodeInfo.getContent().contains(leftNodeInfo.getQuote())) { final int quotePos = rightNodeInfo.getAbsoluteContentRange().getEndOffset(); req.document.replaceString(quotePos, quotePos + 1, leftNodeInfo.getQuote()); return new Result("", 0, 1, rightNodeContentOffset); } else if (!leftNodeInfo.getContent().contains(rightNodeInfo.getQuote())) { final int quotePos = leftNodeInfo.getAbsoluteContentRange().getStartOffset() - 1; req.document.replaceString(quotePos, quotePos + 1, rightNodeInfo.getQuote()); return new Result("", 0, 1, rightNodeContentOffset); } } } } } return null; } private static boolean haveSamePrefixes(@NotNull StringNodeInfo leftNodeInfo, @NotNull StringNodeInfo rightNodeInfo) { return leftNodeInfo.isUnicode() == rightNodeInfo.isUnicode() && leftNodeInfo.isRaw() == rightNodeInfo.isRaw() && leftNodeInfo.isBytes() == rightNodeInfo.isBytes(); } protected static boolean containsChar(@NotNull CharSequence text, @NotNull TextRange range, char c) { return StringUtil.contains(text, range.getStartOffset(), range.getEndOffset(), c); } } private static class CommentJoiner implements Joiner { @Override public Result join(@NotNull Request req) { if (req.leftElem instanceof PsiComment && req.rightElem instanceof PsiComment) { final CharSequence text = req.document.getCharsSequence(); final TextRange rightRange = req.rightElem.getTextRange(); final int initialPos = rightRange.getStartOffset() + 1; int pos = initialPos; // cut '#' final int last = rightRange.getEndOffset(); while (pos < last && " \t".indexOf(text.charAt(pos)) >= 0) pos += 1; final int right = pos - initialPos + 1; // account for the '#' return new Result(" ", 0, 0, right); } return null; } } private static class StripBackslashJoiner implements Joiner { static final TokenSet SINGLE_QUOTED_STRINGS = TokenSet.create(PyTokenTypes.SINGLE_QUOTED_STRING, PyTokenTypes.SINGLE_QUOTED_UNICODE); @Nullable @Override public Result join(@NotNull Request req) { final String gap = req.document.getText(new TextRange(req.firstLineEndOffset + 1, req.secondLineStartOffset)); final int index = gap.indexOf('\\'); if (index >= 0) { if (req.leftElem == req.rightElem && SINGLE_QUOTED_STRINGS.contains(req.leftElem.getNode().getElementType())) { return new Result(gap.replaceFirst("\\\\\\n", ""), 0); } else { return new Result(gap.substring(0, index), 0); } } return null; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy