summaryrefslogtreecommitdiff
path: root/platform/lang-impl/src/com/intellij/codeInsight/folding/impl/PsiNamesElementSignatureProvider.java
blob: 3409d3f93fc80e040edbbe49e7d29950a780041f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
/*
 * 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.intellij.codeInsight.folding.impl;

import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.StringTokenizer;

/**
 * Performs {@code 'PSI element <-> signature'} mappings on the basis of unique path of {@link PsiNamedElement PSI named elements}
 * to the PSI root.
 * <p/>
 * Thread-safe.
 * 
 * @author Denis Zhdanov
 * @since 11/7/11 11:58 AM
 */
public class PsiNamesElementSignatureProvider extends AbstractElementSignatureProvider {
  
  private static final String TYPE_MARKER            = "n";
  private static final String TOP_LEVEL_CHILD_MARKER = "!!top";
  private static final String DOC_COMMENT_MARKER     = "!!doc";
  private static final String CODE_BLOCK_MARKER      = "!!block";
  
  @Override
  protected PsiElement restoreBySignatureTokens(@NotNull PsiFile file,
                                                @NotNull PsiElement parent,
                                                @NotNull final String type,
                                                @NotNull StringTokenizer tokenizer,
                                                @Nullable StringBuilder processingInfoStorage)
  {
    if (!TYPE_MARKER.equals(type)) {
      if (processingInfoStorage != null) {
        processingInfoStorage.append(String.format(
          "Stopping '%s' provider because given signature doesn't have expected type - can work with '%s' but got '%s'%n",
          getClass().getName(), TYPE_MARKER, type
        ));
      }
      return null;
    }
    String elementMarker = tokenizer.nextToken();
    if (TOP_LEVEL_CHILD_MARKER.equals(elementMarker)) {
      PsiElement result = null;
      for (PsiElement child = file.getFirstChild(); child != null; child = child.getNextSibling()) {
        if (child instanceof PsiWhiteSpace) {
          continue;
        }
        if (result == null) {
          result = child;
        }
        else {
          if (processingInfoStorage != null) {
            processingInfoStorage.append(String.format(
              "Stopping '%s' provider because it has top level marker but more than one non white-space child: %s%n",
              getClass().getName(), Arrays.toString(file.getChildren())
            ));
          }
          // More than one top-level non-white space children. Can't match.
          return null;
        }
      }
      if (processingInfoStorage != null) {
        processingInfoStorage.append(String.format(
          "Finished processing of '%s' provider because all of its top-level children have been processed: %s%n",
          getClass().getName(), Arrays.toString(file.getChildren())
        ));
      }
      return result;
    }
    else if (DOC_COMMENT_MARKER.equals(elementMarker)) {
      PsiElement candidate = parent.getFirstChild();
      return candidate instanceof PsiComment ? candidate : null; 
    }
    else if (CODE_BLOCK_MARKER.equals(elementMarker)) {
      int index = 0;
      if (tokenizer.hasMoreTokens()) {
        String indexStr = tokenizer.nextToken();
        try {
          index = Integer.parseInt(indexStr);
        }
        catch (NumberFormatException e) {
          if (processingInfoStorage != null) {
            processingInfoStorage.append("Invalid block index: ").append(indexStr).append("\n");
          }
        }
      }
      for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
        if (isBlockElement(child)) {
          if (--index < 0) {
            return child;
          }
        } 
      }
      return null;
    }

