package com.siberika.idea.pascal.lang.lexer;
import com.google.common.base.Preconditions;
import com.intellij.ide.DataManager;
import com.intellij.lexer.FlexAdapter;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectLocator;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.AsyncResult;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.TokenType;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.SmartList;
import com.intellij.util.containers.HashMap;
import com.intellij.util.io.BaseInputStreamReader;
import com.siberika.idea.pascal.sdk.BasePascalSdkType;
import com.siberika.idea.pascal.sdk.Define;
import com.siberika.idea.pascal.util.StrUtil;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.Reader;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Author: George Bakhtadze
* Date: 05/04/2013
*/
public class PascalFlexLexerImpl extends _PascalLexer {
private static final Logger LOG = Logger.getInstance(PascalFlexLexerImpl.class);
// Files of size less than this will be re-lexed on edit to correctly highlight potentially affected conditional blocks of code
private static final int DEFINE_CORRECT_HIGHLIGHT_THRESHOLD = 120000;
// current conditional compilation level
private int curLevel = 0;
// level on which inactive code branch started
private int inactiveLevel = 0;
// (Offset, curLevel, inactiveLevel) - offset, current conditional compilation level, level on which inactive code branch started
private List<Long> levels = new SmartList<Long>();
// (Offset, defineName). Negative offset - undefine.
private List<Pair<Integer, String>> defines = new SmartList<Pair<Integer, String>>();
private Set<String> actualDefines;
// TODO: replace with defines
private Map<String, Define> allDefines;
private VirtualFile virtualFile;
private Project project;
private final boolean incremental;
private AsyncResult<DataContext> dataContextResult;
private DataContext dataContext;
public void setVirtualFile(VirtualFile virtualFile) {
this.virtualFile = virtualFile;
}
public void setProject(Project project) {
this.project = project;
}
public PascalFlexLexerImpl(Reader in, Project project, VirtualFile virtualFile, boolean incremental) {
super(in);
Preconditions.checkArgument((project != null) || incremental, "No project in non-incremental lexer");
this.virtualFile = virtualFile;
this.project = project;
this.incremental = incremental;
if ((null == virtualFile) && incremental) {
getDataContext();
} else if (null == project) {
this.project = ProjectLocator.getInstance().guessProjectForFile(virtualFile);
}
}
@Override
public void reset(CharSequence buffer, int start, int end, int initialState) {
super.reset(buffer, start, end, initialState);
// System.out.println(String.format("===reset: [%d - %d], %d", start, end, initialState));
// super.reset(buffer, 0, end, YYINITIAL);
levels = levels.subList(0, getLevelIndex(start));
if (levels.isEmpty()) {
curLevel = 0;
inactiveLevel = 0;
} else {
curLevel = (levels.get(levels.size()-1).intValue() >> 16) & 0xFF;
inactiveLevel = levels.get(levels.size()-1).intValue() & 0xFF;
}
actualDefines = null;
allDefines = null;
actualDefines = getActualDefines();
defines = adjustDefines(actualDefines, defines, start);
}
private List<Pair<Integer, String>> adjustDefines(Set<String> defines, List<Pair<Integer, String>> events, int offset) {
for (int i = 0; i < events.size(); i++) {
int ofs = events.get(i).getFirst();
if (Math.abs(ofs) >= offset) {
return events.subList(0, i);
}
if (ofs >= 0) {
defines.add(events.get(i).getSecond());
} else {
defines.remove(events.get(i).getSecond());
}
}
return events;
}
// Index of actual level change for offset. 0 - no changes.
private int getLevelIndex(int start) {
for (int i = 0; i < levels.size(); i++) {
if (levels.get(i) >> 32 > start) {
return i;
}
}
return 0;
}
private DataContext getDataContext() {
try {
if (dataContext != null) {
return dataContext;
}
if (null == dataContextResult) {
dataContextResult = DataManager.getInstance().getDataContextFromFocus();
}
if (dataContextResult.isDone()) {
dataContext = dataContextResult.getResult();
} else if (dataContextResult.isRejected()) {
dataContextResult = DataManager.getInstance().getDataContextFromFocus();
}
return dataContext;
} catch (Throwable t) {
LOG.warn("-=Error=-", t);
return null;
}
}
private <T> T getData(String s) {
DataContext dataContext = getDataContext();
if (dataContext != null) {
return (T) dataContext.getData(s);
}
return null;
}
private Set<String> getActualDefines() {
if ((null == actualDefines) || (actualDefines.isEmpty())) {
initDefines(getProject(), getVirtualFile());
}
return actualDefines;
}
public Map<String, Define> getAllDefines() {
if ((null == allDefines) || (allDefines.isEmpty())) {
initDefines(getProject(), getVirtualFile());
}
return allDefines;
}
private Project getProject() {
if (isValidProject(project) || !incremental) {
return project;
}
project = getData(PlatformDataKeys.PROJECT.getName());
if (!isValidProject(project)) {
project = null;
}
return project;
}
private VirtualFile getVirtualFile() {
if ((virtualFile != null) || !incremental) {
return virtualFile;
}
virtualFile = getData(PlatformDataKeys.VIRTUAL_FILE.getName());
if (!isValidFile(virtualFile)) {
virtualFile = null;
}
return virtualFile;
}
private static boolean isValidFile(VirtualFile result) {
return result != null;
}
private static boolean isValidProject(Project project) {
return (project != null) && !project.isDisposed() && (ProjectRootManager.getInstance(project) != null);
}
private static Sdk getSdk(Project project, VirtualFile virtualFile) {
Module module = virtualFile != null ? ModuleUtil.findModuleForFile(virtualFile, project) : null;
Sdk sdk = module != null ? ModuleRootManager.getInstance(module).getSdk() : null;
return sdk != null ? sdk : ProjectRootManager.getInstance(project).getProjectSdk();
}
@Override
public void define(int pos, CharSequence sequence) {
String name = extractDefineName(sequence);
if (StringUtils.isNotEmpty(name)) {
String key = name.toUpperCase();
getActualDefines().add(key);
defines.add(Pair.create(pos, key));
getAllDefines().put(key, new Define(name, virtualFile, pos));
//if (incremental)System.out.println("Define: " + name);
}
}
@Override
public void unDefine(int pos, CharSequence sequence) {
String name = extractDefineName(sequence);
if (StringUtils.isNotEmpty(name)) {
String key = name.toUpperCase();
getActualDefines().remove(key);
defines.add(Pair.create(-pos, key));
getAllDefines().put(key, new Define(name, virtualFile, pos));
//if (incremental)System.out.println("Undefine: " + name);
}
}
synchronized private void initDefines(Project project, VirtualFile virtualFile) {
actualDefines = new HashSet<String>();
allDefines = new HashMap<String, Define>();
if ((project != null)) {
final Sdk sdk = getSdk(project, virtualFile);
allDefines = (sdk != null) && (sdk.getVersionString() != null) ? BasePascalSdkType.getDefaultDefines(sdk, sdk.getVersionString()) : allDefines;
for (Map.Entry<String, Define> entry : allDefines.entrySet()) {
actualDefines.add(entry.getKey());
}
}
}
private IElementType doHandleIfDef(int pos, CharSequence sequence, boolean negate) {
String name = extractDefineName(sequence);
curLevel++;
if (StringUtils.isNotEmpty(name) && (!getActualDefines().contains(name.toUpperCase()) ^ negate) && (!isInactive())) {
inactiveLevel = curLevel;
yybegin(INACTIVE_BRANCH);
//if (incremental)System.out.println(String.format("%s is NOT %sdefined", name, negate ? "un" : ""));
}
pushLevels(pos);
return CT_DEFINE;
}
private void pushLevels(int pos) {
levels.add((long) (pos) << 32 + curLevel << 16 + inactiveLevel);
}
@Override
public IElementType handleIf(int pos, CharSequence sequence) {
return doHandleIfDef(pos, sequence, false);
}
@Override
public IElementType handleIfDef(int pos, CharSequence sequence) {
return doHandleIfDef(pos, sequence, false);
}
@Override
public IElementType handleIfNDef(int pos, CharSequence sequence) {
return doHandleIfDef(pos, sequence, true);
}
@Override
public IElementType handleIfOpt(int pos, CharSequence sequence) {
return doHandleIfDef(pos, "NOT DEFINED", true);
}
@Override
public IElementType handleElse(int pos) {
if (curLevel <= 0) { return TokenType.BAD_CHARACTER; }
if (isInactive()) {
if (curLevel == inactiveLevel) {
yybegin(YYINITIAL);
}
} else {
inactiveLevel = curLevel;
yybegin(INACTIVE_BRANCH);
pushLevels(pos);
}
return CT_DEFINE;
}
@Override
public IElementType handleEndIf(int pos) {
if (curLevel <= 0) { return TokenType.BAD_CHARACTER; }
if (curLevel == inactiveLevel) {
yybegin(YYINITIAL);
}
curLevel--;
pushLevels(pos);
return CT_DEFINE;
}
@Override
public IElementType handleInclude(int pos, CharSequence sequence) {
String name = extractIncludeName(sequence);
Project project = getProject();
VirtualFile virtualFile = getVirtualFile();
if ((!StringUtils.isEmpty(name)) && (project != null)) {
VirtualFile file = com.siberika.idea.pascal.util.ModuleUtil.getIncludedFile(project, virtualFile, name);
PascalFlexLexerImpl lexer = !ObjectUtils.equals(virtualFile, file) ? processFile(project, file) : null;
if (lexer != null) {
getActualDefines().addAll(lexer.getActualDefines());
getAllDefines().putAll(lexer.getAllDefines());
for (Pair<Integer, String> define : lexer.defines) {
defines.add(Pair.create(define.first > 0 ? pos : -pos, define.second));
}
//TODO: put in levels
} else {
LOG.info(String.format("WARNING: Include %s referenced from %s not found", name, getVFName(virtualFile)));
}
}
return INCLUDE;
}
// Process the file and return the new instance of lexer which processed it
public static PascalFlexLexerImpl processFile(Project project, VirtualFile file) {
Reader reader = null;
try {
if ((file != null) && (file.getCanonicalPath() != null)) {
reader = new BaseInputStreamReader(file.getInputStream());
PascalFlexLexerImpl lexer = new PascalFlexLexerImpl(reader, project, file, false);
Document doc = FileDocumentManager.getInstance().getDocument(file);
if (doc != null) {
lexer.reset(doc.getCharsSequence(), 0, doc.getTextLength(), YYINITIAL);
lexer.setVirtualFile(file);
FlexAdapter flexAdapter = new FlexAdapter(lexer);
while (flexAdapter.getTokenType() != null) {
flexAdapter.advance();
}
return lexer;
}
}
} catch (IOException e) {
LOG.info("Error processing file", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
@NotNull
private static String getVFName(VirtualFile virtualFile) {
return virtualFile != null ? virtualFile.getName() : "<unknown>";
}
@Override
public IElementType getElement(IElementType elementType) {
return elementType;
}
private boolean isInactive() {
return yystate() == INACTIVE_BRANCH;
}
private static final Pattern PATTERN_DEFINE = Pattern.compile("\\{\\$\\w+\\s+(\\w+)\\s*}");
private static String extractDefineName(CharSequence sequence) {
Matcher m = PATTERN_DEFINE.matcher(sequence);
return m.matches() ? m.group(1) : null;
}
private static String extractIncludeName(CharSequence sequence) {
return StrUtil.getIncludeName(sequence.toString());
}
// Returns state modified if lexer state can be modified by a conditional define declared in the text
public int getStateWithConditionals() {
if ((yylength() > 0) && (virtualFile != null) && (virtualFile.getLength() < DEFINE_CORRECT_HIGHLIGHT_THRESHOLD)) {
return yystate() + (levels.size() + defines.size()) * 10;
}
return yystate();
}
}