/*
* Copyright 2008 Georgi Staykov
*
* This file is part of pscoder.
*
* pscoder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* pscoder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with pscoder. If not, see <http://www.gnu.org/licenses/>.
*/
package com.gstaykov.pscoder.editor.completion;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import com.gstaykov.pscoder.Activator;
import com.gstaykov.pscoder.preferences.PreferenceConstants;
import com.gstaykov.pscoder.util.Logger;
import com.gstaykov.pscoder.util.Util;
public class CompletionDictionary {
private static final String DICT_FILENAME_SUFFIX = "Dictionary.dic";
private static final String[] GET_ASSEMBLIES_CMD = new String[] {"powershell.exe", "-Command", "\"(Get-PSSnapin) | select AssemblyName | foreach {write-host $_.AssemblyName}\""};
private static final String CMDLET_FINDER_FILENAME = "CmdletFinder.exe";
public static final String PS_CMDLET_FILENAME = "CmdletFile";
public static final String CUSTOM_CMDLET_FILENAME = "CustomCmdletFile";
private static final String POWERSHELL_FILE_SUFFIX = ".ps1";
private static final int READ_CHARS = 1024;
private static HashMap<String, CompletionDictionary> instances = new HashMap<String, CompletionDictionary>();
private static ResourceChangeListener changeListener = null;
private IProject project = null;
private String projectName = null;
private IWorkspace workspace = null;
private IWorkspaceRoot workspaceRoot = null;
private Pattern srcFilesP = Pattern.compile("\\.\\s*([A-Za-z0-9\\\\/-]*\\.ps1)"); // FIXME: [IN] Should not match commented imports
private Pattern functionsP = Pattern.compile("function \\s*([a-zA-Z0-9]*\\(.*\\))");
private Pattern variablesP = Pattern.compile("\\$(script|global):[a-zA-z0-9_]*");
private Logger logger = new Logger();
private HashMap<String, FileData> dict = new HashMap<String, FileData>();
private CompletionDictionary(String projName) throws CoreException {
this.projectName = projName;
workspace = ResourcesPlugin.getWorkspace();
workspaceRoot = workspace.getRoot();
project = workspaceRoot.getProject(projName);
if (project.exists()) {
if (!project.isOpen()) project.open(null);
if (dict.size() == 0) {
loadDictionary();
}
}
}
private void loadDictionary() throws CoreException {
File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
if (!dictFile.exists()) {
generateDictionary();
} else {
try {
BufferedReader in = new BufferedReader(new FileReader(dictFile));
int files = Integer.parseInt(in.readLine());
for (int i = 0; i < files; i++) {
FileData data = new FileData(in.readLine());
data.setSourcedFiles(getDictFileNextBlock(in));
data.setFunctions(getDictFileNextBlock(in));
data.setVariables(getDictFileNextBlock(in));
dict.put(data.fileName, data);
}
} catch (Exception e) {
logger.logError("", e);
}
}
}
private String[] getDictFileNextBlock(BufferedReader in) throws IOException {
int linesCount = Integer.parseInt(in.readLine());
String[] lines = new String[linesCount];
for (int i = 0; i < linesCount; i++) lines[i] = in.readLine();
return lines;
}
private void generateDictionary() throws CoreException {
IPath projectRootPath = project.getLocation();
File projectRoot = projectRootPath.toFile();
addSourceDirectoryToDict(projectRoot);
IResource[] members = project.members();
for (IResource member : members) {
if (member.isLinked()) {
addSourceDirectoryToDict(member.getLocation().toFile());
}
}
addKnownCmdlets();
saveDictionary();
}
private void addKnownCmdlets() {
try {
// First add powershell native cmdlets
ArrayList<String> assemblies = new ArrayList<String>();
Process proc = Runtime.getRuntime().exec(GET_ASSEMBLIES_CMD);
proc.getOutputStream().close();
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String newLine = null;
while ((newLine = reader.readLine()) != null) {
assemblies.add(newLine);
}
reader.close();
ArrayList<String> cmdlets = new ArrayList<String>();
for (String assembly : assemblies) {
proc = Runtime.getRuntime().exec(new String[] {CMDLET_FINDER_FILENAME, assembly});
reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
newLine = null;
while ((newLine = reader.readLine()) != null) {
cmdlets.add(getCmdletString(newLine));
}
reader.close();
}
FileData data = new FileData(PS_CMDLET_FILENAME);
data.setFunctions(cmdlets.toArray(new String[cmdlets.size()]));
dict.put(PS_CMDLET_FILENAME, data);
// Now add custom snapins
cmdlets.clear();
String[] customFiles = parsePSSnapinFiles(Activator.getDefault().getPluginPreferences().getString(PreferenceConstants.PS_SNAPIN_FILES));
for (int i = 0; i < customFiles.length; i++) {
proc = Runtime.getRuntime().exec(new String[] {CMDLET_FINDER_FILENAME, customFiles[i]});
reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
newLine = null;
while ((newLine = reader.readLine()) != null) {
cmdlets.add(getCmdletString(newLine));
}
reader.close();
}
data = new FileData(CUSTOM_CMDLET_FILENAME);
data.setFunctions(cmdlets.toArray(new String[cmdlets.size()]));
dict.put(CUSTOM_CMDLET_FILENAME, data);
} catch (Exception e) {
logger.logError("An error occured while adding known commandlets", e);
}
}
private String getCmdletString(String cmdlet) {
StringBuffer sb = new StringBuffer();
StringTokenizer st = new StringTokenizer(cmdlet, ",");
if (st.hasMoreTokens()) {
sb.append(st.nextToken());
}
while (st.hasMoreTokens()) {
sb.append(" -" + st.nextToken());
}
return sb.toString();
}
// FIXME: This does exactly the same job as FileEditor.parseString - make utility method
private String[] parsePSSnapinFiles(String fileList) {
StringTokenizer st = new StringTokenizer(fileList, PreferenceConstants.SNAPIN_FILES_SEPARATOR);
ArrayList<String> elements = new ArrayList<String>();
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (token.length() != 0) {
elements.add(token);
}
}
return elements.toArray(new String[elements.size()]);
}
private void addSourceDirectoryToDict(File directory) {
String[] files = directory.list();
for (int i = 0; i < files.length; i++) {
File file = new File(directory.getAbsolutePath() + File.separatorChar + files[i]);
if (file.isDirectory()) {
addSourceDirectoryToDict(file);
}
addFileToDict(file);
}
}
private void addFileToDict(File file) {
if (file.getName().endsWith(POWERSHELL_FILE_SUFFIX)) {
String fileRelPath = file.getAbsolutePath();
FileData data = new FileData(fileRelPath);
String text = loadFile(file);
data.setSourcedFiles(getSourcedFiles(text));
data.setFunctions(getFunctions(text));
data.setVariables(getVariables(text));
dict.put(fileRelPath, data);
}
}
private String[] getSourcedFiles(String content) {
Set<String> sourced = new HashSet<String>();
Matcher matcher = srcFilesP.matcher(content);
while(matcher.find()) {
sourced.add(matcher.group(1));
}
String[] result = new String[sourced.size()];
return sourced.toArray(result);
}
private String[] getFunctions(String content) {
Set<String> sourced = new HashSet<String>();
Matcher matcher = functionsP.matcher(content);
while(matcher.find()) {
sourced.add(matcher.group(1));
}
String[] result = new String[sourced.size()];
return sourced.toArray(result);
}
private String[] getVariables(String content) {
Set<String> sourced = new HashSet<String>();
Matcher matcher = variablesP.matcher(content);
while(matcher.find()) {
sourced.add(matcher.group(0));
}
String[] result = new String[sourced.size()];
return sourced.toArray(result);
}
private String loadFile(File file) {
StringBuffer sb = new StringBuffer();
char[] buffer = new char[READ_CHARS];
try {
BufferedReader br = new BufferedReader(new FileReader(file));
int readChar = br.read(buffer);
while (readChar != -1) {
sb.append(buffer, 0, readChar);
readChar = br.read(buffer);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
private void saveDictionary() {
File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
try {
PrintWriter out = new PrintWriter(dictFile);
Set<String> keys = dict.keySet();
out.println(keys.size());
for (String key : keys) {
FileData data = dict.get(key);
out.println(data.fileName);
// write included files
String[] incFiles = data.getSourcedFiles();
out.println(incFiles.length);
for (int i = 0; i < incFiles.length; i++) {
out.println(incFiles[i]);
}
// write functions
String[] functions = data.getFunctions();
out.println(functions.length);
for (int i = 0; i < functions.length; i++) {
out.println(functions[i]);
}
// write variables
String[] variables = data.getVariables();
out.println(variables.length);
for (int i = 0; i < variables.length; i++) {
out.println(variables[i]);
}
}
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static CompletionDictionary getInstance(String project) throws CoreException {
CompletionDictionary result = CompletionDictionary.instances.get(project);
if (result == null) {
result = new CompletionDictionary(project);
CompletionDictionary.instances.put(project, result);
}
if (changeListener == null) {
// Install a resource change listener which should update the dictionaries if a workspace resource is changed
changeListener = new ResourceChangeListener();
ResourcesPlugin.getWorkspace().addResourceChangeListener(changeListener, IResourceChangeEvent.POST_CHANGE);
}
return result;
}
public String getFullFilenameForEnding(String ending) {
String lowerCasedEnding = Util.convertFileNameToWindowsNotation(ending).toLowerCase();
String result = ending;
// FIXME: HACK NOT CORRECT AND THE IMPLEMENTATION IS UGLY
Set<String> keys = dict.keySet();
for (String key : keys) {
if (key.toLowerCase().endsWith(lowerCasedEnding)) {
result = key;
break;
}
}
return result;
}
public HashMap<String, ICompletionProposal> getProposals(String prefix, String document, int offset) {
HashMap<String, ICompletionProposal> proposals = new HashMap<String, ICompletionProposal>();
// FIXME: HACK NOT CORRECT AND THE IMPLEMENTATION IS UGLY
Set<String> keys = dict.keySet();
for (String key : keys) {
if (key.toLowerCase().endsWith(document.toLowerCase())) {
document = key;
break;
}
}
// HACK MIGHT NOT BE CORRECT
FileData data = dict.get(document);
if (data == null) {
// No proposals for this document
return proposals;
}
String fileNameIncluded = document.substring(document.lastIndexOf('\\') + 1);
String[] variables = data.getVariables();
for (int i = 0; i < variables.length; i++) {
if (variables[i].startsWith(prefix)) {
String displayString = variables[i] + " - " + fileNameIncluded;
String stringToAdd = variables[i].substring(prefix.length());
CompletionProposal prop = new CompletionProposal(stringToAdd, offset, 0, stringToAdd.length(), null, displayString, null, null);
proposals.put(displayString, prop);
}
}
String[] functions = data.getFunctions();
for (int i = 0; i < functions.length; i++) {
String funcName = "";
if (functions[i].indexOf('(') != -1) {
// Add regular functions
funcName = functions[i].substring(0, functions[i].indexOf('('));
} else if (functions[i].indexOf(' ') != -1) {
// Add cmdlets
funcName = functions[i].substring(0, functions[i].indexOf(' '));
} else {
// Nothing to add
break;
}
if (funcName.startsWith(prefix)) {
String displayString = functions[i] + " - " + fileNameIncluded;
String stringToAdd = funcName.substring(prefix.length());
CompletionProposal prop = new CompletionProposal(stringToAdd, offset, 0, stringToAdd.length(), null, displayString, null, null);
proposals.put(displayString, prop);
}
}
String[] includes = data.getSourcedFiles();
for (int i = 0; i < includes.length; i++) {
proposals.putAll(getProposals(prefix, includes[i], offset));
}
return proposals;
}
public void loadFileChanges(File file) {
addFileToDict(file);
// FIXME: For now every time a file is changed save the new dictionary
saveDictionary();
}
public void rebuildDictionary() throws CoreException {
File dictFile = new File(projectName + DICT_FILENAME_SUFFIX);
if (dictFile.exists()) {
dictFile.delete();
}
dict = new HashMap<String, FileData>();
generateDictionary();
}
public FileData getFileData(String filename) {
return dict.get(filename);
}
}