/*
* Copyright 2009 Google Inc.
*
* 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.jstestdriver.idea.config;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.javascript.testFramework.util.JsPsiUtils;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.YAMLTokenTypes;
import org.jetbrains.yaml.psi.*;
import java.io.File;
import java.util.List;
public class JstdConfigFileCompletionContributor extends CompletionContributor {
private static final String IDENTIFIER_END_PATTERN = ".-:";
public JstdConfigFileCompletionContributor() {
extend(CompletionType.BASIC, JstdConfigFileUtils.CONFIG_FILE_ELEMENT_PATTERN, new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
UnquotedText text = new UnquotedText(parameters.getPosition());
int prefixLength = Math.max(0, parameters.getOffset() - text.getUnquotedDocumentTextRange().getStartOffset());
BipartiteString caretBipartiteElementText = splitByPrefixLength(text.getUnquotedText(), prefixLength);
boolean topLevelKeyCompletion = isTopLevelKeyCompletion(parameters);
addInnerSequencePathCompletionsIfNeeded(parameters, result, caretBipartiteElementText);
if (topLevelKeyCompletion) {
addTopLevelKeysCompletionIfNeeded(parameters, result, caretBipartiteElementText);
} else {
addBasePathCompletionsIfNeeded(parameters, result, caretBipartiteElementText);
}
}
});
}
private static boolean isTopLevelKeyCompletion(@NotNull CompletionParameters parameters) {
PsiElement psiElement = parameters.getPosition();
Document document = JsPsiUtils.getDocument(parameters.getOriginalFile());
if (document != null) {
TextRange textRange = psiElement.getTextRange();
int startLine = document.getLineNumber(textRange.getStartOffset());
int startOffset = document.getLineStartOffset(startLine);
return startOffset == textRange.getStartOffset();
}
return false;
}
public void beforeCompletion(@NotNull CompletionInitializationContext context) {
boolean acceptPathSeparator = false;
final int offset = context.getEditor().getCaretModel().getOffset();
final PsiElement element = context.getFile().findElementAt(offset);
if (element != null) {
if (element.getNode().getElementType() == YAMLTokenTypes.SCALAR_KEY) {
return;
}
int prefixLength = offset - element.getTextRange().getStartOffset();
BipartiteString caretBipartiteElementText = splitByPrefixLength(element.getText(), prefixLength);
Character separator = extractDirectoryTrailingFileSeparator(caretBipartiteElementText);
acceptPathSeparator = separator != null;
}
final OffsetMap offsetMap = context.getOffsetMap();
int idEnd = offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET);
final String text = context.getFile().getText();
while (idEnd < text.length()) {
final char ch = text.charAt(idEnd);
if (acceptPathSeparator) {
if (ch == JstdConfigFileUtils.UNIX_PATH_SEPARATOR || ch == JstdConfigFileUtils.WINDOWS_PATH_SEPARATOR) {
idEnd++;
break;
}
}
boolean acceptedChar = Character.isJavaIdentifierPart(ch) || IDENTIFIER_END_PATTERN.indexOf(ch) >= 0;
if (acceptedChar) {
idEnd++;
}
else {
break;
}
}
offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, idEnd);
}
private static void addTopLevelKeysCompletionIfNeeded(@NotNull CompletionParameters parameters,
@NotNull CompletionResultSet result,
@NotNull BipartiteString caretBipartiteElementText) {
PsiElement element = parameters.getPosition();
YAMLDocument yamlDocument = ObjectUtils.tryCast(element.getParent(), YAMLDocument.class);
if (yamlDocument == null) {
yamlDocument = JsPsiUtils.getVerifiedHierarchyHead(
element.getParent(),
new Class[]{YAMLKeyValue.class},
YAMLDocument.class
);
}
if (yamlDocument != null) {
String prefix = caretBipartiteElementText.getPrefix();
result = result.withPrefixMatcher(prefix);
for (String key : JstdConfigFileUtils.VALID_TOP_LEVEL_KEYS) {
if (key.startsWith(prefix)) {
result.addElement(LookupElementBuilder.create(key + ":"));
}
}
}
}
private static void addInnerSequencePathCompletionsIfNeeded(@NotNull CompletionParameters parameters,
@NotNull CompletionResultSet result,
@NotNull BipartiteString caretBipartiteElementText) {
PsiElement element = parameters.getPosition();
YAMLKeyValue keyValue = JsPsiUtils.getVerifiedHierarchyHead(
element.getParent(),
new Class[]{
YAMLSequenceItem.class,
YAMLCompoundValue.class
},
YAMLKeyValue.class
);
BasePathInfo basePathInfo = newBasePathInfo(parameters);
boolean keyMatched = keyValue != null && JstdConfigFileUtils.isTopLevelKeyWithInnerFileSequence(keyValue);
if (basePathInfo != null && keyMatched) {
VirtualFile basePath = basePathInfo.getBasePath();
if (basePath != null && keyValue.getParent() instanceof YAMLDocument) {
addPathCompletions(result, caretBipartiteElementText, basePath, false);
}
}
}
@Nullable
private static BasePathInfo newBasePathInfo(@NotNull CompletionParameters parameters) {
YAMLFile yamlFile = ObjectUtils.tryCast(parameters.getOriginalFile(), YAMLFile.class);
if (yamlFile != null) {
List<YAMLDocument> yamlDocuments = yamlFile.getDocuments();
if (!yamlDocuments.isEmpty()) {
return new BasePathInfo(yamlDocuments.get(0));
}
}
return null;
}
private static void addBasePathCompletionsIfNeeded(@NotNull CompletionParameters parameters,
@NotNull CompletionResultSet result,
@NotNull BipartiteString caretBipartiteElementText) {
YAMLKeyValue keyValue = ObjectUtils.tryCast(parameters.getPosition().getParent(), YAMLKeyValue.class);
if (keyValue != null) {
if (keyValue.getParent() instanceof YAMLDocument && BasePathInfo.isBasePathKey(keyValue)) {
BasePathInfo basePathInfo = newBasePathInfo(parameters);
if (basePathInfo != null) {
VirtualFile configDir = basePathInfo.getConfigDir();
if (configDir != null) {
addPathCompletions(result, caretBipartiteElementText, configDir, true);
}
}
}
}
}
private static void addPathCompletions(@NotNull CompletionResultSet result,
@NotNull BipartiteString caretBipartiteElementText,
@NotNull VirtualFile basePath,
boolean directoryExpected) {
ParentDirWithLastComponentPrefix parentWithLastComponentPrefix = findParentDirWithLastComponentPrefix(
basePath, caretBipartiteElementText.getPrefix()
);
if (parentWithLastComponentPrefix != null) {
PrefixMatcher matcher = new PlainPrefixMatcher(parentWithLastComponentPrefix.getLastComponentPrefix());
result = result.withPrefixMatcher(matcher);
VirtualFile parentFile = parentWithLastComponentPrefix.getParent();
VirtualFile[] children = parentFile.getChildren();
Character dirSeparatorSuffix = extractDirectoryTrailingFileSeparator(caretBipartiteElementText);
if (parentFile.isDirectory()) {
result.addElement(LookupElementBuilder.create(".."));
}
for (VirtualFile child : children) {
if (child.isDirectory() || !directoryExpected) {
String name = child.getName();
if (child.isDirectory() && dirSeparatorSuffix != null) {
name += dirSeparatorSuffix;
}
result.addElement(LookupElementBuilder.create(name));
}
}
}
}
@Nullable
private static ParentDirWithLastComponentPrefix findParentDirWithLastComponentPrefix(@NotNull VirtualFile basePath,
@NotNull String pathBeforeCaret) {
BipartiteString parentDirStrWithLastComponent = findParentDirStrWithLastComponentPrefix(pathBeforeCaret);
String parentDirPath = FileUtil.toSystemIndependentName(parentDirStrWithLastComponent.getPrefix());
{
VirtualFile parentFile = basePath.findFileByRelativePath(parentDirPath);
if (parentFile != null) {
return new ParentDirWithLastComponentPrefix(parentFile, parentDirStrWithLastComponent.getSuffix());
}
}
File absolutePath = new File(parentDirPath);
if (absolutePath.isAbsolute()) {
VirtualFile absolute = LocalFileSystem.getInstance().findFileByIoFile(absolutePath);
if (absolute != null) {
return new ParentDirWithLastComponentPrefix(absolute, parentDirStrWithLastComponent.getSuffix());
}
}
return null;
}
private static BipartiteString findParentDirStrWithLastComponentPrefix(String pathBeforeCaret) {
BipartiteString unixBipartiteString = splitByLastIndexOfSeparatorOccurrence(
pathBeforeCaret, JstdConfigFileUtils.UNIX_PATH_SEPARATOR
);
BipartiteString winBipartiteString = splitByLastIndexOfSeparatorOccurrence(
pathBeforeCaret, JstdConfigFileUtils.WINDOWS_PATH_SEPARATOR
);
if (unixBipartiteString.getSuffix().length() < winBipartiteString.getSuffix().length()) {
return unixBipartiteString;
} else {
return winBipartiteString;
}
}
private static Character extractPrevalentSeparator(String str) {
boolean unix = str.indexOf(JstdConfigFileUtils.UNIX_PATH_SEPARATOR) >= 0;
boolean windows = str.indexOf(JstdConfigFileUtils.WINDOWS_PATH_SEPARATOR) >= 0;
if (unix && !windows) {
return JstdConfigFileUtils.UNIX_PATH_SEPARATOR;
}
if (!unix && windows) {
return JstdConfigFileUtils.WINDOWS_PATH_SEPARATOR;
}
return null;
}
@Nullable
private static Character extractDirectoryTrailingFileSeparator(BipartiteString caretBipartiteElementText) {
Character prefixPrevalentSeparator = extractPrevalentSeparator(caretBipartiteElementText.getWholeString());
if (prefixPrevalentSeparator != null) {
return prefixPrevalentSeparator;
}
Character suffixPrevalentSeparator = extractPrevalentSeparator(caretBipartiteElementText.getSuffix());
if (suffixPrevalentSeparator != null) {
return suffixPrevalentSeparator;
}
return null;
}
@NotNull
private static BipartiteString splitByLastIndexOfSeparatorOccurrence(@NotNull String str, char separator) {
int index = str.lastIndexOf(separator);
if (index >= 0) {
return new BipartiteString(str.substring(0, index + 1), str.substring(index + 1));
}
return new BipartiteString("", str);
}
@NotNull
private static BipartiteString splitByPrefixLength(@NotNull String str, int prefixLength) {
assert prefixLength <= str.length();
return new BipartiteString(str.substring(0, prefixLength), str.substring(prefixLength));
}
private static class ParentDirWithLastComponentPrefix {
private VirtualFile myParent;
private String myLastComponentPrefix;
private ParentDirWithLastComponentPrefix(@NotNull VirtualFile parent, @NotNull String lastComponentPrefix) {
myParent = parent;
myLastComponentPrefix = lastComponentPrefix;
}
@NotNull
public VirtualFile getParent() {
return myParent;
}
@NotNull
public String getLastComponentPrefix() {
return myLastComponentPrefix;
}
}
private static class BipartiteString {
private final String myPrefix;
private final String mySuffix;
private BipartiteString(@NotNull String prefix, @NotNull String suffix) {
myPrefix = prefix;
mySuffix = suffix;
}
@NotNull
public String getPrefix() {
return myPrefix;
}
@NotNull
public String getSuffix() {
return mySuffix;
}
@NotNull
public String getWholeString() {
return myPrefix + mySuffix;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BipartiteString that = (BipartiteString)o;
return myPrefix.equals(that.myPrefix) && mySuffix.equals(that.mySuffix);
}
@Override
public int hashCode() {
int result = myPrefix.hashCode();
result = 31 * result + mySuffix.hashCode();
return result;
}
@Override
public String toString() {
return "prefix:'" + myPrefix + "'\', suffix='" + mySuffix + '\'';
}
}
}