/* * Author: C.Williams * * Copyright (c) 2004 RubyPeople. * * This file is part of the Ruby Development Tools (RDT) plugin for eclipse. You * can get copy of the GPL along with further information about RubyPeople and * third party software bundled with RDT in the file * org.rubypeople.rdt.core_x.x.x/RDT.license or otherwise at * http://www.rubypeople.org/RDT.license. * * RDT 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 2 of the License, or (at your option) any later * version. * * RDT 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 * RDT; if not, write to the Free Software Foundation, Inc., 59 Temple Place, * Suite 330, Boston, MA 02111-1307 USA */ package org.rubypeople.rdt.internal.core; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.jruby.ast.Node; import org.jruby.ast.RootNode; import org.jruby.lexer.yacc.SyntaxException; import org.rubypeople.rdt.core.CompletionRequestor; import org.rubypeople.rdt.core.IBuffer; import org.rubypeople.rdt.core.ICodeAssist; import org.rubypeople.rdt.core.IImportContainer; import org.rubypeople.rdt.core.IImportDeclaration; import org.rubypeople.rdt.core.IOpenable; import org.rubypeople.rdt.core.IProblemRequestor; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyModelStatusConstants; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.ISourceFolderRoot; import org.rubypeople.rdt.core.ISourceRange; import org.rubypeople.rdt.core.IType; import org.rubypeople.rdt.core.RubyConventions; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.core.WorkingCopyOwner; import org.rubypeople.rdt.core.compiler.CategorizedProblem; import org.rubypeople.rdt.internal.codeassist.CompletionEngine; import org.rubypeople.rdt.internal.compiler.ISourceElementRequestor; import org.rubypeople.rdt.internal.core.buffer.BufferManager; import org.rubypeople.rdt.internal.core.util.MementoTokenizer; import org.rubypeople.rdt.internal.core.util.Util; /** * @author Chris * */ public class RubyScript extends Openable implements IRubyScript { public WorkingCopyOwner owner; protected String name; public Node lastGoodAST; /** * @param name */ public RubyScript(SourceFolder parent, String name, WorkingCopyOwner owner) { super(parent); this.name = name; this.owner = owner; } /** * Returns a new element info for this element. */ protected Object createElementInfo() { return new RubyScriptElementInfo(); } protected boolean buildStructure(OpenableElementInfo info, final IProgressMonitor pm, Map newElements, IResource underlyingResource) throws RubyModelException { // check if this compilation unit can be opened if (!isWorkingCopy()) { // no check is done on root kind or exclusion // pattern for working copies IStatus status = validateRubyScript(underlyingResource); if (!status.isOK()) throw newRubyModelException(status); } // prevents reopening of non-primary working copies (they are closed // when they are discarded and should not be reopened) if (!isPrimary() && getPerWorkingCopyInfo() == null) { throw newNotPresentException(); } RubyScriptElementInfo unitInfo = (RubyScriptElementInfo) info; // get buffer contents final char[] contents = getCharacters(pm, unitInfo); RubyModelManager.PerWorkingCopyInfo perWorkingCopyInfo = getPerWorkingCopyInfo(); IRubyProject project = getRubyProject(); boolean createAST; HashMap<String, CategorizedProblem[]> problems; if (info instanceof ASTHolderCUInfo) { ASTHolderCUInfo astHolder = (ASTHolderCUInfo) info; createAST = true; problems = astHolder.problems; } else { createAST = false; problems = null; } boolean computeProblems = RubyProject.hasRubyNature(project.getProject()) && perWorkingCopyInfo != null && perWorkingCopyInfo.isActive(); Node ast = null; try { ISourceElementRequestor requestor = new RubyScriptStructureBuilder(this, unitInfo, newElements); SourceElementParser sp = new SourceElementParser(requestor){ @Override public Object visitRootNode(RootNode iVisited) { lastGoodAST = iVisited; return super.visitRootNode(iVisited); } }; sp.parse(contents, getElementName().toCharArray()); ast = lastGoodAST; unitInfo.setIsStructureKnown(true); } catch (SyntaxException e) { unitInfo.setIsStructureKnown(false); unitInfo.setSyntaxException(e) ; } catch (Exception e) { RubyCore.log(e); } // update timestamp (might be IResource.NULL_STAMP if original does not // exist) if (underlyingResource == null) { underlyingResource = getResource(); } unitInfo.timestamp = ((IFile) underlyingResource).getModificationStamp(); // compute other problems if needed if (computeProblems) { if (problems == null) { // report problems to the problem requestor problems = new HashMap<String, CategorizedProblem[]>(); RubyScriptProblemFinder.process(this, contents, problems, pm); try { perWorkingCopyInfo.beginReporting(); for (Iterator<CategorizedProblem[]> iteraror = problems.values().iterator(); iteraror.hasNext();) { CategorizedProblem[] categorizedProblems = iteraror.next(); if (categorizedProblems == null) continue; for (int i = 0, length = categorizedProblems.length; i < length; i++) { perWorkingCopyInfo.acceptProblem(categorizedProblems[i]); } } } finally { perWorkingCopyInfo.endReporting(); } } else { // collect problems RubyScriptProblemFinder.process(this, contents, problems, pm); } perWorkingCopyInfo.endReporting(); } if (createAST) { ((ASTHolderCUInfo) info).ast = (RootNode) ast; } return unitInfo.isStructureKnown(); } protected char[] getCharacters(final IProgressMonitor pm, RubyScriptElementInfo unitInfo) throws RubyModelException { IBuffer buffer = getBufferManager().getBuffer(this); if (buffer == null) { buffer = openBuffer(pm, unitInfo); // open buffer independently // from the info, since we are // building the info } final char[] contents = buffer == null ? null : buffer.getCharacters(); return contents; } /* * Assume that this is a working copy */ protected void updateTimeStamp(RubyScript original) throws RubyModelException { long timeStamp = ((IFile) original.getResource()).getModificationStamp(); if (timeStamp == IResource.NULL_STAMP) { throw new RubyModelException(new RubyModelStatus(IRubyModelStatusConstants.INVALID_RESOURCE)); } ((RubyScriptElementInfo) getElementInfo()).timestamp = timeStamp; } protected IStatus validateRubyScript(IResource resource) { ISourceFolderRoot root = getSourceFolderRoot(); // root never null as validation is not done for working copies if (resource != null) { char[][] inclusionPatterns = ((SourceFolderRoot)root).fullInclusionPatternChars(); char[][] exclusionPatterns = ((SourceFolderRoot)root).fullExclusionPatternChars(); if (Util.isExcluded(resource, inclusionPatterns, exclusionPatterns)) return new RubyModelStatus(IRubyModelStatusConstants.ELEMENT_NOT_ON_CLASSPATH, this); if (!resource.isAccessible()) return new RubyModelStatus(IRubyModelStatusConstants.ELEMENT_DOES_NOT_EXIST, this); } return RubyConventions.validateRubyScriptName(getElementName()); } /** * @see IRubyScript#getElementAt(int) */ public IRubyElement getElementAt(int position) throws RubyModelException { IRubyElement e= getSourceElementAt(position); if (e == this) { return null; } return e; } public String getElementName() { return this.name; } /** * @throws RubyModelException * @see IRubyElement */ public IResource getUnderlyingResource() throws RubyModelException { if (isWorkingCopy() && !isPrimary()) return null; return super.getUnderlyingResource(); } public IResource getResource() { SourceFolderRoot root = getSourceFolderRoot(); if (root == null) return null; // working copy not in workspace if (root.isArchive()) { return root.getResource(); } else { return ((IContainer) getParent().getResource()).getFile(new Path(getElementName())); } } /* * @see IOpenable#close */ public void close() throws RubyModelException { if (getPerWorkingCopyInfo() != null) return; // a working copy must // remain opened until it is discarded super.close(); } /* * @see Openable#closing */ protected void closing(Object info) { if (getPerWorkingCopyInfo() == null) { super.closing(info); } // else the buffer of a working copy must remain open for the // lifetime of the working copy } /* * @see IRubyScript#getOwner() */ public WorkingCopyOwner getOwner() { return isPrimary() || !isWorkingCopy() ? null : this.owner; } /** * @see IRubyElement#getPath() */ public IPath getPath() { return getResource().getFullPath(); } /* * @see IRubyScript#getPrimary() */ public IRubyScript getPrimary() { return (IRubyScript) getPrimaryElement(true); } /* * @see RubyElement#getPrimaryElement(boolean) */ public IRubyElement getPrimaryElement(boolean checkOwner) { if (checkOwner && isPrimary()) return this; return new RubyScript((SourceFolder) getParent(), getElementName(), DefaultWorkingCopyOwner.PRIMARY); } /* * (non-Javadoc) * * @see org.rubypeople.rdt.internal.core.parser.RubyElement#getElementType() */ public int getElementType() { return RubyElement.SCRIPT; } /* * (non-Javadoc) * * @see org.rubypeople.rdt.core.IRubyScript#reconcile() */ public void reconcile() throws RubyModelException { reconcile(false, null, null); } /* * (non-Javadoc) * * @see org.rubypeople.rdt.core.IRubyScript#reconcile() */ public RootNode reconcile(boolean forceProblemDetection, WorkingCopyOwner workingCopyOwner, IProgressMonitor monitor) throws RubyModelException { if (!isWorkingCopy()) return null; // Reconciling is not supported on non working copies if (workingCopyOwner == null) workingCopyOwner = DefaultWorkingCopyOwner.PRIMARY; ReconcileWorkingCopyOperation op = new ReconcileWorkingCopyOperation(this, forceProblemDetection, workingCopyOwner); op.runOperation(monitor); return op.ast; } public IRubyScript getRubyScript() { return this; } public char[] getContents() { try { IBuffer buffer = this.getBuffer(); return buffer == null ? null : buffer.getCharacters(); } catch (RubyModelException e) { return new char[0]; } } /* * (non-Javadoc) * * @see org.rubypeople.rdt.core.ISourceReference#getSourceRange() */ public ISourceRange getSourceRange() throws RubyModelException { return ((RubyScriptElementInfo) getElementInfo()).getSourceRange(); } /** * @see IRubyScript#getType(String) */ public IType getType(String typeName) { return new RubyType(this, typeName); } /* * (non-Javadoc) * * @see org.rubypeople.rdt.core.ISourceReference#getSource() */ public String getSource() throws RubyModelException { IBuffer buffer = getBuffer(); if (buffer == null) return ""; //$NON-NLS-1$ return buffer.getContents(); } /** * @param unitInfo * @see Openable#openBuffer(IProgressMonitor) */ protected IBuffer openBuffer(IProgressMonitor pm, Object info) throws RubyModelException { boolean isWorkingCopy = isWorkingCopy(); IBuffer buffer = isWorkingCopy ? this.owner.createBuffer(this) : BufferManager.getDefaultBufferManager().createBuffer(this); if (buffer == null) return null; // set the buffer source if (buffer.getCharacters() == null) { if (isWorkingCopy) { IRubyScript original; if (!isPrimary() && (original = new RubyScript((SourceFolder) getParent(), getElementName(), DefaultWorkingCopyOwner.PRIMARY)).isOpen()) { buffer.setContents(original.getSource()); } else { IFile file = (IFile) getResource(); if (file == null || !file.exists()) { // initialize buffer with empty contents buffer.setContents(new char[0]); } else { buffer.setContents(Util.getResourceContentsAsCharArray(file)); } } } else { IFile file = (IFile) this.getResource(); if (file == null || !file.exists()) throw newNotPresentException(); buffer.setContents(Util.getResourceContentsAsCharArray(file)); } } // add buffer to buffer cache BufferManager bufManager = getBufferManager(); bufManager.addBuffer(buffer); // listen to buffer changes buffer.addBufferChangedListener(this); return buffer; } public boolean isPrimary() { return this.owner == DefaultWorkingCopyOwner.PRIMARY; } /* * @see IRubyScript#isWorkingCopy() */ public boolean isWorkingCopy() { return !isPrimary() || getPerWorkingCopyInfo() != null; } /* * Returns the per working copy info for the receiver, or null if none * exist. Note: the use count of the per working copy info is NOT * incremented. */ public RubyModelManager.PerWorkingCopyInfo getPerWorkingCopyInfo() { return RubyModelManager.getRubyModelManager().getPerWorkingCopyInfo(this, false/* * don't * create */, false/* * don't record usage */, null); } /** * @throws RubyModelException * @see IRubyScript#getWorkingCopy(IProgressMonitor) */ public IRubyScript getWorkingCopy(IProgressMonitor monitor) throws RubyModelException { return getWorkingCopy(new WorkingCopyOwner() {/* * non shared working * copy */ }, null, monitor); } /** * @throws RubyModelException * @see IRubyScript#getWorkingCopy(WorkingCopyOwner, IProblemRequestor, * IProgressMonitor) */ public IRubyScript getWorkingCopy(WorkingCopyOwner workingCopyOwner, IProblemRequestor problemRequestor, IProgressMonitor monitor) throws RubyModelException { if (!isPrimary()) return this; RubyModelManager manager = RubyModelManager.getRubyModelManager(); RubyScript workingCopy = new RubyScript((SourceFolder) getParent(), getElementName(), workingCopyOwner); RubyModelManager.PerWorkingCopyInfo perWorkingCopyInfo = manager.getPerWorkingCopyInfo(workingCopy, false/* * don't * create */, true/* * record usage */, null); if (perWorkingCopyInfo != null) { return perWorkingCopyInfo.getWorkingCopy(); // return // existing // handle instead of the // one created above } BecomeWorkingCopyOperation op = new BecomeWorkingCopyOperation(workingCopy, problemRequestor); op.runOperation(monitor); return workingCopy; } /** * */ public void becomeWorkingCopy(IProblemRequestor requestor, IProgressMonitor monitor) throws RubyModelException { RubyModelManager manager = RubyModelManager.getRubyModelManager(); RubyModelManager.PerWorkingCopyInfo perWorkingCopyInfo = manager.getPerWorkingCopyInfo(this, false/* * don't * create */, true /* record usage */, null); if (perWorkingCopyInfo == null) { // close cu and its children close(); BecomeWorkingCopyOperation operation = new BecomeWorkingCopyOperation(this, requestor); operation.runOperation(monitor); } } /** * @see IRubyScript#commitWorkingCopy(boolean, IProgressMonitor) */ public void commitWorkingCopy(boolean force, IProgressMonitor monitor) throws RubyModelException { CommitWorkingCopyOperation op = new CommitWorkingCopyOperation(this, force); op.runOperation(monitor); } /** * Returns true if this handle represents the same Java element as the given * handle. * * @see Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof RubyScript)) return false; RubyScript other = (RubyScript) obj; return this.owner.equals(other.owner) && super.equals(obj); } public boolean exists() { // working copy always exists in the model until it is gotten rid of // (even if not on classpath) if (getPerWorkingCopyInfo() != null) return true; // if not a working copy, it exists only if it is a primary compilation // unit return isPrimary(); } /* * @see Openable#canBeRemovedFromCache */ public boolean canBeRemovedFromCache() { if (getPerWorkingCopyInfo() != null) return false; // working copies // should remain in // the cache until // they are // destroyed return super.canBeRemovedFromCache(); } /* * @see Openable#canBufferBeRemovedFromCache */ public boolean canBufferBeRemovedFromCache(IBuffer buffer) { if (getPerWorkingCopyInfo() != null) return false; // working copy // buffers should // remain in the // cache until // working copy is // destroyed return super.canBufferBeRemovedFromCache(buffer); } /** * @see Openable#hasBuffer() */ protected boolean hasBuffer() { return true; } /* * @see IRubyScript#hasResourceChanged() */ public boolean hasResourceChanged() { if (!isWorkingCopy()) return false; // if resource got deleted, then #getModificationStamp() will answer // IResource.NULL_STAMP, which is always different from the cached // timestamp Object info = RubyModelManager.getRubyModelManager().getInfo(this); if (info == null) return false; return ((RubyScriptElementInfo) info).timestamp != getResource().getModificationStamp(); } /** * @see IOpenable#isConsistent() */ public boolean isConsistent() { return !RubyModelManager.getRubyModelManager().getElementsOutOfSynchWithBuffers().contains(this); } /** * @see IOpenable#makeConsistent(IProgressMonitor) */ public void makeConsistent(IProgressMonitor monitor) throws RubyModelException { makeConsistent(false, null, monitor); } public RootNode makeConsistent(boolean createAST, HashMap<String, CategorizedProblem[]> problems, IProgressMonitor monitor) throws RubyModelException { if (isConsistent()) return null; if (createAST) { ASTHolderCUInfo info = new ASTHolderCUInfo(); info.problems = problems; openWhenClosed(info, monitor); RootNode result = info.ast; info.ast = null; return result; } openWhenClosed(createElementInfo(), monitor); return null; } /* * @see IRubyScript#discardWorkingCopy */ public void discardWorkingCopy() throws RubyModelException { // discard working copy and its children DiscardWorkingCopyOperation op = new DiscardWorkingCopyOperation(this); op.runOperation(null); } /** * @see IOpenable */ public void save(IProgressMonitor pm, boolean force) throws RubyModelException { if (isWorkingCopy()) { // no need to save the buffer for a working copy (this is a noop) reconcile(); // not simply makeConsistent, also computes // fine-grain deltas // in case the working copy is being reconciled already (if not it // would miss // one iteration of deltas). } else { super.save(pm, force); } } /** * @see IRubyScript#getImports() */ public IImportDeclaration[] getImports() throws RubyModelException { IImportContainer container = getImportContainer(); if (container.exists()) { IRubyElement[] elements = container.getChildren(); IImportDeclaration[] imprts = new IImportDeclaration[elements.length]; System.arraycopy(elements, 0, imprts, 0, elements.length); return imprts; } else if (!exists()) { throw newNotPresentException(); } else { return new IImportDeclaration[0]; } } /** * @see IRubyScript#getImport(String) */ public IImportDeclaration getImport(String importName) { return new RubyImport((ImportContainer) getImportContainer(), importName); } /** * @see IRubyScript#getImportContainer() */ public IImportContainer getImportContainer() { return new ImportContainer(this); } /** * @see IRubyScript#getTypeNames() */ public IType[] getTypes() throws RubyModelException { ArrayList<IRubyElement> list = getChildrenOfType(TYPE); IType[] array= new IType[list.size()]; list.toArray(array); return array; } /** * @see IRubyScript#findPrimaryType() */ public IType findPrimaryType() { String typeName = Util.getNameWithoutRubyLikeExtension(getElementName()); typeName = Util.identifierToConstant(typeName); IType primaryType= getType(typeName); if (primaryType.exists()) { return primaryType; } try { IType[] types = getTypes(); if (types != null && types.length > 0) { return types[0]; } } catch (RubyModelException e) { RubyCore.log(e); } return null; } /** * @see ICodeAssist#codeSelect(int, int) */ public IRubyElement[] codeSelect(int offset, int length) throws RubyModelException { return codeSelect(offset, length, DefaultWorkingCopyOwner.PRIMARY); } /** * @see ICodeAssist#codeSelect(int, int, WorkingCopyOwner) */ public IRubyElement[] codeSelect(int offset, int length, WorkingCopyOwner workingCopyOwner) throws RubyModelException { return super.codeSelect(this, offset, length, workingCopyOwner); } public void codeComplete(int offset, CompletionRequestor requestor) throws RubyModelException { CompletionEngine engine = new CompletionEngine(requestor); engine.complete(this, offset); } /* * @see RubyElement */ public IRubyElement getHandleFromMemento(String token, MementoTokenizer memento, WorkingCopyOwner workingCopyOwner) { switch (token.charAt(0)) { case JEM_IMPORTDECLARATION: RubyElement container = (RubyElement)getImportContainer(); return container.getHandleFromMemento(token, memento, workingCopyOwner); case JEM_TYPE: if (!memento.hasMoreTokens()) return this; String typeName = memento.nextToken(); RubyElement type = (RubyElement)getType(typeName); return type.getHandleFromMemento(memento, workingCopyOwner); } return null; } /** * @see RubyElement#getHandleMementoDelimiter() */ protected char getHandleMementoDelimiter() { return RubyElement.JEM_RUBYSCRIPT; } /** * @see IRubyScript#getAllTypes() */ public IType[] getAllTypes() throws RubyModelException { IType[] types = getTypes(); int i; ArrayList<IType> allTypes = new ArrayList<IType>(types.length); ArrayList<IType> typesToTraverse = new ArrayList<IType>(types.length); for (i = 0; i < types.length; i++) { typesToTraverse.add(types[i]); } while (!typesToTraverse.isEmpty()) { IType type = typesToTraverse.get(0); typesToTraverse.remove(type); allTypes.add(type); types = type.getTypes(); for (i = 0; i < types.length; i++) { typesToTraverse.add(types[i]); } } IType[] arrayOfAllTypes = new IType[allTypes.size()]; allTypes.toArray(arrayOfAllTypes); return arrayOfAllTypes; } }