| /*=============================================================================# |
| # Copyright (c) 2007, 2020 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ltk.ui.sourceediting.folding; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.QualifiedName; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jface.text.AbstractDocument; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.source.projection.ProjectionAnnotation; |
| import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| |
| import org.eclipse.statet.ecommons.preferences.PreferencesUtil; |
| import org.eclipse.statet.ecommons.preferences.SettingsChangeNotifier.ChangeListener; |
| import org.eclipse.statet.ecommons.text.TextUtil; |
| |
| import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin; |
| import org.eclipse.statet.ltk.ast.core.AstInfo; |
| import org.eclipse.statet.ltk.core.ISourceModelStamp; |
| import org.eclipse.statet.ltk.model.core.IModelElementDelta; |
| import org.eclipse.statet.ltk.model.core.IModelManager; |
| import org.eclipse.statet.ltk.model.core.elements.IModelElement; |
| import org.eclipse.statet.ltk.model.core.elements.ISourceUnit; |
| import org.eclipse.statet.ltk.model.core.elements.ISourceUnitModelInfo; |
| import org.eclipse.statet.ltk.model.core.elements.IWorkspaceSourceUnit; |
| import org.eclipse.statet.ltk.ui.IModelElementInputListener; |
| import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor; |
| import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditorAddon; |
| import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor1; |
| |
| |
| public class FoldingEditorAddon implements ISourceEditorAddon, IModelElementInputListener, ChangeListener { |
| |
| |
| public static final class FoldingStructureComputationContext { |
| |
| public final AbstractDocument document; |
| public final ISourceUnitModelInfo model; |
| public final AstInfo ast; |
| |
| public final boolean isInitial; |
| |
| private final SortedMap<Position, FoldingAnnotation> table= new TreeMap<>(TextUtil.POSITION_COMPARATOR); |
| |
| |
| protected FoldingStructureComputationContext(final AbstractDocument document, |
| final ISourceUnitModelInfo model, final AstInfo ast, final boolean isInitial) { |
| this.document= document; |
| this.model= model; |
| this.ast= ast; |
| |
| this.isInitial= isInitial; |
| } |
| |
| |
| public void addFoldingRegion(final FoldingAnnotation ann) { |
| if (!this.table.containsKey(ann.getPosition())) { |
| this.table.put(ann.getPosition(), ann); |
| } |
| } |
| |
| } |
| |
| private static final class Input { |
| |
| private final ISourceUnit unit; |
| |
| private boolean isInitilized; |
| private ISourceModelStamp updateStamp; |
| |
| private QualifiedName savePropertyName; |
| |
| public ProjectionAnnotationModel annotationModel; |
| |
| Input(final ISourceUnit unit) { |
| this.unit= unit; |
| this.isInitilized= false; |
| } |
| |
| } |
| |
| |
| private final FoldingProvider provider; |
| |
| private SourceEditor1 editor; |
| |
| private volatile Input input; |
| |
| |
| public FoldingEditorAddon(final FoldingProvider provider) { |
| this.provider= provider; |
| } |
| |
| |
| @Override |
| public void install(final ISourceEditor editor) { |
| this.editor= (SourceEditor1) editor; |
| PreferencesUtil.getSettingsChangeNotifier().addChangeListener(this); |
| this.provider.checkConfig(null); |
| this.editor.getModelInputProvider().addListener(this); |
| } |
| |
| @Override |
| public void elementChanged(final IModelElement element) { |
| final Input input= (element != null) ? new Input((ISourceUnit) element) : null; |
| synchronized (this) { |
| if (this.input != null) { |
| saveState(this.input); |
| } |
| this.input= input; |
| } |
| } |
| |
| @Override |
| public void elementInitialInfo(final IModelElement element) { |
| final Input input= this.input; |
| if (input != null && input.unit == element) { |
| update(input, null); |
| } |
| } |
| |
| @Override |
| public void elementUpdatedInfo(final IModelElement element, final IModelElementDelta delta) { |
| final Input input= this.input; |
| if (input != null && input.unit == element) { |
| update(input, delta.getNewAst().getStamp()); |
| } |
| } |
| |
| @Override |
| public void uninstall() { |
| PreferencesUtil.getSettingsChangeNotifier().removeChangeListener(this); |
| if (this.editor != null) { |
| this.editor.getModelInputProvider().removeListener(this); |
| this.editor= null; |
| } |
| } |
| |
| protected void refresh() { |
| final Input input= this.input; |
| if (input != null) { |
| update(input, null); |
| } |
| } |
| |
| @Override |
| public void settingsChanged(final Set<String> groupIds) { |
| if (groupIds != null && this.provider.checkConfig(groupIds)) { |
| refresh(); |
| } |
| } |
| |
| private FoldingStructureComputationContext createCtx(final Input input) { |
| if (input.unit == null) { |
| return null; |
| } |
| final IProgressMonitor monitor= new NullProgressMonitor(); |
| |
| final ISourceUnitModelInfo modelInfo; |
| final AstInfo ast; |
| if (this.provider.requiresModel()) { |
| modelInfo= input.unit.getModelInfo(null, IModelManager.MODEL_FILE, monitor); |
| if (modelInfo == null) { |
| return null; |
| } |
| ast= modelInfo.getAst(); |
| } |
| else { |
| modelInfo= null; |
| ast= input.unit.getAstInfo(null, false, monitor); |
| } |
| final AbstractDocument document= input.unit.getDocument(monitor); |
| if (ast == null || document == null || ast.getStamp().getSourceStamp() != document.getModificationStamp()) { |
| return null; |
| } |
| return new FoldingStructureComputationContext(document, modelInfo, ast, !input.isInitilized); |
| } |
| |
| private void update(final Input input, final ISourceModelStamp stamp) { |
| synchronized(input) { |
| final SourceEditor1 editor= this.editor; |
| if (editor == null) { |
| return; |
| } |
| if (input.unit == null |
| || (stamp != null && stamp.equals(input.updateStamp)) ) { // already uptodate |
| return; |
| } |
| FoldingStructureComputationContext ctx; |
| if (input != this.input) { |
| return; |
| } |
| ctx= createCtx(input); |
| if (ctx == null) { |
| return; |
| } |
| try { |
| this.provider.collectRegions(ctx); |
| } |
| catch (final InvocationTargetException e) { |
| return; |
| } |
| |
| ProjectionAnnotation[] deletions; |
| if (ctx.isInitial) { |
| deletions= null; |
| input.annotationModel= this.editor.getAdapter(ProjectionAnnotationModel.class); |
| if (input.annotationModel == null) { |
| return; |
| } |
| input.isInitilized= true; |
| input.savePropertyName= new QualifiedName("org.eclipse.statet.ltk", "FoldingState-" + editor.getSite().getId()); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| if (this.provider.isRestoreStateEnabled()) { |
| loadState(input, ctx.table); |
| } |
| } |
| else { |
| final ProjectionAnnotationModel model= input.annotationModel; |
| final List<FoldingAnnotation> del= new ArrayList<>(); |
| for (final Iterator<FoldingAnnotation> iter= (Iterator) model.getAnnotationIterator(); iter.hasNext(); ) { |
| final FoldingAnnotation ann= iter.next(); |
| final Position position= model.getPosition(ann); |
| final FoldingAnnotation newAnn= ctx.table.remove(position); |
| if (newAnn != null) { |
| if (!ann.update(newAnn)) { |
| del.add(ann); |
| ctx.table.put(newAnn.getPosition(), newAnn); |
| } |
| } |
| else { |
| del.add(ann); |
| } |
| } |
| deletions= del.toArray(new FoldingAnnotation[del.size()]); |
| if (ctx.document.getModificationStamp() != ctx.ast.getStamp().getSourceStamp() |
| || input != this.input) { |
| return; |
| } |
| } |
| final LinkedHashMap<FoldingAnnotation, Position> additions= new LinkedHashMap<>(); |
| for (final Iterator<Entry<Position, FoldingAnnotation>> iter= ctx.table.entrySet().iterator(); iter.hasNext(); ) { |
| final Entry<Position, FoldingAnnotation> next= iter.next(); |
| additions.put(next.getValue(), next.getKey()); |
| } |
| input.annotationModel.modifyAnnotations(deletions, additions, null); |
| input.updateStamp= ctx.ast.getStamp(); |
| } |
| } |
| |
| |
| //---- Persistence ---- |
| |
| private static class EncodedValue { |
| |
| private static final int I_OFFSET= 0x20; |
| private static final int I_SHIFT= 14; |
| private static final int I_RADIX= 1 << I_SHIFT; |
| private static final int I_MASK= I_RADIX - 1; |
| private static final int I_FOLLOW= 1 << (I_SHIFT + 1); |
| private static final int I_VALUE= I_FOLLOW - 1; |
| |
| |
| public static void writeInt(final StringBuilder sb, int value) { |
| while (true) { |
| final int c= (value & I_MASK) + I_OFFSET; |
| value >>>= I_SHIFT; |
| if (value == 0) { |
| sb.append((char) c); |
| return; |
| } |
| sb.append((char) (I_FOLLOW | c)); |
| } |
| } |
| |
| public static void writeLong(final StringBuilder sb, long value) { |
| while (true) { |
| final int c= (int) (value & I_MASK) + I_OFFSET; |
| value >>>= I_SHIFT; |
| if (value == 0) { |
| sb.append((char) c); |
| return; |
| } |
| sb.append((char) (I_FOLLOW | c)); |
| } |
| } |
| |
| |
| private final String value; |
| |
| private int offset; |
| |
| public EncodedValue(final String value) { |
| this.value= value; |
| } |
| |
| |
| public boolean hasNext() { |
| return this.offset < this.value.length(); |
| } |
| |
| public long readLong() { |
| long value= 0; |
| int shift= 0; |
| while (true) { |
| final int c= (this.value.charAt(this.offset++)); |
| if ((c & I_FOLLOW) == 0) { |
| value |= (long) (c - I_OFFSET) << shift; |
| return value; |
| } |
| value |= ((c & I_VALUE) - I_OFFSET) << shift; |
| shift+= I_SHIFT; |
| } |
| } |
| |
| public int readInt() { |
| int value= 0; |
| int shift= 0; |
| while (true) { |
| final int c= (this.value.charAt(this.offset++)); |
| if ((c & I_FOLLOW) == 0) { |
| value |= (c - I_OFFSET) << shift; |
| return value; |
| } |
| value |= ((c & I_VALUE) - I_OFFSET) << shift; |
| shift+= I_SHIFT; |
| } |
| } |
| |
| } |
| |
| // public static void main(String[] args) { |
| // StringBuilder sb= new StringBuilder(); |
| //// int start= 0; |
| //// int stop= 10000000; |
| // int start= Integer.MAX_VALUE - 1000000; |
| // int stop= Integer.MAX_VALUE; |
| // for (int i= start; i < stop; i++) { |
| // EncodedValue.writeInt(sb, i); |
| // } |
| // |
| // EncodedValue v= new EncodedValue(sb.toString()); |
| // for (int i= start; i < stop; i++) { |
| // int r= v.readInt(); |
| // if (i != r) { |
| // System.out.println("ERROR " + i + " " + r); |
| // } |
| // } |
| // } |
| |
| private static final int MAX_PERSISTENT_LENGTH= 2 * 1024; |
| private static final int CURRENT_VERSION= 1; |
| |
| private class SaveJob extends Job { |
| |
| private final IResource resource; |
| private final QualifiedName propertyName; |
| |
| public SaveJob(final IResource resource, final QualifiedName propertyName) { |
| super(NLS.bind("Save Folding State for ''{0}''", resource.toString())); //$NON-NLS-1$ |
| setSystem(true); |
| setUser(false); |
| setPriority(Job.LONG); |
| |
| this.resource= resource; |
| this.propertyName= propertyName; |
| } |
| |
| @Override |
| protected IStatus run(final IProgressMonitor monitor) { |
| try { |
| if (!this.resource.exists()) { |
| return Status.OK_STATUS; |
| } |
| String value= (String) this.resource.getSessionProperty(this.propertyName); |
| value= checkValue(value); |
| if (value != null) { |
| this.resource.setPersistentProperty(this.propertyName, value); |
| } |
| return Status.OK_STATUS; |
| } |
| catch (final CoreException e) { |
| return new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, |
| NLS.bind("An error occurred when saving the code folding state for {0}", this.resource.toString()), |
| e ); |
| } |
| } |
| |
| private String checkValue(final String value) { |
| if (value == null || value.isEmpty()) { |
| return null; |
| } |
| if (value.length() <= MAX_PERSISTENT_LENGTH) { |
| return value; |
| } |
| final EncodedValue encoded= new EncodedValue(value); |
| final StringBuilder sb= new StringBuilder(MAX_PERSISTENT_LENGTH); |
| |
| if (encoded.readInt() != CURRENT_VERSION) { |
| return null; |
| } |
| EncodedValue.writeInt(sb, CURRENT_VERSION); |
| EncodedValue.writeLong(sb, encoded.readLong()); |
| |
| int collapedBegin= -1; |
| int collapedEnd= -1; |
| while (encoded.hasNext()) { |
| final int l= sb.length(); |
| |
| final int offset= encoded.readInt(); |
| final int length= encoded.readInt(); |
| final int state= encoded.readInt(); |
| |
| if (offset >= collapedBegin && offset + length <= collapedEnd) { |
| continue; |
| } |
| |
| EncodedValue.writeInt(sb, offset); |
| EncodedValue.writeInt(sb, length); |
| EncodedValue.writeInt(sb, state); |
| |
| if (sb.length() > MAX_PERSISTENT_LENGTH) { |
| return sb.substring(0, l); |
| } |
| |
| if (state == FoldingAnnotation.COLLAPSED_STATE) { |
| collapedBegin= offset; |
| collapedEnd= offset + length; |
| } |
| } |
| return sb.toString(); |
| } |
| |
| } |
| |
| private void saveState(final Input input) { |
| final SourceEditor1 editor= this.editor; |
| if (editor == null || !input.isInitilized || !input.unit.isSynchronized() |
| || !(input.unit instanceof IWorkspaceSourceUnit) ) { |
| return; |
| } |
| final IResource resource= ((IWorkspaceSourceUnit) input.unit).getResource(); |
| if (resource == null || !resource.exists()) { |
| return; |
| } |
| |
| final String value; |
| { final StringBuilder sb= new StringBuilder(1024); |
| |
| EncodedValue.writeInt(sb, CURRENT_VERSION); |
| EncodedValue.writeLong(sb, resource.getModificationStamp()); |
| |
| final ProjectionAnnotationModel model= input.annotationModel; |
| for (final Iterator<FoldingAnnotation> iter= (Iterator) model.getAnnotationIterator(); iter.hasNext(); ) { |
| final FoldingAnnotation ann= iter.next(); |
| final int state= ann.getState(); |
| if (state != ann.getInitialState()) { |
| final Position position= model.getPosition(ann); |
| if (position != null) { |
| EncodedValue.writeInt(sb, position.getOffset()); |
| EncodedValue.writeInt(sb, position.getLength()); |
| EncodedValue.writeInt(sb, state); |
| } |
| } |
| } |
| value= sb.toString(); |
| } |
| try { |
| final QualifiedName propertyName= input.savePropertyName; |
| |
| resource.setSessionProperty(propertyName, value); |
| |
| new SaveJob(resource, propertyName).schedule(); |
| } |
| catch (final CoreException e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, |
| NLS.bind("An error occurred when saving the code folding state for {0}", resource.toString()), |
| e )); |
| } |
| } |
| |
| private void loadState(final Input input, final SortedMap<Position, FoldingAnnotation> table) { |
| final SourceEditor1 editor= this.editor; |
| if (editor == null || !input.isInitilized || !input.unit.isSynchronized() |
| || !(input.unit instanceof IWorkspaceSourceUnit) ) { |
| return; |
| } |
| final IResource resource= ((IWorkspaceSourceUnit) input.unit).getResource(); |
| if (resource == null || !resource.exists()) { |
| return; |
| } |
| EncodedValue encoded; |
| try { |
| final QualifiedName propertyName= input.savePropertyName; |
| |
| String s= (String) resource.getSessionProperty(propertyName); |
| if (s == null) { |
| s= resource.getPersistentProperty(propertyName); |
| if (s == null) { |
| resource.setSessionProperty(propertyName, ""); //$NON-NLS-1$ |
| return; |
| } |
| } |
| if (s.isEmpty()) { |
| return; |
| } |
| encoded= new EncodedValue(s); |
| } |
| catch (final CoreException e) { |
| return; |
| } |
| if (encoded.readInt() != CURRENT_VERSION) { |
| return; |
| } |
| if (encoded.readLong() != resource.getModificationStamp()) { |
| return; |
| } |
| { final Position position= new Position(0, 0); |
| while (encoded.hasNext()) { |
| position.offset= encoded.readInt(); |
| position.length= encoded.readInt(); |
| final FoldingAnnotation ann= table.get(position); |
| if (ann != null) { |
| ann.applyState(encoded.readInt()); |
| } |
| } |
| } |
| } |
| |
| } |