    if (!tokenizer.hasMoreTokens()) {
      if (processingInfoStorage != null) {
        processingInfoStorage.append(String.format("Stopping '%s' provider because it has no more data to process%n", getClass().getName()));
      }
      return null;
    }
    try {
      int index = Integer.parseInt(tokenizer.nextToken());
      if (processingInfoStorage != null) {
        processingInfoStorage.append(String.format("Looking for the child with a name '%s' # %d at the element '%s'%n",
                                                   elementMarker, index, parent));
      }
      return restoreElementInternal(parent, unescape(elementMarker), index, PsiNamedElement.class);
    }
    catch (NumberFormatException e) {
      return null;
    }
  }

  @Override
  public String getSignature(@NotNull final PsiElement element) {
    StringBuilder buffer = null;
    int length;
    for (PsiElement current = element; current != null && !(current instanceof PsiFile); current = current.getParent()) {
      length = buffer == null ? 0 : buffer.length();
      StringBuilder b = getSignature(current, buffer);
      if (b == null && buffer != null && current.getParent() instanceof PsiFile && canResolveTopLevelChild(current)) {
        buffer.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(TOP_LEVEL_CHILD_MARKER).append(ELEMENTS_SEPARATOR);
        break;
      } 
      buffer = b;
      if (buffer == null || length >= buffer.length()) {
        return null;
      }
      buffer.append(ELEMENTS_SEPARATOR);
    }

    if (buffer == null) {
      return null;
    } 
    
    buffer.setLength(buffer.length() - 1);
    return buffer.toString();
  }

  /**
   * Allows to answer if it's possible to use {@link #TOP_LEVEL_CHILD_MARKER} for the given element.
   * 
   * @param element  element to check
   * @return         <code>true</code> if {@link #TOP_LEVEL_CHILD_MARKER} can be used for the given element; <code>false</code> otherwise
   */
  private static boolean canResolveTopLevelChild(@NotNull PsiElement element) {
    final PsiElement parent = element.getParent();
    if (parent == null) {
      return false;
    }
    for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
      if (child instanceof PsiWhiteSpace) {
        continue;
      }
      if (child != element) {
        return false;
      } 
    }
    return true;
  }
  
  /**
   * Tries to produce signature for the exact given PSI element.
   * 
   * @param element  target element
   * @param buffer   buffer to store the signature in
   * @return         buffer that contains signature of the given element if it was produced;
   *                 <code>null</code> as an indication that signature for the given element was not produced
   */
  @SuppressWarnings("unchecked")
  @Nullable
  private static StringBuilder getSignature(@NotNull PsiElement element, @Nullable StringBuilder buffer) {
    if (element instanceof PsiNamedElement) {
      PsiNamedElement named = (PsiNamedElement)element;
      final String name = named.getName();
      if (StringUtil.isEmpty(name)) {
        return null;
      }
      int index = getChildIndex(named, element.getParent(), name, PsiNamedElement.class);
      StringBuilder bufferToUse = buffer;
      if (bufferToUse == null) {
        bufferToUse = new StringBuilder();
      }
      bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(escape(name))
        .append(ELEMENT_TOKENS_SEPARATOR).append(index);
      return bufferToUse;
    }
    else if (element instanceof PsiComment) {
      PsiElement parent = element.getParent();
      boolean nestedComment = false;
      if (parent instanceof PsiComment && parent.getTextRange().equals(element.getTextRange())) {
        parent = parent.getParent();
        nestedComment = true;
      }
      if (parent instanceof PsiNamedElement && (nestedComment || parent.getFirstChild() == element)) {
        // Consider doc comment element that is the first child of named element to be a doc comment.
        StringBuilder bufferToUse = buffer;
        if (bufferToUse == null) {
          bufferToUse = new StringBuilder();
        }
        bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(DOC_COMMENT_MARKER);
        return bufferToUse;
      } 
    }

    PsiElement parent = element.getParent();
    if (parent instanceof PsiNamedElement && !(parent instanceof PsiFile)) {
      if (isBlockElement(element)) {
        int index = getBlockElementIndex(element);
        StringBuilder bufferToUse = buffer;
        if (bufferToUse == null) {
          bufferToUse = new StringBuilder();
        }
        bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(CODE_BLOCK_MARKER);
        if (index > 0) {
          bufferToUse.append(ELEMENT_TOKENS_SEPARATOR).append(index);
        }
        return bufferToUse;
      }
    } 
     
    return null;
  }

  private static boolean isBlockElement(@NotNull PsiElement element) {
    PsiElement firstChild = element.getFirstChild();
    PsiElement lastChild = element.getLastChild();
    return firstChild != null && "{".equals(firstChild.getText()) && lastChild != null && "}".equals(lastChild.getText());
  }

  private static int getBlockElementIndex(@NotNull PsiElement element) {
    int i = 0;
    for (PsiElement sibling : element.getParent().getChildren()) {
      if (element.equals(sibling)) {
        return i;
      }
      if (isBlockElement(sibling)) {
        i++;
      }
    }
    throw new RuntimeException("Malformed PSI");
  }
}