| /******************************************************************************* |
| * Copyright (c) 2005, 2017 IBM Corporation and others. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| *******************************************************************************/ |
| package org.eclipse.dltk.internal.core; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.dltk.codeassist.ICompletionEngine; |
| import org.eclipse.dltk.codeassist.ISelectionEngine; |
| import org.eclipse.dltk.codeassist.ISelectionRequestor; |
| import org.eclipse.dltk.compiler.env.IModuleSource; |
| import org.eclipse.dltk.compiler.problem.DefaultProblem; |
| import org.eclipse.dltk.compiler.problem.IProblemIdentifier; |
| import org.eclipse.dltk.compiler.problem.ProblemSeverity; |
| import org.eclipse.dltk.core.BufferChangedEvent; |
| import org.eclipse.dltk.core.CompletionRequestor; |
| import org.eclipse.dltk.core.DLTKCore; |
| import org.eclipse.dltk.core.DLTKLanguageManager; |
| import org.eclipse.dltk.core.IBuffer; |
| import org.eclipse.dltk.core.IBufferChangedListener; |
| import org.eclipse.dltk.core.ICompletionRequestorExtension; |
| import org.eclipse.dltk.core.IDLTKLanguageToolkit; |
| import org.eclipse.dltk.core.IModelElement; |
| import org.eclipse.dltk.core.IModelStatusConstants; |
| import org.eclipse.dltk.core.IOpenable; |
| import org.eclipse.dltk.core.IProjectFragment; |
| import org.eclipse.dltk.core.IScriptProject; |
| import org.eclipse.dltk.core.ISourceRange; |
| import org.eclipse.dltk.core.ModelException; |
| import org.eclipse.dltk.core.ScriptModelUtil; |
| import org.eclipse.dltk.core.WorkingCopyOwner; |
| |
| /** |
| * Abstract class for implementations of model elements which are IOpenable. |
| * |
| * @see IModelElement |
| * @see IOpenable |
| */ |
| public abstract class Openable extends ModelElement |
| implements IOpenable, IBufferChangedListener { |
| |
| protected Openable(ModelElement parent) { |
| super(parent); |
| } |
| |
| /** |
| * The buffer associated with this element has changed. Registers this |
| * element as being out of synch with its buffer's contents. If the buffer |
| * has been closed, this element is set as NOT out of synch with the |
| * contents. |
| * |
| * @see IBufferChangedListener |
| */ |
| @Override |
| public void bufferChanged(BufferChangedEvent event) { |
| if (event.getBuffer().isClosed()) { |
| ModelManager.getModelManager().getElementsOutOfSynchWithBuffers() |
| .remove(this); |
| getBufferManager().removeBuffer(event.getBuffer()); |
| } else { |
| ModelManager.getModelManager().getElementsOutOfSynchWithBuffers() |
| .add(this); |
| } |
| } |
| |
| /** |
| * Builds this element's structure and properties in the given info object, |
| * based on this element's current contents (reuse buffer contents if this |
| * element has an open buffer, or resource contents if this element does not |
| * have an open buffer). Children are placed in the given newElements table |
| * (note, this element has already been placed in the newElements table). |
| * Returns true if successful, or false if an error is encountered while |
| * determining the structure of this element. |
| */ |
| protected abstract boolean buildStructure(OpenableElementInfo info, |
| IProgressMonitor pm, Map newElements, IResource underlyingResource) |
| throws ModelException; |
| |
| /* |
| * Returns whether this element can be removed from the model cache to make |
| * space. |
| */ |
| public boolean canBeRemovedFromCache() { |
| try { |
| return !hasUnsavedChanges(); |
| } catch (ModelException e) { |
| return false; |
| } |
| } |
| |
| /* |
| * Returns whether the buffer of this element can be removed from the Script |
| * model cache to make space. |
| */ |
| public boolean canBufferBeRemovedFromCache(IBuffer buffer) { |
| return !buffer.hasUnsavedChanges(); |
| } |
| |
| /** |
| * Close the buffer associated with this element, if any. |
| */ |
| protected void closeBuffer() { |
| if (!hasBuffer()) |
| return; // nothing to do |
| IBuffer buffer = getBufferManager().getBuffer(this); |
| if (buffer != null) { |
| buffer.close(); |
| buffer.removeBufferChangedListener(this); |
| } |
| } |
| |
| /** |
| * This element is being closed. Do any necessary cleanup. |
| */ |
| @Override |
| protected void closing(Object info) { |
| closeBuffer(); |
| } |
| |
| /** |
| * @see IModelElement |
| */ |
| @Override |
| public boolean exists() { |
| ModelManager manager = ModelManager.getModelManager(); |
| if (manager.getInfo(this) != null) |
| return true; |
| if (!parentExists()) |
| return false; |
| IProjectFragment root = getProjectFragment(); |
| if (root != null && (root == this || !root.isArchive())) { |
| return resourceExists(); |
| } |
| return super.exists(); |
| } |
| |
| @Override |
| protected void generateInfos(Object info, HashMap newElements, |
| IProgressMonitor monitor) throws ModelException { |
| |
| if (ModelManager.VERBOSE) { |
| String element; |
| switch (getElementType()) { |
| case SCRIPT_PROJECT: |
| element = "project"; //$NON-NLS-1$ |
| break; |
| case PROJECT_FRAGMENT: |
| element = "fragment"; //$NON-NLS-1$ |
| break; |
| case SCRIPT_FOLDER: |
| element = "folder"; //$NON-NLS-1$ |
| break; |
| case BINARY_MODULE: |
| element = "binary module"; //$NON-NLS-1$ |
| break; |
| case SOURCE_MODULE: |
| element = "source module"; //$NON-NLS-1$ |
| break; |
| default: |
| element = "element"; //$NON-NLS-1$ |
| } |
| System.out.println(Thread.currentThread() + " OPENING " + element //$NON-NLS-1$ |
| + " " + this.toStringWithAncestors()); //$NON-NLS-1$ |
| } |
| |
| // open the parent if necessary |
| openParent(info, newElements, monitor); |
| if (monitor != null && monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| |
| // puts the info before building the structure so that questions to the |
| // handle behave as if the element existed |
| // (case of compilation units becoming working copies) |
| newElements.put(this, info); |
| |
| // build the structure of the openable (this will open the buffer if |
| // needed) |
| try { |
| OpenableElementInfo openableElementInfo = (OpenableElementInfo) info; |
| boolean isStructureKnown = buildStructure(openableElementInfo, |
| monitor, newElements, getResource()); |
| openableElementInfo.setIsStructureKnown(isStructureKnown); |
| } catch (ModelException e) { |
| newElements.remove(this); |
| throw e; |
| } |
| |
| // remove out of sync buffer for this element |
| ModelManager.getModelManager().getElementsOutOfSynchWithBuffers() |
| .remove(this); |
| |
| if (ModelManager.VERBOSE) { |
| System.out.println(ModelManager.getModelManager().cache |
| .toStringFillingRation("-> ")); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Note: a buffer with no unsaved changes can be closed by the Model since |
| * it has a finite number of buffers allowed open at one time. If this is |
| * the first time a request is being made for the buffer, an attempt is made |
| * to create and fill this element's buffer. If the buffer has been closed |
| * since it was first opened, the buffer is re-created. |
| * |
| * @see IOpenable |
| */ |
| @Override |
| public IBuffer getBuffer() throws ModelException { |
| if (hasBuffer()) { |
| // ensure element is open |
| Object info = getElementInfo(); |
| IBuffer buffer = getBufferManager().getBuffer(this); |
| if (buffer == null) { |
| // try to (re)open a buffer |
| buffer = openBuffer(null, info); |
| } |
| return buffer; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns {@link IBuffer} or <code>null</code> if this element is not |
| * opened yet. |
| */ |
| public IBuffer getBufferNotOpen() throws ModelException { |
| if (hasBuffer()) { |
| return getBufferManager().getBuffer(this); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the buffer manager for this element. |
| */ |
| protected BufferManager getBufferManager() { |
| return BufferManager.getDefaultBufferManager(); |
| } |
| |
| /** |
| * Return my underlying resource. Elements that may not have a corresponding |
| * resource must override this method. |
| * |
| * @see IScriptElement |
| */ |
| @Override |
| public IResource getCorrespondingResource() throws ModelException { |
| return getUnderlyingResource(); |
| } |
| |
| /* |
| * @see IModelElement |
| */ |
| @Override |
| public IOpenable getOpenable() { |
| return this; |
| } |
| |
| @Override |
| public IResource getUnderlyingResource() throws ModelException { |
| IResource parentResource = this.parent.getUnderlyingResource(); |
| if (parentResource == null) { |
| return null; |
| } |
| int type = parentResource.getType(); |
| if (type == IResource.FOLDER || type == IResource.PROJECT) { |
| IContainer folder = (IContainer) parentResource; |
| IResource resource = folder.findMember(getElementName()); |
| if (resource == null) { |
| throw newNotPresentException(); |
| } else { |
| return resource; |
| } |
| } else { |
| return parentResource; |
| } |
| } |
| |
| /** |
| * Returns true if this element may have an associated source buffer, |
| * otherwise false. Subclasses must override as required. |
| */ |
| protected boolean hasBuffer() { |
| return false; |
| } |
| |
| /** |
| * @see IOpenable |
| */ |
| @Override |
| public boolean hasUnsavedChanges() throws ModelException { |
| |
| if (isReadOnly() || !isOpen()) { |
| return false; |
| } |
| if (hasBuffer()) { |
| IBuffer buf = this.getBufferNotOpen(); |
| if (buf != null && buf.hasUnsavedChanges()) { |
| return true; |
| } |
| } |
| // for package fragments, package fragment roots, and projects must |
| // check open buffers |
| // to see if they have an child with unsaved changes |
| int elementType = getElementType(); |
| if (elementType == SCRIPT_FOLDER || elementType == PROJECT_FRAGMENT |
| || elementType == SCRIPT_PROJECT |
| || elementType == SCRIPT_MODEL) { // fix |
| // for |
| // 1FWNMHH |
| Enumeration openBuffers = getBufferManager().getOpenBuffers(); |
| while (openBuffers.hasMoreElements()) { |
| IBuffer buffer = (IBuffer) openBuffers.nextElement(); |
| if (buffer.hasUnsavedChanges()) { |
| IModelElement owner = buffer.getOwner(); |
| if (isAncestorOf(owner)) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Subclasses must override as required. |
| * |
| * @see IOpenable |
| */ |
| @Override |
| public boolean isConsistent() { |
| return true; |
| } |
| |
| /** |
| * |
| * @see IOpenable |
| */ |
| @Override |
| public boolean isOpen() { |
| return ModelManager.getModelManager().getInfo(this) != null; |
| } |
| |
| /** |
| * Returns true if this represents a source element. Openable source |
| * elements have an associated buffer created when they are opened. |
| */ |
| protected boolean isSourceElement() { |
| return false; |
| } |
| |
| /** |
| * @see IModelElement |
| */ |
| @Override |
| public boolean isStructureKnown() throws ModelException { |
| return ((OpenableElementInfo) getElementInfo()).isStructureKnown(); |
| } |
| |
| /** |
| * @see IOpenable |
| */ |
| @Override |
| public void makeConsistent(IProgressMonitor monitor) throws ModelException { |
| // only source modules can be inconsistent |
| // other openables cannot be inconsistent so default is to do nothing |
| } |
| |
| /** |
| * @see IOpenable |
| */ |
| @Override |
| public void open(IProgressMonitor pm) throws ModelException { |
| getElementInfo(pm); |
| } |
| |
| /** |
| * Opens a buffer on the contents of this element, and returns the buffer, |
| * or returns <code>null</code> if opening fails. By default, do nothing - |
| * subclasses that have buffers must override as required. |
| */ |
| protected IBuffer openBuffer(IProgressMonitor pm, Object info) |
| throws ModelException { |
| return null; |
| } |
| |
| /** |
| * Open the parent element if necessary. |
| */ |
| protected void openParent(Object childInfo, HashMap newElements, |
| IProgressMonitor pm) throws ModelException { |
| |
| Openable openableParent = (Openable) getOpenableParent(); |
| if (openableParent != null && !openableParent.isOpen()) { |
| openableParent.generateInfos(openableParent.createElementInfo(), |
| newElements, pm); |
| } |
| } |
| |
| /** |
| * Answers true if the parent exists (null parent is answering true) |
| * |
| */ |
| protected boolean parentExists() { |
| IModelElement parentElement = getParent(); |
| if (parentElement == null) |
| return true; |
| return parentElement.exists(); |
| } |
| |
| /** |
| * Returns whether the corresponding resource or associated file exists |
| */ |
| protected boolean resourceExists() { |
| IWorkspace workspace = ResourcesPlugin.getWorkspace(); |
| if (workspace == null) |
| return false; // workaround for |
| // http://bugs.eclipse.org/bugs/show_bug.cgi?id=34069 |
| return Model.getTarget(workspace.getRoot(), |
| this.getPath().makeRelative(), // ensure path is relative (see |
| // http://dev.eclipse.org/bugs/show_bug.cgi?id=22517) |
| true) != null; |
| } |
| |
| /** |
| * @see IOpenable |
| */ |
| @Override |
| public void save(IProgressMonitor pm, boolean force) throws ModelException { |
| if (isReadOnly()) { |
| throw new ModelException( |
| new ModelStatus(IModelStatusConstants.READ_ONLY, this)); |
| } |
| IBuffer buf = getBuffer(); |
| if (buf != null) { // some Openables (like a ScriptProject) don't have |
| // a |
| // buffer |
| buf.save(pm, force); |
| this.makeConsistent(pm); // update the element info of this |
| // element |
| } |
| } |
| |
| /** |
| * Find enclosing project fragment if any |
| */ |
| public IProjectFragment getProjectFragment() { |
| return (IProjectFragment) getAncestor(IModelElement.PROJECT_FRAGMENT); |
| } |
| |
| static class CompletionThread extends Thread { |
| final IDLTKLanguageToolkit toolkit; |
| final IScriptProject project; |
| final IModuleSource cu; |
| final int position; |
| final CompletionRequestor requestor; |
| final NullProgressMonitor monitor = new NullProgressMonitor(); |
| |
| public CompletionThread(IDLTKLanguageToolkit toolkit, |
| IScriptProject project, IModuleSource cu, int position, |
| CompletionRequestor requestor) { |
| super("CompletionThread-" + toolkit.getLanguageName()); |
| this.toolkit = toolkit; |
| this.project = project; |
| this.cu = cu; |
| this.position = position; |
| this.requestor = requestor; |
| } |
| |
| private boolean done = false; |
| |
| @Override |
| public void run() { |
| // code complete |
| final ICompletionEngine[] engines = DLTKLanguageManager |
| .getCompletionEngines(toolkit.getNatureId()); |
| if (engines != null) { |
| for (ICompletionEngine engine : engines) { |
| run(engine); |
| if (requestor.isIgnored(CompletionRequestor.ALL)) { |
| break; |
| } |
| if (monitor.isCanceled()) |
| break; |
| } |
| } |
| done = true; |
| } |
| |
| private void run(ICompletionEngine engine) { |
| // engine.setEnvironment(environment); |
| engine.setRequestor(requestor); |
| |
| engine.setOptions(project.getOptions(true)); |
| engine.setProject(project); |
| |
| engine.setProgressMonitor(monitor); |
| engine.complete(cu, position, 0); |
| } |
| |
| boolean execute(long timeout) { |
| start(); |
| try { |
| join(timeout); |
| if (!done) { |
| monitor.setCanceled(true); |
| interrupt(); |
| } |
| return done; |
| } catch (InterruptedException e) { |
| if (DLTKCore.DEBUG) { |
| e.printStackTrace(); |
| } |
| return false; |
| } |
| } |
| } |
| |
| /** Code Completion */ |
| protected void codeComplete(final IModuleSource cu, final int position, |
| CompletionRequestor requestor, WorkingCopyOwner owner, long timeout) |
| throws ModelException { |
| if (requestor == null) { |
| throw new IllegalArgumentException( |
| Messages.Openable_completionRequesterCannotBeNull); |
| } |
| |
| IBuffer buffer = getBuffer(); |
| if (buffer == null) { |
| return; |
| } |
| if (position < -1 || position > buffer.getLength()) { |
| if (DLTKCore.DEBUG) { |
| throw new ModelException(new ModelStatus( |
| IModelStatusConstants.INDEX_OUT_OF_BOUNDS)); |
| } |
| return; |
| } |
| |
| IDLTKLanguageToolkit toolkit = DLTKLanguageManager |
| .getLanguageToolkit(this); |
| if (toolkit == null) { |
| return; |
| } |
| |
| CompletionThread thread = new CompletionThread(toolkit, |
| getScriptProject(), cu, position, requestor); |
| if (!thread.execute(timeout)) { |
| Thread.interrupted(); |
| requestor.completionFailure(new DefaultProblem( |
| "Computation of proposals takes too long. Please try again.", |
| IProblemIdentifier.NULL, null, ProblemSeverity.WARNING, 0, |
| 0, 0)); |
| if (requestor instanceof ICompletionRequestorExtension) { |
| ((ICompletionRequestorExtension) requestor).reset(); |
| } |
| } |
| } |
| |
| static class ModelElementSelectionRequestor implements ISelectionRequestor { |
| private final List<IModelElement> elements = new ArrayList<>(); |
| private List<Object> foreignElements; |
| private Map<Object, ISourceRange> ranges; |
| |
| @Override |
| public void acceptForeignElement(Object element) { |
| if (element instanceof IModelElement) { |
| acceptModelElement((IModelElement) element); |
| } else { |
| if (foreignElements == null) { |
| foreignElements = new ArrayList<>(); |
| } |
| foreignElements.add(element); |
| } |
| } |
| |
| @Override |
| public void acceptModelElement(IModelElement element) { |
| elements.add(element); |
| } |
| |
| @Override |
| public void acceptElement(Object element, ISourceRange range) { |
| acceptForeignElement(element); |
| if (range != null) { |
| if (ranges == null) { |
| ranges = new HashMap<>(); |
| } |
| ranges.put(element, range); |
| } |
| } |
| |
| void addModelElements(IModelElement[] elements) { |
| Collections.addAll(this.elements, elements); |
| } |
| |
| IModelElement[] toModelElementArray() { |
| if (elements.isEmpty()) { |
| return ScriptModelUtil.NO_ELEMENTS; |
| } else { |
| return elements.toArray(new IModelElement[elements.size()]); |
| } |
| } |
| |
| boolean isEmpty() { |
| return elements.isEmpty() |
| && (foreignElements == null || foreignElements.isEmpty()); |
| } |
| |
| CodeSelection asResponse() { |
| if (isEmpty()) { |
| return null; |
| } else { |
| final IModelElement[] elementArray = elements != null |
| && !elements.isEmpty() |
| ? elements.toArray( |
| new IModelElement[elements.size()]) |
| : null; |
| final Object[] foreignElementArray = foreignElements != null |
| && !foreignElements.isEmpty() |
| ? foreignElements.toArray() |
| : null; |
| return new CodeSelection(elementArray, foreignElementArray, |
| ranges); |
| } |
| } |
| } |
| |
| private void codeSelect(org.eclipse.dltk.compiler.env.IModuleSource cu, |
| int offset, int length, WorkingCopyOwner owner, |
| ModelElementSelectionRequestor requestor) throws ModelException { |
| |
| ScriptProject project = (ScriptProject) getScriptProject(); |
| |
| IBuffer buffer = getBuffer(); |
| int end = -1; |
| if (buffer != null) { |
| end = buffer.getLength(); |
| } |
| IDLTKLanguageToolkit toolkit = DLTKLanguageManager |
| .getLanguageToolkit(this); |
| if (toolkit == null) { |
| if (DLTKCore.VERBOSE) { |
| System.out.println( |
| "DLTK.Openable.VERBOSE: Failed to detect language toolkit... for module:" //$NON-NLS-1$ |
| + this.getResource().getName()); |
| } |
| return; |
| } |
| |
| if (offset < 0 || length < 0 |
| || (end != -1 && (offset + length > end))) { |
| throw new ModelException( |
| new ModelStatus(IModelStatusConstants.INDEX_OUT_OF_BOUNDS)); |
| } |
| |
| final ISelectionEngine[] engines = DLTKLanguageManager |
| .getSelectionEngines(toolkit.getNatureId()); |
| if (engines != null) { |
| for (ISelectionEngine engine : engines) { |
| engine.setOptions(project.getOptions(true)); |
| engine.setRequestor(requestor); |
| final IModelElement[] result = engine.select(cu, offset, |
| offset + length - 1); |
| if (result != null) { |
| requestor.addModelElements(result); |
| } |
| if (!requestor.isEmpty()) { |
| return; |
| } |
| } |
| } |
| } |
| |
| protected IModelElement[] codeSelect( |
| org.eclipse.dltk.compiler.env.IModuleSource cu, int offset, |
| int length, WorkingCopyOwner owner) throws ModelException { |
| final ModelElementSelectionRequestor requestor = new ModelElementSelectionRequestor(); |
| codeSelect(cu, offset, length, owner, requestor); |
| return requestor.toModelElementArray(); |
| } |
| |
| protected CodeSelection codeSelectAll( |
| org.eclipse.dltk.compiler.env.IModuleSource cu, int offset, |
| int length, WorkingCopyOwner owner) throws ModelException { |
| final ModelElementSelectionRequestor requestor = new ModelElementSelectionRequestor(); |
| codeSelect(cu, offset, length, owner, requestor); |
| return requestor.asResponse(); |
| } |
| |
| } |