JavaHelperView.java

/*******************************************************************************
 * Copyright (c) 2020 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.ui.helper.views;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.e4.core.services.log.Logger;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJarEntryResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.WildcardType;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.core.SourceType;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.refactoring.RenameSupport;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ContributionItemFactory;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.progress.IProgressService;
import org.sandbox.jdt.internal.corext.util.NamingUtils;
import org.sandbox.jdt.internal.corext.util.TypeWideningAnalyzer.TypeWideningResult;
import org.sandbox.jdt.ui.helper.views.colum.AbstractColumn;
import org.sandbox.jdt.ui.helper.views.colum.ConflictHighlightingLabelProvider;
import org.sandbox.jdt.ui.helper.views.colum.DeclaringMethodColumn;
import org.sandbox.jdt.ui.helper.views.colum.DeprecatedColumn;
import org.sandbox.jdt.ui.helper.views.colum.NameColumn;
import org.sandbox.jdt.ui.helper.views.colum.PackageColumn;
import org.sandbox.jdt.ui.helper.views.colum.QualifiednameColumn;
import org.sandbox.jdt.ui.helper.views.colum.WidestTypeColumn;

public class JavaHelperView extends ViewPart implements IShowInSource, IShowInTarget {

	Logger logger= PlatformUI.getWorkbench().getService(Logger.class);
	TableViewer variableTableViewer;
	private Table variableTable;
	private Action refreshAction;
	private Action propertiesAction;

	private Action linkWithSelectionAction;
	
	private Action filterConflictsAction;
	
	private Action renameVariableAction;
	
	/** When true, the view automatically updates when selections change in other views */
	private boolean linkWithSelectionEnabled = true;
	
	/** When true, only shows variables with naming conflicts (same name, different type) */
	private boolean filterConflictsEnabled = false;
	
	/** Filter for showing only naming conflicts */
	private NamingConflictFilter namingConflictFilter = new NamingConflictFilter();

	private IPartListener2 editorPartListener;

	private ISelectionListener workbenchSelectionListener;

	private IJavaElement currentJavaElementInput = null;

	/** Cache for type widening analysis results, shared between content provider and column */
	private final TypeWideningCache typeWideningCache = new TypeWideningCache();

	@Override
	public void createPartControl(Composite parent) {
		GridLayout layout = new GridLayout(1, false);
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 0;
		parent.setLayout(layout);

		// Create a composite to hold the table with TableColumnLayout
		Composite tableComposite = new Composite(parent, SWT.NONE);
		tableComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		TableColumnLayout tableColumnLayout = new TableColumnLayout();
		tableComposite.setLayout(tableColumnLayout);

		variableTableViewer= new TableViewer(tableComposite, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
		variableTableViewer.setColumnProperties(new String[] {});
		variableTableViewer.setUseHashlookup(true);
		variableTable= variableTableViewer.getTable();
		variableTable.setHeaderVisible(true);
		variableTable.setHeaderBackground(parent.getDisplay().getSystemColor(SWT.COLOR_TITLE_BACKGROUND));
		variableTable.setLinesVisible(true);
		variableTableViewer.setContentProvider(createContentProvider());
		// Enable tooltip support for the widest type column
		org.eclipse.jface.viewers.ColumnViewerToolTipSupport.enableFor(variableTableViewer);
		// This will create the columns for the table with proper weights
		AbstractColumn.addColumn(variableTableViewer, new NameColumn(), tableColumnLayout);
		AbstractColumn.addColumn(variableTableViewer, new QualifiednameColumn(), tableColumnLayout);
		AbstractColumn.addColumn(variableTableViewer, new WidestTypeColumn(typeWideningCache), tableColumnLayout);
		AbstractColumn.addColumn(variableTableViewer, new PackageColumn(), tableColumnLayout);
		AbstractColumn.addColumn(variableTableViewer, new DeprecatedColumn(), tableColumnLayout);
		AbstractColumn.addColumn(variableTableViewer, new DeclaringMethodColumn(), tableColumnLayout);

		variableTableViewer.setComparator(AbstractColumn.getComparator());
		makeActions();
		hookContextMenu();
		hookDoubleClickAction();
		hookConflictHighlighting();
		getSite().setSelectionProvider(new JHViewSelectionProvider(variableTableViewer));
		contributeToActionBars();
		
		// Add part listener to track editor changes
		addEditorPartListener();
		// Add selection listener to track Package Explorer selections
		addWorkbenchSelectionListener();
	}

	private JHViewContentProvider createContentProvider() {
		JHViewContentProvider contentProvider = new JHViewContentProvider();
		contentProvider.setTypeWideningCache(typeWideningCache);
		return contentProvider;
	}

	private void contributeToActionBars() {
		IActionBars bars= getViewSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
		bars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), refreshAction);
		bars.setGlobalActionHandler(ActionFactory.PROPERTIES.getId(), propertiesAction);
	}

	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(linkWithSelectionAction);
		manager.add(filterConflictsAction);
		manager.add(new Separator());
		manager.add(refreshAction);
	}

	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(linkWithSelectionAction);
		manager.add(filterConflictsAction);
		manager.add(new Separator());
		manager.add(refreshAction);
	}

	private void makeActions() {
		// Toggle action for linking with selection
		linkWithSelectionAction = new Action("Link with Selection", Action.AS_CHECK_BOX) { //$NON-NLS-1$
			@Override
			public void run() {
				linkWithSelectionEnabled = isChecked();
			}
		};
		linkWithSelectionAction.setToolTipText("Link with Selection - when enabled, the view automatically updates based on the current selection"); //$NON-NLS-1$
		linkWithSelectionAction.setImageDescriptor(JHPluginImages.IMG_SET_FOCUS);
		linkWithSelectionAction.setChecked(linkWithSelectionEnabled);
		
		// Toggle action for filtering naming conflicts
		filterConflictsAction = new Action("Filter Naming Conflicts", Action.AS_CHECK_BOX) { //$NON-NLS-1$
			@Override
			public void run() {
				filterConflictsEnabled = isChecked();
				applyConflictFilter();
			}
		};
		filterConflictsAction.setToolTipText("Filter Naming Conflicts - when enabled, only shows variables with the same name but different types"); //$NON-NLS-1$
		filterConflictsAction.setImageDescriptor(JHPluginImages.IMG_FILTER_CONFLICTS);
		filterConflictsAction.setChecked(filterConflictsEnabled);
		
		// Action to rename a variable with a suggested name based on type
		renameVariableAction = new Action("Rename Variable with Type Suffix...") { //$NON-NLS-1$
			@Override
			public void run() {
				renameSelectedVariable();
			}
		};
		renameVariableAction.setToolTipText("Rename the selected variable with a suggested name based on its type"); //$NON-NLS-1$
		
		propertiesAction= new Action("&Properties", JHPluginImages.IMG_PROPERTIES) { //$NON-NLS-1$
			@Override
			public void run() {
				String viewId= IPageLayout.ID_PROP_SHEET;
				IWorkbenchPage page= getViewSite().getPage();
				IViewPart view;
				try {
					view= page.showView(viewId);
					page.activate(JavaHelperView.this);
					page.bringToTop(view);
				} catch (PartInitException e) {
					logger.error(e, "could not find Properties view"); //$NON-NLS-1$
				}
			}
		};
		propertiesAction.setActionDefinitionId(IWorkbenchCommandConstants.FILE_PROPERTIES);
		
		refreshAction= new Action("Re&fresh", JHPluginImages.IMG_REFRESH) { //$NON-NLS-1$
			@Override
			public void run() {
				BusyIndicator.showWhile(getSite().getShell().getDisplay(), () -> variableTableViewer.refresh());
			}
		};
		refreshAction.setToolTipText("Refresh"); //$NON-NLS-1$
		refreshAction.setActionDefinitionId("org.eclipse.ui.file.refresh"); //$NON-NLS-1$
	}
	
	/**
	 * Applies or removes the naming conflict filter based on the filterConflictsEnabled flag.
	 * When applying, analyzes all elements first to identify conflicts.
	 */
	private void applyConflictFilter() {
		if (filterConflictsEnabled) {
			// Analyze current elements to find conflicts
			JHViewContentProvider contentProvider = (JHViewContentProvider) variableTableViewer.getContentProvider();
			Object[] elements = contentProvider.getElements(variableTableViewer.getInput());
			namingConflictFilter.analyzeElements(elements);
			
			// Add filter
			variableTableViewer.addFilter(namingConflictFilter);
		} else {
			// Remove filter
			variableTableViewer.removeFilter(namingConflictFilter);
		}
	}
	
	/**
	 * Renames the currently selected variable with a suggested name based on its type.
	 * Shows a dialog to allow the user to modify the suggested name before applying.
	 */
	private void renameSelectedVariable() {
		IStructuredSelection selection = variableTableViewer.getStructuredSelection();
		Object element = selection.getFirstElement();
		
		if (!(element instanceof IVariableBinding variableBinding)) {
			return;
		}
		
		IJavaElement javaElement = variableBinding.getJavaElement();
		if (javaElement == null) {
			return;
		}
		
		String suggestedName = VariableNameSuggester.suggestName(variableBinding);
		String currentName = variableBinding.getName();
		
		// Show input dialog with suggested name pre-filled
		InputDialog dialog = new InputDialog(
				getSite().getShell(),
				"Rename Variable", //$NON-NLS-1$
				"Enter new name for variable '" + currentName + "':", //$NON-NLS-1$ //$NON-NLS-2$
				suggestedName,
				newText -> {
					if (newText == null || newText.trim().isEmpty()) {
						return "Name cannot be empty"; //$NON-NLS-1$
					}
					if (!NamingUtils.isValidJavaIdentifier(newText)) {
						return "Invalid Java identifier"; //$NON-NLS-1$
					}
					return null;
				});
		
		if (dialog.open() == Window.OK) {
			String newName = dialog.getValue();
			if (!newName.equals(currentName)) {
				performRename(javaElement, newName);
			}
		}
	}
	
	/**
	 * Performs the rename refactoring using JDT's RenameSupport.
	 * Opens the refactoring wizard dialog with preview option.
	 * 
	 * @param javaElement the Java element to rename
	 * @param newName the new name
	 */
	private void performRename(IJavaElement javaElement, String newName) {
		try {
			RenameSupport renameSupport = null;
			
			if (javaElement instanceof IField field) {
				renameSupport = RenameSupport.create(field, newName, 
						RenameSupport.UPDATE_REFERENCES | RenameSupport.UPDATE_GETTER_METHOD | RenameSupport.UPDATE_SETTER_METHOD);
			} else if (javaElement instanceof ILocalVariable localVariable) {
				renameSupport = RenameSupport.create(localVariable, newName, RenameSupport.UPDATE_REFERENCES);
			}
			
			if (renameSupport != null) {
				// Check if refactoring is valid
				if (renameSupport.preCheck().isOK()) {
					// Open the refactoring wizard dialog with preview
					renameSupport.openDialog(getSite().getShell(), true);
				}
			}
		} catch (CoreException e) {
			logger.error(e, "Error performing rename refactoring"); //$NON-NLS-1$
		}
	}

	/**
	 * Changes the declared type of a variable to the given target type.
	 * Uses AST rewriting to update the type declaration and manages imports.
	 * After a successful change, the view is re-analyzed to reflect the update.
	 *
	 * @param variableBinding the variable whose type should be changed
	 * @param targetType      the new type to use (must be a supertype of the current type)
	 */
	private void performChangeType(IVariableBinding variableBinding, ITypeBinding targetType) {
		IJavaElement javaElement = variableBinding.getJavaElement();
		if (javaElement == null) {
			return;
		}
		ICompilationUnit cu = (ICompilationUnit) javaElement.getAncestor(IJavaElement.COMPILATION_UNIT);
		if (cu == null) {
			return;
		}

		try {
			// Parse AST with bindings
			ASTParser parser = ASTParser.newParser(AST.JLS_Latest);
			parser.setSource(cu);
			parser.setResolveBindings(true);
			parser.setBindingsRecovery(true);
			CompilationUnit ast = (CompilationUnit) parser.createAST(null);

			// Find the variable declaration and replace the type
			ASTRewrite rewrite = ASTRewrite.create(ast.getAST());
			ImportRewrite importRewrite = ImportRewrite.create(ast, true);
			String targetKey = variableBinding.getKey();
			boolean[] found = {false};

			ast.accept(new ASTVisitor() {
				@Override
				public boolean visit(VariableDeclarationStatement node) {
					for (Object fragObj : node.fragments()) {
						VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
						IVariableBinding b = frag.resolveBinding();
						if (b != null && targetKey.equals(b.getKey())) {
							Type newType = createTypeNode(node.getAST(), targetType, importRewrite);
							rewrite.replace(node.getType(), newType, null);
							found[0] = true;
						}
					}
					return !found[0];
				}
			});

			if (!found[0]) {
				return;
			}

			// Step 1: Apply the AST rewrite (type replacement) to the document
			String source = cu.getSource();
			Document document = new Document(source);
			Map<String, String> options = cu.getJavaProject() != null
					? cu.getJavaProject().getOptions(true)
					: JavaCore.getOptions();
			TextEdit astEdits = rewrite.rewriteAST(document, options);
			astEdits.apply(document);

			// Step 2: Apply import edits from the original ImportRewrite
			TextEdit importEdits = importRewrite.rewriteImports(null);
			importEdits.apply(document);

			// Step 3: Save changes
			IBuffer buffer = cu.getBuffer();
			buffer.setContents(document.get());
			cu.save(null, false);

			// Reconcile the compilation unit
			reconcile(cu);

			// Refresh the view to reflect the updated types while preserving the
			// existing input scope (package/project/multi-selection).
			getSite().getShell().getDisplay().asyncExec(() -> {
				typeWideningCache.clear();
				variableTableViewer.refresh();
			});

		} catch (CoreException | BadLocationException e) {
			logger.error(e, "Error performing change type refactoring"); //$NON-NLS-1$
		}
	}

	/**
	 * Creates an AST {@link Type} node for the given type binding, registering the
	 * required import via {@code importRewrite}. Handles parameterized types.
	 *
	 * @param ast           the AST to use for node creation
	 * @param typeBinding   the type binding to represent
	 * @param importRewrite the import rewrite to record needed imports
	 * @return the AST type node
	 */
	@SuppressWarnings("unchecked")
	private Type createTypeNode(AST ast, ITypeBinding typeBinding, ImportRewrite importRewrite) {
		ITypeBinding erasure = typeBinding.getErasure();
		String simpleName = importRewrite.addImport(erasure.getQualifiedName());
		SimpleType baseType = ast.newSimpleType(ast.newName(simpleName));

		if (!typeBinding.isParameterizedType()) {
			return baseType;
		}

		ParameterizedType paramType = ast.newParameterizedType(baseType);
		for (ITypeBinding typeArg : typeBinding.getTypeArguments()) {
			paramType.typeArguments().add(createTypeArgNode(ast, typeArg, importRewrite));
		}
		return paramType;
	}

	/**
	 * Creates an AST type node for a type argument (handles wildcards and type variables).
	 */
	private Type createTypeArgNode(AST ast, ITypeBinding typeArg, ImportRewrite importRewrite) {
		if (typeArg.isWildcardType()) {
			WildcardType wc = ast.newWildcardType();
			if (typeArg.getBound() != null) {
				wc.setBound(createTypeNode(ast, typeArg.getBound(), importRewrite), typeArg.isUpperbound());
			}
			return wc;
		}
		if (typeArg.isTypeVariable()) {
			return ast.newSimpleType(ast.newName(typeArg.getName()));
		}
		// For other cases (e.g., parameterized type args), use the erasure
		return createTypeNode(ast, typeArg.isParameterizedType() ? typeArg : typeArg.getErasure(), importRewrite);
	}

	/* see JavaModelUtil.reconcile((ICompilationUnit) input) */
	static void reconcile(ICompilationUnit unit) throws JavaModelException {
		synchronized (unit) {
			unit.reconcile(ICompilationUnit.NO_AST, false /* don't force problem detection */,
					null /* use primary owner */, null /* no progress monitor */);
		}
	}

	private void hookContextMenu() {
		MenuManager menuMgr= new MenuManager("#PopupMenu"); //$NON-NLS-1$
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(this::fillContextMenu);
		Menu menu= menuMgr.createContextMenu(variableTable);
		variableTable.setMenu(menu);
		getSite().registerContextMenu(menuMgr, variableTableViewer);
		
		// Add mouse listener to select the row under cursor on right-click
		variableTable.addListener(SWT.MenuDetect, event -> {
			org.eclipse.swt.graphics.Point pt = variableTable.getDisplay().map(null, variableTable, event.x, event.y);
			org.eclipse.swt.widgets.TableItem item = variableTable.getItem(pt);
			if (item != null) {
				Object data = item.getData();
				if (data != null) {
					variableTableViewer.setSelection(new StructuredSelection(data), true);
				}
			}
		});
	}

	/**
	 * Adds a double-click listener to the table viewer that navigates to the
	 * declaration of the selected variable binding.
	 */
	private void hookDoubleClickAction() {
		variableTableViewer.addDoubleClickListener(event -> {
			IStructuredSelection selection = (IStructuredSelection) event.getSelection();
			Object element = selection.getFirstElement();
			if (element instanceof IVariableBinding variableBinding) {
				openVariableDeclaration(variableBinding);
			}
		});
	}

	/**
	 * Adds custom painting for conflict highlighting using SWT's EraseItem event.
	 * This paints a light red background for rows where the variable name has type conflicts.
	 */
	private void hookConflictHighlighting() {
		final org.eclipse.swt.graphics.Color conflictColor = new org.eclipse.swt.graphics.Color(
				variableTable.getDisplay(), 255, 200, 200);
		
		variableTable.addListener(SWT.EraseItem, event -> {
			// Only handle background painting
			if ((event.detail & SWT.BACKGROUND) == 0) {
				return;
			}
			
			org.eclipse.swt.widgets.TableItem item = (org.eclipse.swt.widgets.TableItem) event.item;
			Object data = item.getData();
			
			if (data instanceof IVariableBinding variableBinding) {
				Set<String> conflicts = ConflictHighlightingLabelProvider.getConflictingNames();
				if (conflicts != null && conflicts.contains(variableBinding.getName())) {
					// Paint conflict background
					org.eclipse.swt.graphics.GC gc = event.gc;
					org.eclipse.swt.graphics.Color oldBackground = gc.getBackground();
					gc.setBackground(conflictColor);
					gc.fillRectangle(event.x, event.y, event.width, event.height);
					gc.setBackground(oldBackground);
					// Mark that we handled the background
					event.detail &= ~SWT.BACKGROUND;
				}
			}
		});
		
		// Dispose color when table is disposed
		variableTable.addDisposeListener(e -> conflictColor.dispose());
	}

	/**
	 * Opens the editor and navigates to the declaration of the given variable binding.
	 * Automatically disables linking with selection to prevent the table from refreshing
	 * when the editor is opened.
	 * 
	 * @param variableBinding the variable binding whose declaration to open
	 */
	private void openVariableDeclaration(IVariableBinding variableBinding) {
		IJavaElement javaElement = variableBinding.getJavaElement();
		if (javaElement != null) {
			// Disable linking before opening the editor to prevent table refresh
			disableLinkingWithSelection();
			try {
				JavaUI.openInEditor(javaElement, true, true);
			} catch (PartInitException | JavaModelException e) {
				logger.error(e, "Could not open editor for variable: " + variableBinding.getName()); //$NON-NLS-1$
			}
		}
	}
	
	/**
	 * Disables linking with selection and updates the toggle button state.
	 * This is called when navigating from the table to prevent the view from
	 * automatically refreshing and losing the current content.
	 */
	private void disableLinkingWithSelection() {
		linkWithSelectionEnabled = false;
		if (linkWithSelectionAction != null) {
			linkWithSelectionAction.setChecked(false);
		}
	}

	void fillContextMenu(IMenuManager manager) {
		manager.add(refreshAction);
		manager.add(new Separator());

		// Add rename action when a variable is selected
		IStructuredSelection selection = variableTableViewer.getStructuredSelection();
		if (!selection.isEmpty()) {
			Object selectedElement = selection.getFirstElement();
			if (selectedElement instanceof IVariableBinding variableBinding) {
				IJavaElement javaElement = variableBinding.getJavaElement();
				// Enable rename only for fields and local variables
				boolean canRename = javaElement instanceof IField || javaElement instanceof ILocalVariable;
				renameVariableAction.setEnabled(canRename);
				manager.add(renameVariableAction);
				manager.add(new Separator());

				// Add "Change Variable Type" submenu when widest type is available
				TypeWideningResult result = typeWideningCache.getResult(variableBinding.getKey());
				if (result != null && result.canWiden()) {
					MenuManager changeTypeMenu = new MenuManager("Change Variable Type"); //$NON-NLS-1$
					List<ITypeBinding> intermediateTypes = result.getIntermediateTypes();
					for (ITypeBinding targetType : intermediateTypes) {
						String simpleTypeName = targetType.getErasure().getName();
						boolean isWidest = targetType.getErasure().getQualifiedName()
								.equals(result.getWidestType().getErasure().getQualifiedName());
						String label = simpleTypeName + (isWidest ? " (widest)" : ""); //$NON-NLS-1$ //$NON-NLS-2$
						changeTypeMenu.add(new Action(label) {
							@Override
							public void run() {
								performChangeType(variableBinding, targetType);
							}
						});
					}
					if (!intermediateTypes.isEmpty()) {
						manager.add(changeTypeMenu);
					}
				}
			}

			MenuManager showInSubMenu= new MenuManager(getShowInMenuLabel());
			IWorkbenchWindow workbenchWindow= getSite().getWorkbenchWindow();
			showInSubMenu.add(ContributionItemFactory.VIEWS_SHOW_IN.create(workbenchWindow));
			manager.add(showInSubMenu);
		}
		manager.add(new Separator());
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
		manager.add(new Separator());
		manager.add(propertiesAction);
	}

	private String getShowInMenuLabel() {
		String keyBinding= null;

		IBindingService bindingService= PlatformUI.getWorkbench().getAdapter(IBindingService.class);
		if (bindingService != null) {
			keyBinding= bindingService
					.getBestActiveBindingFormattedFor(IWorkbenchCommandConstants.NAVIGATE_SHOW_IN_QUICK_MENU);
		}

		if (keyBinding == null) {
			keyBinding= ""; //$NON-NLS-1$
		}

		return "Sho&w In" + '\t' + keyBinding; //$NON-NLS-1$
	}

	/**
	 * Passing the focus request to the viewer's control.
	 */
	@Override
	public void setFocus() {
		variableTableViewer.getControl().setFocus();
	}

	@Override
	public boolean show(ShowInContext context) {
		ISelection selection= context.getSelection();
		if (selection instanceof IStructuredSelection structuredSelection) {
			if (structuredSelection.size() >= 1) {
				List<Object> input= new ArrayList<>();
				for (Object item : structuredSelection) {
					if (item instanceof IJavaElement || item instanceof IResource
							|| item instanceof IJarEntryResource) {
						input.add(item);
					}
				}
				if (!input.isEmpty()) {
					setInput(input);
					return true;
				}
			}
		}

		Object input= context.getInput();
		if (input instanceof IEditorInput) {
			SourceType elementOfInput= (SourceType) getElementOfInput((IEditorInput) context.getInput());
			if (elementOfInput != null) {
				//				setSingleInput(elementOfInput);
				return true;
			}
		}

		return false;
	}

	void setSingleInput(IResource iResource) {
		setInput(Collections.singletonList(iResource));
	}

	void setSingleInput(IJavaElement javaElement) {
		setInput(Collections.singletonList(javaElement));
	}

	Object getElementOfInput(IEditorInput input) {
		Object adapted= input.getAdapter(IClassFile.class);
		if (adapted != null) {
			return adapted;
		}

		if (input instanceof IFileEditorInput) {
			IFile file= ((IFileEditorInput) input).getFile();
			IJavaElement javaElement= JavaCore.create(file);
			if (javaElement != null) {
				return javaElement;
			}
			return file;
		}
		if (input instanceof IStorageEditorInput) {
			try {
				return ((IStorageEditorInput) input).getStorage();
			} catch (CoreException e) {
			}
		}
		return null;
	}

	@Override
	public ShowInContext getShowInContext() {
		IWorkbenchPartSite site= getSite();
		if (site == null) {
			return null;
		}
		ISelectionProvider selectionProvider= site.getSelectionProvider();
		if (selectionProvider == null) {
			return null;
		}
		return new ShowInContext(null, selectionProvider.getSelection());
	}

	void setInput(List<?> javaElementsOrResources) {
		//		fInput = new JERoot(javaElementsOrResources);
		//		variableTableViewer.setInput(fInput);
		variableTableViewer.setInput(javaElementsOrResources);
		
		// Clear the editor input cache if the input is not a single IJavaElement
		// This ensures the view will refresh when switching back to an editor after
		// actions like reset() or setInputFromEditorLocationAction that set IResource inputs
		if (javaElementsOrResources != null && javaElementsOrResources.size() == 1) {
			Object input = javaElementsOrResources.get(0);
			if (!(input instanceof IJavaElement)) {
				currentJavaElementInput = null;
			}
		} else {
			currentJavaElementInput = null;
		}
		
		JHViewContentProvider contentProvider= (JHViewContentProvider) variableTableViewer.getContentProvider();
		Object[] elements= contentProvider.getElements(javaElementsOrResources);
		
		// Analyze elements for naming conflicts and update the highlighting
		namingConflictFilter.analyzeElements(elements);
		ConflictHighlightingLabelProvider.setConflictingNames(namingConflictFilter.getConflictingNames());
		
		// Refresh the table to apply the conflict highlighting
		variableTableViewer.refresh();
		
		if (elements.length > 0) {
			variableTableViewer.setSelection(new StructuredSelection(elements[0]));
		}
	}

	@Override
	public <T> T getAdapter(Class<T> adapter) {
		// if (adapter == IPropertySheetPage.class) {
		// return (T) getPropertySheetPage();
		// }
		return super.getAdapter(adapter);
	}

	/**
	 * Adds a part listener to track when editors are activated and automatically
	 * refresh the view with the active editor's content.
	 */
	private void addEditorPartListener() {
		// Check if listener is already registered to prevent duplicates
		if (editorPartListener != null) {
			return;
		}
		
		editorPartListener = new IPartListener2() {
			@Override
			public void partActivated(IWorkbenchPartReference partRef) {
				if (linkWithSelectionEnabled && partRef.getPart(false) instanceof IEditorPart) {
					// Update the view when an editor is activated
					// Ensure UI updates happen on the UI thread
					getSite().getShell().getDisplay().asyncExec(() -> updateViewFromActiveEditor());
				}
			}

			@Override
			public void partBroughtToTop(IWorkbenchPartReference partRef) {
				// Not needed
			}

			@Override
			public void partClosed(IWorkbenchPartReference partRef) {
				// Not needed
			}

			@Override
			public void partDeactivated(IWorkbenchPartReference partRef) {
				// Not needed
			}

			@Override
			public void partOpened(IWorkbenchPartReference partRef) {
				if (linkWithSelectionEnabled && partRef.getPart(false) instanceof IEditorPart) {
					// Update the view when an editor is opened
					// Ensure UI updates happen on the UI thread
					getSite().getShell().getDisplay().asyncExec(() -> updateViewFromActiveEditor());
				}
			}

			@Override
			public void partHidden(IWorkbenchPartReference partRef) {
				// Not needed
			}

			@Override
			public void partVisible(IWorkbenchPartReference partRef) {
				// Not needed
			}

			@Override
			public void partInputChanged(IWorkbenchPartReference partRef) {
				if (linkWithSelectionEnabled && partRef.getPart(false) instanceof IEditorPart) {
					// Update the view when editor input changes
					// Ensure UI updates happen on the UI thread
					getSite().getShell().getDisplay().asyncExec(() -> updateViewFromActiveEditor());
				}
			}
		};

		getSite().getPage().addPartListener(editorPartListener);
	}

	/**
	 * Updates the view content based on the currently active editor.
	 */
	private void updateViewFromActiveEditor() {
		IEditorPart editor = getSite().getPage().getActiveEditor();
		if (editor == null) {
			return;
		}
		
		IEditorInput input = editor.getEditorInput();
		if (input == null) {
			return;
		}

		IJavaElement javaElement = input.getAdapter(IJavaElement.class);
		if (javaElement != null && !javaElement.equals(currentJavaElementInput)) {
			currentJavaElementInput = javaElement;
			setSingleInput(javaElement);
		}
	}

	/**
	 * Adds a selection listener to track selections in views like Package Explorer.
	 * When a package, source folder, or project is selected, the view will update
	 * to show variables from all contained compilation units.
	 */
	private void addWorkbenchSelectionListener() {
		if (workbenchSelectionListener != null) {
			return;
		}

		workbenchSelectionListener = (part, selection) -> {
			// Skip if link with selection is disabled
			if (!linkWithSelectionEnabled) {
				return;
			}
			
			// Ignore selections from this view itself
			if (part == JavaHelperView.this) {
				return;
			}

			if (selection instanceof IStructuredSelection structuredSelection) {
				Object firstElement = structuredSelection.getFirstElement();
				if (firstElement instanceof IPackageFragment 
						|| firstElement instanceof IPackageFragmentRoot
						|| firstElement instanceof IJavaProject) {
					IJavaElement javaElement = (IJavaElement) firstElement;
					if (!javaElement.equals(currentJavaElementInput)) {
						currentJavaElementInput = javaElement;
						setInputWithProgress(javaElement);
					}
				} else if (firstElement instanceof ICompilationUnit compilationUnit) {
					if (!compilationUnit.equals(currentJavaElementInput)) {
						currentJavaElementInput = compilationUnit;
						setSingleInput(compilationUnit);
					}
				}
			}
		};

		getSite().getPage().addSelectionListener(workbenchSelectionListener);
	}

	/**
	 * Sets the input with a progress dialog for potentially long-running operations.
	 * This is used when processing packages, source folders, or projects that may
	 * contain many compilation units.
	 * 
	 * @param javaElement the Java element to process (package, source folder, or project)
	 */
	private void setInputWithProgress(IJavaElement javaElement) {
		IProgressService progressService = PlatformUI.getWorkbench().getProgressService();
		
		try {
			progressService.busyCursorWhile(monitor -> {
				monitor.beginTask("Processing " + javaElement.getElementName() + "...", IProgressMonitor.UNKNOWN); //$NON-NLS-1$ //$NON-NLS-2$
				
				try {
					int totalUnits = countCompilationUnits(javaElement);
					if (totalUnits > 0) {
						monitor.beginTask("Processing " + totalUnits + " compilation units...", totalUnits); //$NON-NLS-1$ //$NON-NLS-2$
					}
					
					// Create a progress-aware content provider
					VariableBindingContentProviderWithProgress contentProvider = new VariableBindingContentProviderWithProgress(monitor);
					final Object[] elements = contentProvider.getElements(Collections.singletonList(javaElement));
					
					// Update UI on the display thread
					getSite().getShell().getDisplay().asyncExec(() -> {
						variableTableViewer.setInput(Collections.singletonList(javaElement));
						// Manually set the elements since we already computed them
						variableTableViewer.refresh();
						if (elements.length > 0) {
							variableTableViewer.setSelection(new StructuredSelection(elements[0]));
						}
					});
				} finally {
					monitor.done();
				}
			});
		} catch (InvocationTargetException | InterruptedException e) {
			logger.error(e, "Error processing Java element: " + javaElement.getElementName()); //$NON-NLS-1$
		}
	}

	/**
	 * Counts the number of compilation units in a Java element.
	 * 
	 * @param javaElement the Java element to count compilation units for
	 * @return the number of compilation units
	 */
	private int countCompilationUnits(IJavaElement javaElement) {
		try {
			if (javaElement instanceof ICompilationUnit) {
				return 1;
			} else if (javaElement instanceof IPackageFragment pf) {
				return pf.getCompilationUnits().length;
			} else if (javaElement instanceof IPackageFragmentRoot pfr) {
				int count = 0;
				for (IJavaElement child : pfr.getChildren()) {
					if (child instanceof IPackageFragment pf) {
						count += pf.getCompilationUnits().length;
					}
				}
				return count;
			} else if (javaElement instanceof IJavaProject project) {
				int count = 0;
				for (IPackageFragmentRoot root : project.getPackageFragmentRoots()) {
					if (root.getKind() == IPackageFragmentRoot.K_SOURCE) {
						for (IJavaElement child : root.getChildren()) {
							if (child instanceof IPackageFragment pf) {
								count += pf.getCompilationUnits().length;
							}
						}
					}
				}
				return count;
			}
		} catch (JavaModelException e) {
			// Ignore and return 0
		}
		return 0;
	}

	@Override
	public void dispose() {
		// Remove workbench selection listener when view is disposed
		if (workbenchSelectionListener != null) {
			try {
				IWorkbenchPage page = getSite().getPage();
				if (page != null) {
					page.removeSelectionListener(workbenchSelectionListener);
				}
			} catch (Exception e) {
				// Ignore errors during shutdown
			}
			workbenchSelectionListener = null;
		}
		// Remove editor part listener when view is disposed
		if (editorPartListener != null) {
			// Check if page is still available to prevent errors during workbench shutdown
			try {
				IWorkbenchPage page = getSite().getPage();
				if (page != null) {
					page.removePartListener(editorPartListener);
				}
			} catch (Exception e) {
				// Ignore errors during shutdown
			}
			editorPartListener = null;
		}
		super.dispose();
	}

}