• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521519

21 Apr 2025 01:57PM UTC coverage: 82.17% (+0.01%) from 82.156%
#525521519

push

github

hurricup
CAMELCADE-22634 Cleanup

8 of 8 new or added lines in 2 files covered. (100.0%)

102 existing lines in 15 files now uncovered.

30868 of 37566 relevant lines covered (82.17%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.83
/plugin/core/src/main/java/com/perl5/lang/perl/idea/editor/smartkeys/PerlTypedHandler.java
1
/*
2
 * Copyright 2015-2025 Alexandr Evstigneev
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 * http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package com.perl5.lang.perl.idea.editor.smartkeys;
18

19
import com.intellij.codeInsight.AutoPopupController;
20
import com.intellij.codeInsight.CodeInsightSettings;
21
import com.intellij.codeInsight.completion.CompletionType;
22
import com.intellij.openapi.editor.CaretModel;
23
import com.intellij.openapi.editor.Document;
24
import com.intellij.openapi.editor.Editor;
25
import com.intellij.openapi.editor.EditorModificationUtilEx;
26
import com.intellij.openapi.editor.ex.EditorEx;
27
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
28
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
29
import com.intellij.openapi.fileTypes.FileType;
30
import com.intellij.openapi.project.Project;
31
import com.intellij.openapi.util.NotNullLazyValue;
32
import com.intellij.openapi.util.text.StringUtil;
33
import com.intellij.psi.PsiDocumentManager;
34
import com.intellij.psi.PsiElement;
35
import com.intellij.psi.PsiFile;
36
import com.intellij.psi.PsiWhiteSpace;
37
import com.intellij.psi.codeStyle.CodeStyleManager;
38
import com.intellij.psi.tree.IElementType;
39
import com.intellij.psi.tree.TokenSet;
40
import com.intellij.psi.util.PsiTreeUtil;
41
import com.intellij.psi.util.PsiUtilCore;
42
import com.perl5.lang.perl.PerlLanguage;
43
import com.perl5.lang.perl.idea.codeInsight.Perl5CodeInsightSettings;
44
import com.perl5.lang.perl.lexer.PerlElementTypes;
45
import com.perl5.lang.perl.psi.*;
46
import com.perl5.lang.perl.psi.impl.PerlHeredocElementImpl;
47
import com.perl5.lang.perl.psi.impl.PsiPerlPerlRegexImpl;
48
import com.perl5.lang.perl.psi.utils.PerlPsiUtil;
49
import org.jetbrains.annotations.NotNull;
50
import org.jetbrains.annotations.Nullable;
51

52
import static com.intellij.psi.TokenType.WHITE_SPACE;
53
import static com.perl5.lang.perl.lexer.PerlTokenSets.*;
54

55

56
public class PerlTypedHandler extends PerlTypedHandlerDelegate implements PerlElementTypes {
1✔
57
  // these chars are automatically closed by IDEA and we can't control this
58
  private static final String HANDLED_BY_BRACE_MATCHER = "{([";
59

60
  private static final NotNullLazyValue<TokenSet> AUTO_OPENED_TOKENS = NotNullLazyValue.createValue(() -> TokenSet.create(
1✔
61
    RESERVED_USE,
62
    RESERVED_NO,
63
    RESERVED_PACKAGE,
64
    RESERVED_CLASS,
65
    ANNOTATION_RETURNS_KEY,
66
    ANNOTATION_TYPE_KEY,
67
    ANNOTATION_SCALAR,
68
    ANNOTATION_ARRAY,
69
    ANNOTATION_HASH
70
  ));
71

72

73
  @Override
74
  public @NotNull Result beforeCharTyped(char c,
75
                                         @NotNull Project project,
76
                                         @NotNull Editor editor,
77
                                         @NotNull PsiFile file,
78
                                         @NotNull FileType fileType) {
79
    if (!isMyFile(file)) {
1✔
80
      return Result.CONTINUE;
1✔
81
    }
82
    CaretModel caretModel = editor.getCaretModel();
1✔
83
    int currentOffset = caretModel.getOffset();
1✔
84
    Document document = editor.getDocument();
1✔
85
    CharSequence documentSequence = document.getCharsSequence();
1✔
86
    if (currentOffset > documentSequence.length()) {
1✔
UNCOV
87
      return Result.CONTINUE;
×
88
    }
89

90
    EditorHighlighter highlighter = editor.getHighlighter();
1✔
91
    HighlighterIterator nextPositionIterator = highlighter.createIterator(currentOffset);
1✔
92
    IElementType nextTokenType = PerlEditorUtil.getTokenType(nextPositionIterator);
1✔
93
    int nextTokenLength = PerlEditorUtil.getTokenLength(nextPositionIterator);
1✔
94

95
    HighlighterIterator prevPositionIterator = currentOffset > 0 ? highlighter.createIterator(currentOffset - 1) : null;
1✔
96
    IElementType prevTokenType = PerlEditorUtil.getTokenType(prevPositionIterator);
1✔
97
    int prevTokenLength = PerlEditorUtil.getTokenLength(prevPositionIterator);
1✔
98

99
    if (c == '<' && prevTokenType == LEFT_ANGLE && prevTokenLength == 1 && nextTokenType == RIGHT_ANGLE && nextTokenLength == 1) {
1✔
100
      document.replaceString(currentOffset, currentOffset + 1, "<");
1✔
101
      caretModel.moveToOffset(currentOffset + 1);
1✔
102
      return Result.STOP;
1✔
103
    }
104

105
    char nextChar = currentOffset == documentSequence.length() ? 0 : documentSequence.charAt(currentOffset);
1✔
106
    if (QUOTE_CLOSE_FIRST_ANY.contains(nextTokenType) && c == nextChar) {
1✔
107
      caretModel.moveToOffset(currentOffset + 1);
1✔
108
      return Result.STOP;
1✔
109
    }
110

111
    if (c == ':') {
1✔
112
      if (Perl5CodeInsightSettings.getInstance().AUTO_INSERT_COLON && currentOffset > 0 && shouldAddColon(prevPositionIterator, file)) {
1✔
113
        if (documentSequence.charAt(currentOffset - 1) != ':') {
1✔
114
          document.insertString(currentOffset, "::");
1✔
115
          caretModel.moveToOffset(currentOffset + 2);
1✔
116
        }
117
        AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
118
        return Result.STOP;
1✔
119
      }
120
      else if (currentOffset > 1 && documentSequence.charAt(currentOffset - 1) == ':') {
1✔
121
        AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
122
      }
123
    }
124

125
    if (c == ' ') {
1✔
126
      Result result = tryToAddFatComma(editor, file, currentOffset);
1✔
127
      if (result != null) {
1✔
128
        return result;
1✔
129
      }
130
    }
131

132
    if (c == '>' && PerlEditorUtil.getPreviousTokenType(prevPositionIterator, true) == OPERATOR_DEREFERENCE) {
1✔
133
      AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
134
      return Result.STOP;
1✔
135
    }
136

137
    return Result.CONTINUE;
1✔
138
  }
139

140
  static final TokenSet COLON_HANDLING_VARIABLE_TOKENS = TokenSet.orSet(SIGILS, VARIABLE_NAMES);
1✔
141

142
  /**
143
   * @return true if we are at proper place for inserting two colons
144
   */
145
  private boolean shouldAddColon(@NotNull HighlighterIterator precedingIterator,
146
                                 @NotNull PsiFile psiFile) {
147
    IElementType tokenType = precedingIterator.getTokenType();
1✔
148
    if (isPreColonSuffixBase(tokenType, precedingIterator.getStart(), psiFile)) {
1✔
149
      return true;
1✔
150
    }
151
    else if (tokenType != SUB_NAME) {
1✔
152
      return false;
1✔
153
    }
154
    precedingIterator.retreat();
1✔
155
    return !precedingIterator.atEnd() && precedingIterator.getTokenType() == QUALIFYING_PACKAGE;
1✔
156
  }
157

158
  /**
159
   * @return {@code true} iff we should add/remove colon after the token starting at {@code elementOffset} in {@code psiFile} with
160
   * {@code tokenType}.
161
   */
162
  public static boolean isPreColonSuffixBase(@Nullable IElementType tokenType,
163
                                             int elementOffset,
164
                                             @NotNull PsiFile psiFile) {
165
    if (PACKAGE_LIKE_TOKENS.contains(tokenType)) {
1✔
166
      return true;
1✔
167
    }
168
    else if (COLON_HANDLING_VARIABLE_TOKENS.contains(tokenType)) {
1✔
169
      var perlLeafAtOffset = psiFile.getViewProvider().findElementAt(elementOffset, PerlLanguage.INSTANCE);
1✔
170
      var perlVariable = perlLeafAtOffset == null ? null : perlLeafAtOffset.getParent();
1✔
171
      var variableContainer = perlVariable == null ? null : perlVariable.getParent();
1✔
172
      return !(variableContainer instanceof PerlString ||
1✔
173
               variableContainer instanceof PerlHeredocElementImpl ||
174
               variableContainer instanceof PsiPerlPerlRegexImpl);
175
    }
176
    return false;
1✔
177
  }
178

179
  private boolean isMyFile(@NotNull PsiFile file) {
180
    return file.getLanguage().isKindOf(PerlLanguage.INSTANCE);
1✔
181
  }
182

183
  @Override
184
  public @NotNull Result charTyped(char typedChar, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
185
    if (!isMyFile(file)) {
1✔
186
      return Result.CONTINUE;
1✔
187
    }
188
    final int currentOffset = editor.getCaretModel().getOffset();
1✔
189
    final int offset = currentOffset - 1;
1✔
190
    if (offset < 0) {
1✔
UNCOV
191
      return Result.CONTINUE;
×
192
    }
193

194
    Perl5CodeInsightSettings perlCodeInsightSettings = Perl5CodeInsightSettings.getInstance();
1✔
195
    EditorHighlighter highlighter = editor.getHighlighter();
1✔
196
    HighlighterIterator iterator = highlighter.createIterator(offset);
1✔
197
    IElementType elementTokenType = iterator.getTokenType();
1✔
198
    Document document = editor.getDocument();
1✔
199
    CharSequence text = document.getCharsSequence();
1✔
200
    if (QUOTE_OPEN_ANY.contains(elementTokenType) && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE) {
1✔
201
      IElementType quotePrefixType = offset > 0 ? PerlEditorUtil.getPreviousTokenType(highlighter.createIterator(offset - 1), false) : null;
1✔
202
      if (offset > text.length() - 1 || text.charAt(offset) != typedChar) {
1✔
UNCOV
203
        return Result.CONTINUE;
×
204
      }
205
      if (elementTokenType == QUOTE_DOUBLE_OPEN || elementTokenType == QUOTE_SINGLE_OPEN) {
1✔
206
        AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
207
      }
208
      char openChar = text.charAt(offset);
1✔
209
      char closeChar = PerlString.getQuoteCloseChar(openChar);
1✔
210
      iterator.advance();
1✔
211
      IElementType possibleCloseQuoteType = PerlEditorUtil.getTokenType(iterator);
1✔
212
      if (QUOTE_CLOSE_FIRST_ANY.contains(possibleCloseQuoteType) && closeChar == text.charAt(iterator.getStart())) {
1✔
213
        if (COMPLEX_QUOTE_OPENERS.contains(quotePrefixType) && StringUtil.containsChar(HANDLED_BY_BRACE_MATCHER, openChar)) {
1✔
214
          iterator.advance();
1✔
215
          if (iterator.atEnd() || !QUOTE_OPEN_ANY.contains(iterator.getTokenType())) {
1✔
216
            EditorModificationUtilEx.insertStringAtCaret(editor, Character.toString(closeChar) + openChar, false, false);
1✔
217
          }
218
        }
219
        else if (SIMPLE_QUOTE_OPENERS.contains(quotePrefixType) &&
1✔
220
                 StringUtil.containsChar(HANDLED_BY_BRACE_MATCHER, openChar) &&
1✔
221
                 !PerlEditorUtil.areMarkersBalanced((EditorEx)editor, offset, openChar)) {
1✔
222
          EditorModificationUtilEx.insertStringAtCaret(editor, Character.toString(closeChar), false, false);
1✔
223
          return Result.STOP;
1✔
224
        }
225
        return Result.CONTINUE;
1✔
226
      }
227

228
      StringBuilder textToAppend = new StringBuilder();
1✔
229
      textToAppend.append(closeChar);
1✔
230

231
      if (COMPLEX_QUOTE_OPENERS.contains(quotePrefixType)) {
1✔
232
        textToAppend.append(openChar);
1✔
233
        if (openChar != closeChar) {
1✔
234
          textToAppend.append(closeChar);
1✔
235
        }
236
      }
237

238
      EditorModificationUtilEx.insertStringAtCaret(editor, textToAppend.toString(), false, false);
1✔
239
    }
1✔
240
    else if (elementTokenType == STRING_SPECIAL_HEX && perlCodeInsightSettings.AUTO_BRACE_HEX_SUBSTITUTION ||
1✔
241
             elementTokenType == STRING_SPECIAL_OCT && perlCodeInsightSettings.AUTO_BRACE_OCT_SUBSTITUTION) {
242
      EditorModificationUtilEx.insertStringAtCaret(editor, "{}", false, 1);
1✔
243
    }
244
    else if (elementTokenType == STRING_SPECIAL_UNICODE) {
1✔
245
      EditorModificationUtilEx.insertStringAtCaret(editor, "{}", false, 1);
1✔
246
      AutoPopupController.getInstance(project).scheduleAutoPopup(editor, CompletionType.BASIC, null);
1✔
247
    }
248
    else if (elementTokenType == LEFT_BRACE) {
1✔
249
      AutoPopupController.getInstance(project).scheduleAutoPopup(editor, CompletionType.BASIC, psiFile -> {
1✔
250
        PsiElement newElement = psiFile.findElementAt(offset);
1✔
251
        return PsiUtilCore.getElementType(newElement) == elementTokenType &&
1✔
252
               newElement.getParent() instanceof PsiPerlHashIndex;
1✔
253
      });
254
    }
255
    else if (typedChar == '=' && !iterator.atEnd()) {
1✔
256
      handleEqualSign(editor, file, offset, iterator, document);
1✔
257
    }
258
    else if (typedChar == '-' && offset > 0) {
1✔
259
      iterator.retreat();
1✔
260
      if (PerlEditorUtil.getPreviousTokenType(iterator, true) == PACKAGE &&
1✔
261
          (currentOffset == text.length() || text.charAt(currentOffset) != '>')) {
1✔
262
        EditorModificationUtilEx.insertStringAtCaret(editor, ">");
1✔
263
        AutoPopupController.getInstance(project).scheduleAutoPopup(editor, CompletionType.BASIC, null);
1✔
264
      }
265
    }
266

267
    return Result.CONTINUE;
1✔
268
  }
269

270
  private void handleEqualSign(@NotNull Editor editor,
271
                               @NotNull PsiFile file,
272
                               int offset,
273
                               HighlighterIterator iterator,
274
                               Document document) {
275
    int previousNonSpaceTokenStart = -1;
1✔
276
    while (true) {
277
      iterator.retreat();
1✔
278
      if (iterator.atEnd()) {
1✔
UNCOV
279
        return;
×
280
      }
281
      if (iterator.getTokenType() != WHITE_SPACE) {
1✔
282
        previousNonSpaceTokenStart = iterator.getStart();
1✔
283
        break;
1✔
284
      }
285
    }
286

287
    PsiElement elementAtOffset = file.findElementAt(previousNonSpaceTokenStart);
1✔
288
    PsiPerlSignatureContent wrappingSignature = PsiTreeUtil.getParentOfType(elementAtOffset, PsiPerlSignatureContent.class);
1✔
289
    if (wrappingSignature != null) {
1✔
290
      int signatureOffset = wrappingSignature.getNode().getStartOffset();
1✔
291
      EditorModificationUtilEx.insertStringAtCaret(editor, " ", false, true);
1✔
292
      Project project = file.getProject();
1✔
293
      PsiDocumentManager.getInstance(project).commitDocument(document);
1✔
294
      CodeStyleManager.getInstance(project).reformatText(file, signatureOffset, offset);
1✔
295
      AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
296
    }
297
  }
1✔
298

299
  private @Nullable Result tryToAddFatComma(@NotNull Editor editor, @NotNull PsiFile file, int offset) {
300
    if (!Perl5CodeInsightSettings.getInstance().SMART_COMMA_SEQUENCE_TYPING) {
1✔
301
      return null;
1✔
302
    }
303

304
    PsiElement elementAtCaret = file.findElementAt(offset);
1✔
305
    if (!(elementAtCaret instanceof PsiWhiteSpace)) {
1✔
306
      return null;
1✔
307
    }
308

309
    PsiElement commaSequence = elementAtCaret.getPrevSibling();
1✔
310
    if (!(commaSequence instanceof PsiPerlCommaSequenceExpr)) {
1✔
311
      return null;
1✔
312
    }
313

314
    PsiElement lastChild = commaSequence.getLastChild();
1✔
315
    IElementType lastChildElementType = PsiUtilCore.getElementType(lastChild);
1✔
316
    if (lastChildElementType == COMMA || lastChildElementType == FAT_COMMA) {
1✔
UNCOV
317
      return null;
×
318
    }
319

320
    PsiElement commaElement = PerlPsiUtil.getPrevSignificantSibling(lastChild);
1✔
321
    if (PsiUtilCore.getElementType(commaElement) != COMMA) {
1✔
UNCOV
322
      return null;
×
323
    }
324

325
    PsiElement fatCommaElement = PerlPsiUtil.getPrevSignificantSibling(PerlPsiUtil.getPrevSignificantSibling(commaElement));
1✔
326
    if (PsiUtilCore.getElementType(fatCommaElement) != FAT_COMMA) {
1✔
UNCOV
327
      return null;
×
328
    }
329

330
    int reformatFrom = commaSequence.getNode().getStartOffset();
1✔
331

332
    Document document = editor.getDocument();
1✔
333
    document.insertString(offset, "=>");
1✔
334
    editor.getCaretModel().moveToOffset(offset + 2);
1✔
335
    Project project = file.getProject();
1✔
336
    PsiDocumentManager.getInstance(project).commitDocument(document);
1✔
337
    CodeStyleManager.getInstance(project).reformatText(file, reformatFrom, offset + 2);
1✔
338
    AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
1✔
339

340
    return Result.CONTINUE;
1✔
341
  }
342

343
  @Override
344
  protected boolean shouldShowPopup(char typedChar,
345
                                    @NotNull Editor editor,
346
                                    @Nullable PsiElement element) {
347
    IElementType elementType = PsiUtilCore.getElementType(element);
1✔
348
    return typedChar == '^' && PRE_VARIABLE_NAME_TOKENS.contains(elementType) ||
1✔
349
           typedChar == '>' && elementType == OPERATOR_MINUS ||
350
           typedChar == ':' && elementType == COLON ||
351
           typedChar == ' ' && (AUTO_OPENED_TOKENS.get().contains(elementType) || PerlStringList.isListElement(element)) ||
1✔
352
           typedChar == '{' && (elementType == STRING_SPECIAL_UNICODE || SIGILS.contains(elementType)) ||
1✔
353
           StringUtil.containsChar("$@%#", typedChar);
1✔
354
  }
355
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc