/*******************************************************************************
 * Copyright (c) 2000, 2020 IBM Corporation and others.
 *
 * 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:
 * IBM Corporation - initial API and implementation
 * Willian Mitsuda <wmitsuda@gmail.com>
 *    - Fix for bug 196553 - [Dialogs] Support IColorProvider/IFontProvider in FilteredItemsSelectionDialog
 * Peter Friese <peter.friese@gentleware.com>
 *    - Fix for bug 208602 - [Dialogs] Open Type dialog needs accessible labels
 * Simon Muschel <smuschel@gmx.de> - bug 258493
 * Kris De Volder Copied and modified from org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog
 *                to create QuickSearchDialog
 *******************************************************************************/
package org.eclipse.text.quicksearch.internal.ui;

import static org.eclipse.jface.resource.JFaceResources.TEXT_FONT;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.ICoreRunnable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.LegacyActionTools;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.viewers.ILazyContentProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.osgi.util.NLS;
import org.eclipse.search.internal.ui.text.EditorOpener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.text.quicksearch.internal.core.LineItem;
import org.eclipse.text.quicksearch.internal.core.QuickTextQuery;
import org.eclipse.text.quicksearch.internal.core.QuickTextQuery.TextRange;
import org.eclipse.text.quicksearch.internal.core.QuickTextSearchRequestor;
import org.eclipse.text.quicksearch.internal.core.QuickTextSearcher;
import org.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatchers;
import org.eclipse.text.quicksearch.internal.util.DocumentFetcher;
import org.eclipse.ui.ActiveShellExpression;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.SelectionStatusDialog;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.IWorkbenchGraphicConstants;
import org.eclipse.ui.internal.WorkbenchImages;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.progress.UIJob;
import org.osgi.framework.FrameworkUtil;

/**
 * Shows a list of items to the user with a text entry field for a string
 * pattern used to filter the list of items.
 *
 * @since 3.3
 */
@SuppressWarnings({ "rawtypes", "restriction", "unchecked" })
public class QuickSearchDialog extends SelectionStatusDialog {

	private static final int OPEN_BUTTON_ID = IDialogConstants.OK_ID;
	private static final int REFRESH_BUTTON_ID = IDialogConstants.RETRY_ID;

	public static final Styler HIGHLIGHT_STYLE = org.eclipse.search.internal.ui.text.DecoratingFileSearchLabelProvider.HIGHLIGHT_STYLE;

	private UIJob refreshJob = UIJob.create(Messages.QuickSearchDialog_RefreshJob,
			(ICoreRunnable) m -> refreshWidgets());

	protected void openSelection() {
		try {
			LineItem item = (LineItem) this.getFirstResult();
			if (item!=null) {
				QuickTextQuery q = this.getQuery();
				TextRange range = q.findFirst(item.getText());
				EditorOpener opener = new EditorOpener();
				IWorkbenchPage page = window.getActivePage();
				if (page!=null) {
					opener.openAndSelect(page, item.getFile(), range.getOffset()+item.getOffset(),
						range.getLength(), true);
				}
			}
		} catch (PartInitException e) {
			QuickSearchActivator.log(e);
		}
	}

	/**
	 * Job that shows a simple busy indicator while a search is active.
	 * The job must be scheduled when a search starts/resumes.
	 */
	private UIJob progressJob =  new UIJob(Messages.QuickSearchDialog_RefreshJob) {
		int animate = 0; // number of dots to display.

		protected String dots(int animate) {
			char[] chars = new char[animate];
			for (int i = 0; i < chars.length; i++) {
				chars[i] = '.';
			}
			return new String(chars);
		}

		protected String currentFileInfo(IFile currentFile, int animate) {
			if (currentFile!=null) {
				String path = currentFile.getFullPath().toString();
				if (path.length()<=30) {
					return path;
				}
				return "..."+path.substring(path.length()-30); //$NON-NLS-1$
			}
			return dots(animate);
		}

		@Override
		public IStatus runInUIThread(IProgressMonitor mon) {
			if (!mon.isCanceled() && progressLabel!=null && !progressLabel.isDisposed()) {
				if (searcher==null || !searcher.isActive()) {
					progressLabel.setText(EMPTY_STRING);
				} else {
					progressLabel.setText(NLS.bind(Messages.QuickSearchDialog_searching, currentFileInfo(searcher.getCurrentFile(), animate)));
					animate = (animate+1)%4;
					this.schedule(333);
				}
			}
			return Status.OK_STATUS;
		}
	};

	public final StyledCellLabelProvider LINE_NUMBER_LABEL_PROVIDER = new StyledCellLabelProvider() {
		@Override
		public void update(ViewerCell cell) {
			LineItem item = (LineItem) cell.getElement();
			if (item!=null) {
				cell.setText(Integer.toString(item.getLineNumber()));
			} else {
				cell.setText("?"); //$NON-NLS-1$
			}
			cell.setImage(getBlankImage());
		};
	};

	private static final Color GREY = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GRAY);

	private final StyledCellLabelProvider LINE_TEXT_LABEL_PROVIDER = new StyledCellLabelProvider() {

		@Override
		public void update(ViewerCell cell) {
			LineItem item = (LineItem) cell.getElement();
			if (item!=null) {
				StyledString text = highlightMatches(item.getText());
				cell.setText(text.getString());
				cell.setStyleRanges(text.getStyleRanges());
			} else {
				cell.setText(EMPTY_STRING);
				cell.setStyleRanges(null);
			}
			cell.setImage(getBlankImage());
			super.update(cell);
		}
	};

	private Image blankImage;

	private Image getBlankImage() {
		if (blankImage==null) {
			blankImage = new Image(Display.getDefault(), 1, 1);
//			GC gc = new GC(blankImage);
//			gc.fillRectangle(0, 0, 16, 16);
//			gc.dispose();
		}
		return blankImage;
	}

	private final StyledCellLabelProvider LINE_FILE_LABEL_PROVIDER = new StyledCellLabelProvider() {

		@Override
		public void update(ViewerCell cell) {
			LineItem item = (LineItem) cell.getElement();
			if (item!=null) {
				IPath path = item.getFile().getFullPath();
				String name = path.lastSegment();
				String dir = path.removeLastSegments(1).toString();
				if (dir.startsWith("/")) { //$NON-NLS-1$
					dir = dir.substring(1);
				}
				cell.setText(name + " - " + dir); //$NON-NLS-1$
				StyleRange[] styleRanges = new StyleRange[] {
						new StyleRange(name.length(), dir.length()+3, GREY, null)
				};
				cell.setStyleRanges(styleRanges);
			} else {
				cell.setText(EMPTY_STRING);
				cell.setStyleRanges(null);
			}
			cell.setImage(getBlankImage());
			super.update(cell);
		}

//		public String getToolTipText(Object element) {
//			LineItem item = (LineItem) element;
//			if (item!=null) {
//				return ""+item.getFile().getFullPath();
//			}
//			return "";
//		};

//		public String getText(Object _item) {
//			if (_item!=null) {
//				LineItem item = (LineItem) _item;
//				return item.getFile().getName().toString();
//			}
//			return "?";
//		};
	};

	private static final String DIALOG_SETTINGS = QuickSearchDialog.class.getName()+".DIALOG_SETTINGS"; //$NON-NLS-1$

	private static final String DIALOG_BOUNDS_SETTINGS = "DialogBoundsSettings"; //$NON-NLS-1$

	private static final String DIALOG_HEIGHT = "DIALOG_HEIGHT"; //$NON-NLS-1$
	private static final String DIALOG_WIDTH = "DIALOG_WIDTH"; //$NON-NLS-1$
	private static final String DIALOG_COLUMNS = "COLUMN_WIDTHS"; //$NON-NLS-1$
	private static final String DIALOG_SASH_WEIGHTS = "SASH_WEIGHTS"; //$NON-NLS-1$

	private static final String DIALOG_LAST_QUERY = "LAST_QUERY"; //$NON-NLS-1$
	private static final String DIALOG_PATH_FILTER = "PATH_FILTER"; //$NON-NLS-1$
	private static final String CASE_SENSITIVE = "CASE_SENSITIVE"; //$NON-NLS-1$
	private static final boolean CASE_SENSITIVE_DEFAULT = true;

	private static final String KEEP_OPEN = "KEEP_OPEN"; //$NON-NLS-1$
	private static final boolean KEEP_OPEN_DEFAULT = false;

	/**
	 * Represents an empty selection in the pattern input field (used only for
	 * initial pattern).
	 */
	public static final int NONE = 0;

	/**
	 * Pattern input field selection where caret is at the beginning (used only
	 * for initial pattern).
	 */
	public static final int CARET_BEGINNING = 1;

	/**
	 * Represents a full selection in the pattern input field (used only for
	 * initial pattern).
	 */
	public static final int FULL_SELECTION = 2;

	private Text pattern;

	private TableViewer list;

	private MenuManager menuManager;

	private MenuManager contextMenuManager;

	private boolean multi;

	private ToolBar toolBar;

	private ToolItem toolItem;

	private Label progressLabel;

	private ContentProvider contentProvider;

	private String initialPatternText;

	private int selectionMode;

	private static final String EMPTY_STRING = ""; //$NON-NLS-1$

	private final int MAX_LINE_LEN;
	private final int MAX_RESULTS;

	private IHandlerActivation showViewHandler;

	private QuickTextSearcher searcher;

	private StyledText details;

	private DocumentFetcher documents;


	private ToggleCaseSensitiveAction toggleCaseSensitiveAction;
	private ToggleKeepOpenAction toggleKeepOpenAction;


	private QuickSearchContext context;


	private SashForm sashForm;

	private Label headerLabel;

	private IWorkbenchWindow window;
	private Text searchIn;
	private Label listLabel;

	/**
	 * Creates a new instance of the class.
	 *
	 * @param window.getShell()
	 *           shell to parent the dialog on
	 * @param multi
	 *           indicates whether dialog allows to select more than one
	 *           position in its list of items
	 */
	public QuickSearchDialog(IWorkbenchWindow window) {
		super(window.getShell());
		this.window = window;
		setShellStyle(SWT.CLOSE | SWT.MODELESS | SWT.BORDER | SWT.TITLE | SWT.RESIZE);
		setBlockOnOpen(false);
		this.setTitle(Messages.QuickSearchDialog_title);
		this.context = new QuickSearchContext(window);
		this.multi = false;
		contentProvider = new ContentProvider();
		selectionMode = NONE;
		MAX_LINE_LEN = QuickSearchActivator.getDefault().getPreferences().getMaxLineLen();
		MAX_RESULTS = QuickSearchActivator.getDefault().getPreferences().getMaxResults();
		progressJob.setSystem(true);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jface.window.Window#create()
	 */
	@Override
	public void create() {
		super.create();
		pattern.setFocus();
	}

	/**
	 * Restores dialog using persisted settings.
	 */
	protected void restoreDialog(IDialogSettings settings) {
		try {
			if (initialPatternText==null) {
				String lastSearch = settings.get(DIALOG_LAST_QUERY);
				if (lastSearch==null) {
					lastSearch = EMPTY_STRING;
				}
				pattern.setText(lastSearch);
				pattern.selectAll();
			}
			if (settings.get(DIALOG_PATH_FILTER)!=null) {
				String filter = settings.get(DIALOG_PATH_FILTER);
				searchIn.setText(filter);
			}

			if (settings.getArray(DIALOG_COLUMNS)!=null) {
				String[] columnWidths = settings.getArray(DIALOG_COLUMNS);
				Table table = list.getTable();
				int cols = table.getColumnCount();
				for (int i = 0; i < cols; i++) {
					TableColumn col = table.getColumn(i);
					try {
						if (col!=null) {
							col.setWidth(Integer.parseInt(columnWidths[i]));
						}
					} catch (Throwable e) {
						QuickSearchActivator.log(e);
					}
				}
			}

			if (settings.getArray(DIALOG_SASH_WEIGHTS)!=null) {
				String[] _weights = settings.getArray(DIALOG_SASH_WEIGHTS);
				int[] weights = new int[_weights.length];
				for (int i = 0; i < weights.length; i++) {
					weights[i] = Integer.parseInt(_weights[i]);
				}
				sashForm.setWeights(weights);
			}
		} catch (Throwable e) {
			//None of this stuff is critical so shouldn't stop opening dialog if it fails!
			QuickSearchActivator.log(e);
		}
	}

	private class ToggleKeepOpenAction extends Action {
		public ToggleKeepOpenAction(IDialogSettings settings) {
			super(
					Messages.QuickSearchDialog_keepOpen_toggle,
					IAction.AS_CHECK_BOX
			);
			if (settings.get(KEEP_OPEN)==null) {
				setChecked(KEEP_OPEN_DEFAULT);
			} else{
				setChecked(settings.getBoolean(KEEP_OPEN));
			}
		}

		@Override
		public void run() {
			//setChecked(!isChecked());
		}

	}


	private class ToggleCaseSensitiveAction extends Action {

		public ToggleCaseSensitiveAction(IDialogSettings settings) {
			super(
					Messages.QuickSearchDialog_caseSensitive_toggle,
					IAction.AS_CHECK_BOX
			);
			if (settings.get(CASE_SENSITIVE)==null) {
				setChecked(CASE_SENSITIVE_DEFAULT);
			} else{
				setChecked(settings.getBoolean(CASE_SENSITIVE));
			}
		}

		@Override
		public void run() {
			//setChecked(!isChecked());
			refreshHeaderLabel();
			applyFilter(false);
		}
	}


	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jface.window.Window#close()
	 */
	@Override
	public boolean close() {
		this.progressJob.cancel();
		this.progressJob = null;
//		this.refreshProgressMessageJob.cancel();
		if (showViewHandler != null) {
			IHandlerService service = PlatformUI
					.getWorkbench().getService(IHandlerService.class);
			service.deactivateHandler(showViewHandler);
			showViewHandler.getHandler().dispose();
			showViewHandler = null;
		}
		if (menuManager != null)
			menuManager.dispose();
		if (contextMenuManager != null)
			contextMenuManager.dispose();
		storeDialog(getDialogSettings());
		if (searcher!=null) {
			searcher.cancel();
		}
		return super.close();
	}

	/**
	 * Stores dialog settings.
	 *
	 * @param settings
	 *           settings used to store dialog
	 */
	protected void storeDialog(IDialogSettings settings) {
		String currentSearch = pattern.getText();
		settings.put(DIALOG_LAST_QUERY, currentSearch);
		settings.put(DIALOG_PATH_FILTER, searchIn.getText());
		if (toggleCaseSensitiveAction!=null) {
			settings.put(CASE_SENSITIVE, toggleCaseSensitiveAction.isChecked());
		}
		if (toggleKeepOpenAction!=null) {
			settings.put(KEEP_OPEN, toggleKeepOpenAction.isChecked());
		}
		Table table = list.getTable();
		if (table.getColumnCount()>0) {
			String[] columnWidths = new String[table.getColumnCount()];
			for (int i = 0; i < columnWidths.length; i++) {
				columnWidths[i] = Integer.toString(table.getColumn(i).getWidth());
			}
			settings.put(DIALOG_COLUMNS, columnWidths);
		}
		if (sashForm.getWeights()!=null) {
			int[] w = sashForm.getWeights();
			String[] ws = new String[w.length];
			for (int i = 0; i < ws.length; i++) {
				ws[i] = Integer.toString(w[i]);
			}
			settings.put(DIALOG_SASH_WEIGHTS, ws);
		}
	}

	/**
	 * Create a new header which is labelled by headerLabel.
	 *
	 * @param parent
	 * @return Label the label of the header
	 */
	private Label createHeader(Composite parent) {
		Composite header = new Composite(parent, SWT.NONE);

		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		header.setLayout(layout);

		headerLabel = new Label(header, SWT.NONE);
		headerLabel.addTraverseListener(e -> {
			if (e.detail == SWT.TRAVERSE_MNEMONIC && e.doit) {
				e.detail = SWT.TRAVERSE_NONE;
				pattern.setFocus();
			}
		});

		GridData gd = new GridData(GridData.FILL_HORIZONTAL);
		headerLabel.setLayoutData(gd);

		createViewMenu(header);
		header.setLayoutData(gd);

		refreshHeaderLabel();
		return headerLabel;
	}

	private void refreshHeaderLabel() {
		String msg = toggleCaseSensitiveAction.isChecked() ? Messages.QuickSearchDialog_caseSensitive_label : Messages.QuickSearchDialog_caseInsensitive_label;
		headerLabel.setText(msg);
	}

	/**
	 * Create the labels for the list and the progress. Return the list label.
	 *
	 * @param parent
	 * @return Label
	 */
	private Label createLabels(Composite parent) {
		Composite labels = new Composite(parent, SWT.NONE);

		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		labels.setLayout(layout);

		Label listLabel = new Label(labels, SWT.NONE);
		listLabel
				.setText(WorkbenchMessages.FilteredItemsSelectionDialog_listLabel);

		listLabel.addTraverseListener(e -> {
			if (e.detail == SWT.TRAVERSE_MNEMONIC && e.doit) {
				e.detail = SWT.TRAVERSE_NONE;
				list.getTable().setFocus();
			}
		});

		GridData gd = new GridData(GridData.FILL_HORIZONTAL);
		listLabel.setLayoutData(gd);

		progressLabel = new Label(labels, SWT.RIGHT);
		gd = new GridData(GridData.FILL_HORIZONTAL);
		progressLabel.setLayoutData(gd);
		createButton(labels, REFRESH_BUTTON_ID, Messages.QuickSearchDialog_Refresh, false);

		labels.setLayoutData(gd);
		return listLabel;
	}

	private void createViewMenu(Composite parent) {
		toolBar = new ToolBar(parent, SWT.FLAT);
		toolItem = new ToolItem(toolBar, SWT.PUSH, 0);

		GridData data = new GridData();
		data.horizontalAlignment = GridData.END;
		toolBar.setLayoutData(data);

		toolBar.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDown(MouseEvent e) {
				showViewMenu();
			}
		});

		toolItem.setImage(WorkbenchImages
				.getImage(IWorkbenchGraphicConstants.IMG_LCL_VIEW_MENU));
		toolItem
				.setToolTipText(WorkbenchMessages.FilteredItemsSelectionDialog_menu);
		toolItem.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				showViewMenu();
			}
		});

		menuManager = new MenuManager();

		fillViewMenu(menuManager);

		IHandlerService service = PlatformUI.getWorkbench()
				.getService(IHandlerService.class);
		IHandler handler = new AbstractHandler() {
			@Override
			public Object execute(ExecutionEvent event) {
				showViewMenu();
				return null;
			}
		};
		showViewHandler = service.activateHandler(
				IWorkbenchCommandConstants.WINDOW_SHOW_VIEW_MENU, handler,
				new ActiveShellExpression(getShell()));
	}

	/**
	 * Fills the menu of the dialog.
	 *
	 * @param menuManager
	 *           the menu manager
	 */
	protected void fillViewMenu(IMenuManager menuManager) {
		IDialogSettings settings = getDialogSettings();
		toggleCaseSensitiveAction = new ToggleCaseSensitiveAction(settings);
		menuManager.add(toggleCaseSensitiveAction);
		toggleKeepOpenAction = new ToggleKeepOpenAction(settings);
		menuManager.add(toggleKeepOpenAction);
	}

	private void showViewMenu() {
		Menu menu = menuManager.createContextMenu(getShell());
		Rectangle bounds = toolItem.getBounds();
		Point topLeft = new Point(bounds.x, bounds.y + bounds.height);
		topLeft = toolBar.toDisplay(topLeft);
		menu.setLocation(topLeft.x, topLeft.y);
		menu.setVisible(true);
	}

    /**
     * Hook that allows to add actions to the context menu.
	 * <p>
	 * Subclasses may extend in order to add other actions.</p>
     *
     * @param menuManager the context menu manager
     * @since 3.5
     */
	protected void fillContextMenu(IMenuManager menuManager) {
	}

	private void createPopupMenu() {

		contextMenuManager = new MenuManager();
		contextMenuManager.setRemoveAllWhenShown(true);
		contextMenuManager.addMenuListener(this::fillContextMenu);

		final Table table = list.getTable();
		Menu menu= contextMenuManager.createContextMenu(table);
		table.setMenu(menu);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	protected Control createDialogArea(Composite parent) {
		Composite dialogArea = (Composite) super.createDialogArea(parent);

		dialogArea.addDisposeListener(e -> QuickSearchDialog.this.dispose());

		Composite content = createNestedComposite(dialogArea, 1, false);
		GridData gd = new GridData(GridData.FILL_BOTH);
		content.setLayoutData(gd);

		final Label headerLabel = createHeader(content);

		Composite inputRow = createNestedComposite(content, 10, true);
		GridDataFactory.fillDefaults().grab(true, false).applyTo(inputRow);
		pattern = new Text(inputRow, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL);
		pattern.getAccessible().addAccessibleListener(new AccessibleAdapter() {
			@Override
			public void getName(AccessibleEvent e) {
				e.result = LegacyActionTools.removeMnemonics(headerLabel
						.getText());
			}
		});
		GridDataFactory.fillDefaults().span(6,1).grab(true, false).applyTo(pattern);

		Composite searchInComposite = createNestedComposite(inputRow, 2, false);
		GridDataFactory.fillDefaults().span(4,1).grab(true, false).applyTo(searchInComposite);
		Label searchInLabel = new Label(searchInComposite, SWT.NONE);
		searchInLabel.setText(Messages.QuickSearchDialog_In);
		GridDataFactory.swtDefaults().indent(5, 0).applyTo(searchInLabel);
		searchIn = new Text(searchInComposite, SWT.SINGLE | SWT.BORDER | SWT.ICON_CANCEL);
		searchIn.setToolTipText(Messages.QuickSearchDialog_InTooltip);
		GridDataFactory.fillDefaults().grab(true, false).indent(5, 0).applyTo(searchIn);

		listLabel = createLabels(content);

		sashForm = new SashForm(content, SWT.VERTICAL);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(sashForm);

		list = new TableViewer(sashForm, (multi ? SWT.MULTI : SWT.SINGLE) |
				SWT.FULL_SELECTION | SWT.BORDER | SWT.V_SCROLL | SWT.VIRTUAL);
//		ColumnViewerToolTipSupport.enableFor(list, ToolTip.NO_RECREATE);

		list.getTable().setHeaderVisible(true);
		list.getTable().setLinesVisible(true);
		list.getTable().getAccessible().addAccessibleListener(
				new AccessibleAdapter() {
					@Override
					public void getName(AccessibleEvent e) {
						if (e.childID == ACC.CHILDID_SELF) {
							e.result = LegacyActionTools
									.removeMnemonics(listLabel.getText());
						}
					}
				});
		list.setContentProvider(contentProvider);
//		new ScrollListener(list.getTable().getVerticalBar());
//		new SelectionChangedListener(list);

		TableViewerColumn col = new TableViewerColumn(list, SWT.RIGHT);
		col.setLabelProvider(LINE_NUMBER_LABEL_PROVIDER);
		col.getColumn().setText(Messages.QuickSearchDialog_line);
		col.getColumn().setWidth(40);
		col = new TableViewerColumn(list, SWT.LEFT);
		col.getColumn().setText(Messages.QuickSearchDialog_text);
		col.setLabelProvider(LINE_TEXT_LABEL_PROVIDER);
		col.getColumn().setWidth(400);
		col = new TableViewerColumn(list, SWT.LEFT);
		col.getColumn().setText(Messages.QuickSearchDialog_path);
		col.setLabelProvider(LINE_FILE_LABEL_PROVIDER);
		col.getColumn().setWidth(150);

		new TableResizeHelper(list).enableResizing();

		//list.setLabelProvider(getItemsListLabelProvider());
		list.setInput(new Object[0]);
		list.setItemCount(contentProvider.getNumberOfElements());
		gd = new GridData(GridData.FILL_BOTH);
		applyDialogFont(list.getTable());
		gd.heightHint= list.getTable().getItemHeight() * 15;
		list.getTable().setLayoutData(gd);

		createPopupMenu();

		pattern.addModifyListener(e -> {
			applyFilter(false);
		});

		searchIn.addModifyListener(e -> {
			applyPathMatcher();
		});

		pattern.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if (e.keyCode == SWT.ARROW_DOWN) {
					if (list.getTable().getItemCount() > 0) {
						list.getTable().setFocus();
						list.getTable().select(0);
						//programatic selection may not fire selection events so...
						refreshDetails();
					}
				}
			}
		});

		list.addSelectionChangedListener(event -> {
			StructuredSelection selection = (StructuredSelection) event
					.getSelection();
			handleSelected(selection);
		});

		list.addDoubleClickListener(event -> handleDoubleClick());

		list.getTable().addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {

				if (e.keyCode == SWT.ARROW_UP && (e.stateMask & SWT.SHIFT) == 0
						&& (e.stateMask & SWT.CTRL) == 0) {
					StructuredSelection selection = (StructuredSelection) list
							.getSelection();

					if (selection.size() == 1) {
						Object element = selection.getFirstElement();
						if (element.equals(list.getElementAt(0))) {
							pattern.setFocus();
						}
						list.getTable().notifyListeners(SWT.Selection,
								new Event());

					}
				}

				if (e.keyCode == SWT.ARROW_DOWN
						&& (e.stateMask & SWT.SHIFT) != 0
						&& (e.stateMask & SWT.CTRL) != 0) {

					list.getTable().notifyListeners(SWT.Selection, new Event());
				}

			}
		});

		createDetailsArea(sashForm);
		sashForm.setWeights(new int[] {5,2});

		applyDialogFont(content);

		restoreDialog(getDialogSettings());

		if (initialPatternText != null) {
			pattern.setText(initialPatternText);
		}

		switch (selectionMode) {
		case CARET_BEGINNING:
			pattern.setSelection(0, 0);
			break;
		case FULL_SELECTION:
			pattern.selectAll();
			break;
		}

		// apply filter even if pattern is empty (display history)
		applyFilter(false);

		return dialogArea;
	}

	private Composite createNestedComposite(Composite parent, int numRows, boolean equalRows) {
		Composite nested = new Composite(parent, SWT.NONE);
		{
			GridLayout layout = new GridLayout(numRows, equalRows);
			layout.marginWidth = 0;
			layout.marginHeight = 0;
			layout.marginLeft = 0;
			layout.marginRight = 0;
			layout.horizontalSpacing = 0;
			nested.setLayout(layout);
		}
		GridDataFactory.fillDefaults().grab(true, false).applyTo(nested);
		return nested;
	}

	protected void dispose() {
		if (blankImage!=null) {
			blankImage.dispose();
			blankImage = null;
		}
	}

	private void createDetailsArea(Composite parent) {
		details = new StyledText(parent, SWT.MULTI+SWT.READ_ONLY+SWT.BORDER+SWT.H_SCROLL+SWT.V_SCROLL);
		details.setFont(JFaceResources.getFont(TEXT_FONT));

		list.addSelectionChangedListener(event -> refreshDetails());
		details.addControlListener(new ControlAdapter() {
			@Override
			public void controlResized(ControlEvent e) {
				refreshDetails();
			}
		});
	}


	// Dumber version just using the a 'raw' StyledText widget.
	private void refreshDetails() {
		if (details!=null && list!=null && !list.getTable().isDisposed()) {
			if (documents==null) {
				documents = new DocumentFetcher();
			}
			IStructuredSelection sel = (IStructuredSelection) list.getSelection();
			if (sel==null || sel.isEmpty()) {
				details.setText(EMPTY_STRING);
			} else {
				//Not empty selection
				final int context = 100; // number of lines before and after match to include in preview
				int numLines = computeLines();
				if (numLines > 0) {
					LineItem item = (LineItem) sel.getFirstElement();
					IDocument document = documents.getDocument(item.getFile());
					if (document!=null) {
						try {
							int line = item.getLineNumber()-1; //in document lines are 0 based. In search 1 based.
							int contextStartLine = Math.max(line-(numLines-1)/2 - context, 0);
							int start = document.getLineOffset(contextStartLine);
							int end = document.getLength();
							try {
								IRegion lineInfo = document.getLineInformation(line + numLines/2 + context);
								end = lineInfo.getOffset() + lineInfo.getLength();
							} catch (BadLocationException e) {
								//Presumably line number is past the end of document.
								//ignore.
							}

							StyledString styledString = highlightMatches(document.get(start, end-start));
							details.setText(styledString.getString());
							details.setStyleRanges(styledString.getStyleRanges());
							details.setTopIndex(Math.max(line - contextStartLine - numLines/2, 0));
							return;
						} catch (BadLocationException e) {
						}
					}
				}
			}
			//empty selection or some error:
			details.setText(EMPTY_STRING);
		}
	}

	/**
	 * Computes how many lines of text can be displayed in the details section.
	 */
	private int computeLines() {
		if (details!=null && !details.isDisposed()) {
			int lineHeight = details.getLineHeight();
			int areaHeight = details.getClientArea().height;
			return (areaHeight + lineHeight - 1) / lineHeight;
		}
		return 0;
	}

	/**
	 * Helper function to highlight all the matches for the current query in a given piece
	 * of text.
	 *
	 * @return StyledString instance.
	 */
	private StyledString highlightMatches(String visibleText) {
		StyledString styledText = new StyledString(visibleText);
		List<TextRange> matches = getQuery().findAll(visibleText);
		for (TextRange m : matches) {
			styledText.setStyle(m.getOffset(), m.getLength(), HIGHLIGHT_STYLE);
		}
		return styledText;
	}

// Version using sourceviewer
//	private void refreshDetails() {
//		if (details!=null && list!=null && !list.getTable().isDisposed()) {
//			if (documents==null) {
//				documents = new DocumentFetcher();
//			}
//			IStructuredSelection sel = (IStructuredSelection) list.getSelection();
//			if (sel!=null && !sel.isEmpty()) {
//				//Not empty selection
//				LineItem item = (LineItem) sel.getFirstElement();
//				IDocument document = documents.getDocument(item.getFile());
//				try {
//					int line = item.getLineNumber()-1; //in document lines are 0 based. In search 1 based.
//					int start = document.getLineOffset(Math.max(line-2, 0));
//					int end = document.getLength();
//					try {
//						end = document.getLineOffset(line+3);
//					} catch (BadLocationException e) {
//						//Presumably line number is past the end of document.
//						//ignore.
//					}
//					details.setDocument(document, start, end-start);
//
//					String visibleText = document.get(start, end-start);
//					List<TextRange> matches = getQuery().findAll(visibleText);
//					Region visibleRegion = new Region(start, end-start);
//					TextPresentation presentation = new TextPresentation(visibleRegion, 20);
//					presentation.setDefaultStyleRange(new StyleRange(0, document.getLength(), null, null));
//					for (TextRange m : matches) {
//						presentation.addStyleRange(new StyleRange(m.start+start, m.len, null, YELLOW));
//					}
//					details.changeTextPresentation(presentation, true);
//
//					return;
//				} catch (BadLocationException e) {
//				}
//			}
//			details.setDocument(null);
//		}
//	}

	/**
	 * Handle selection in the items list by updating labels of selected and
	 * unselected items and refresh the details field using the selection.
	 *
	 * @param selection
	 *           the new selection
	 */
	protected void handleSelected(StructuredSelection selection) {
		IStatus status = new Status(IStatus.OK, PlatformUI.PLUGIN_ID,
				IStatus.OK, EMPTY_STRING, null);

		updateStatus(status);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jface.window.Dialog#getDialogBoundsSettings()
	 */
	@Override
	protected IDialogSettings getDialogBoundsSettings() {
		IDialogSettings settings = getDialogSettings();
		IDialogSettings section = settings.getSection(DIALOG_BOUNDS_SETTINGS);
		if (section == null) {
			section = settings.addNewSection(DIALOG_BOUNDS_SETTINGS);
			section.put(DIALOG_HEIGHT, 500);
			section.put(DIALOG_WIDTH, 600);
		}
		return section;
	}

	/**
	 * Returns the dialog settings. Returned object can't be null.
	 *
	 * @return return dialog settings for this dialog
	 */
	protected IDialogSettings getDialogSettings() {
		IDialogSettings dialogSettings = PlatformUI.getDialogSettingsProvider(FrameworkUtil.getBundle(IDEWorkbenchPlugin.class))
				.getDialogSettings();
		IDialogSettings settings = dialogSettings.getSection(DIALOG_SETTINGS);
		if (settings == null) {
			settings = dialogSettings
					.addNewSection(DIALOG_SETTINGS);
		}
		return settings;
	}

	/**
	 * Has to be called in UI thread.
	 */
	public void refreshWidgets() {
		if (list != null && !list.getTable().isDisposed()) {
			int itemCount = contentProvider.getNumberOfElements();
			list.setItemCount(itemCount);
			if (itemCount < MAX_RESULTS) {
				listLabel.setText(NLS.bind(Messages.QuickSearchDialog_listLabel, itemCount));
			} else {
				listLabel.setText(NLS.bind(Messages.QuickSearchDialog_listLabel_limit_reached, itemCount));
			}
			listLabel.pack();
			list.refresh(true, false);
			Button openButton = getButton(OPEN_BUTTON_ID);
			if (openButton!=null && !openButton.isDisposed()) {
				//Even if no element is selected. The dialog should be have as if the first
				//element in the list is selected. So the button is enabled if any
				//element is available in the list.
				openButton.setEnabled(itemCount>0);
			}
			refreshDetails();
		}
	}

	/**
	 * Schedule refresh job.
	 */
	public void scheduleRefresh() {
		refreshJob.schedule();
//		refreshCacheJob.cancelAll();
//		refreshCacheJob.schedule();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.ui.dialogs.SelectionStatusDialog#computeResult()
	 */
	@Override
	protected void computeResult() {
		List objectsToReturn = ((StructuredSelection) list.getSelection())
				.toList();
		if (objectsToReturn.isEmpty()) {
			//Pretend that the first element is selected.
			Object first = list.getElementAt(0);
			if (first!=null) {
				objectsToReturn = Arrays.asList(first);
			}
		}
		setResult(objectsToReturn);
	}



	/**
	 * Handles double-click of items, but *also* by pressing the 'enter' key.
	 */
	protected void handleDoubleClick() {
		okPressed();
	}

	protected void refreshButtonPressed() {
		applyFilter(true);
	}


	@Override
	protected void okPressed() {
		computeResult();
		openSelection();
		if (!toggleKeepOpenAction.isChecked()) {
			setReturnCode(OK);
			close();
		}
	}

	@Override
	protected void buttonPressed(int buttonId) {
		if (buttonId == REFRESH_BUTTON_ID) {
			refreshButtonPressed();
		} else {
			super.buttonPressed(buttonId);
		}
	}

	@Override
	protected void createButtonsForButtonBar(Composite parent) {
		createButton(parent, OPEN_BUTTON_ID, Messages.QuickSearchDialog_Open, true);
		refreshWidgets();
	}

	/**
	 * Sets the initial pattern used by the filter. This text is copied into the
	 * selection input on the dialog. A full selection is used in the pattern
	 * input field.
	 *
	 * @param text
	 *           initial pattern for the filter
	 * @see QuickSearchDialog#FULL_SELECTION
	 */
	public void setInitialPattern(String text) {
		setInitialPattern(text, FULL_SELECTION);
	}

	/**
	 * Sets the initial pattern used by the filter. This text is copied into the
	 * selection input on the dialog. The <code>selectionMode</code> is used
	 * to choose selection type for the input field.
	 *
	 * @param text
	 *           initial pattern for the filter
	 * @param selectionMode
	 *           one of: {@link QuickSearchDialog#NONE},
	 *           {@link QuickSearchDialog#CARET_BEGINNING},
	 *           {@link QuickSearchDialog#FULL_SELECTION}
	 */
	public void setInitialPattern(String text, int selectionMode) {
		this.initialPatternText = text;
		this.selectionMode = selectionMode;
	}

	/**
	 * Gets initial pattern.
	 *
	 * @return initial pattern, or <code>null</code> if initial pattern is not
	 *        set
	 */
	protected String getInitialPattern() {
		return this.initialPatternText;
	}

	/**
	 * Returns the current selection.
	 *
	 * @return the current selection
	 */
	protected StructuredSelection getSelectedItems() {

		StructuredSelection selection = (StructuredSelection) list
				.getSelection();

		List selectedItems = selection.toList();

		return new StructuredSelection(selectedItems);
	}

	/**
	 * Validates the item. When items on the items list are selected or
	 * deselected, it validates each item in the selection and the dialog status
	 * depends on all validations.
	 *
	 * @param item
	 *           an item to be checked
	 * @return status of the dialog to be set
	 */
	protected IStatus validateItem(Object item) {
		return Status.OK_STATUS;
	}

	/**
	 * Creates an instance of a filter.
	 *
	 * @return a filter for items on the items list. Can be <code>null</code>,
	 *        no filtering will be applied then, causing no item to be shown in
	 *        the list.
	 */
	protected QuickTextQuery createFilter() {
		return new QuickTextQuery(pattern.getText(), !toggleCaseSensitiveAction.isChecked());
	}

	/**
	 * Applies the filter created by <code>createFilter()</code> method to the
	 * items list. When new filter is different than previous one it will cause
	 * refiltering.
	 * <p>
	 * The 'force' parameter forces a full refresh of the search results / filter even
	 * when the filter is unchanged, or when a incremenal filtering optimisation could be
	 * applied based on query structure. (The use case for this, is to trigger forced refresh
	 * because the underlying resources may have changed).
	 */
	protected void applyFilter(boolean force) {
		QuickTextQuery newFilter = createFilter();
		if (this.searcher==null) {
			if (!newFilter.isTrivial()) {
				//Create the QuickTextSearcher with the inital query.
				this.searcher = new QuickTextSearcher(newFilter, context.createPriorityFun(), MAX_LINE_LEN, new QuickTextSearchRequestor() {
					@Override
					public void add(LineItem match) {
						contentProvider.add(match);
						contentProvider.refresh();
					}
					@Override
					public void clear() {
						contentProvider.reset();
						contentProvider.refresh();
					}
					@Override
					public void revoke(LineItem match) {
						contentProvider.remove(match);
						contentProvider.refresh();
					}
					@Override
					public void update(LineItem match) {
						contentProvider.refresh();
					}
				});
				this.searcher.setMaxResults(MAX_RESULTS);
				applyPathMatcher();
				refreshWidgets();
			}
//			this.list.setInput(input)
		} else {
			//The QuickTextSearcher is already active update the query
			this.searcher.setQuery(newFilter, force);
		}
		if (progressJob!=null) {
			progressJob.schedule();
		}
	}

	private void applyPathMatcher() {
		if (this.searcher!=null) {
			this.searcher.setPathMatcher(ResourceMatchers.commaSeparatedPaths(searchIn.getText()));
		}
	}


	/**
	 * Collects filtered elements. Contains one synchronized, sorted set for
	 * collecting filtered elements.
	 * Implementation of <code>ItemsFilter</code> is used to filter elements.
	 * The key function of filter used in to filtering is
	 * <code>matchElement(Object item)</code>.
	 * <p>
	 * The <code>ContentProvider</code> class also provides item filtering
	 * methods. The filtering has been moved from the standard TableView
	 * <code>getFilteredItems()</code> method to content provider, because
	 * <code>ILazyContentProvider</code> and virtual tables are used. This
	 * class is responsible for adding a separator below history items and
	 * marking each items as duplicate if its name repeats more than once on the
	 * filtered list.
	 */
	private class ContentProvider implements IStructuredContentProvider, ILazyContentProvider {

		private List items;

		/**
		 * Creates new instance of <code>ContentProvider</code>.
		 */
		public ContentProvider() {
			this.items = Collections.synchronizedList(new ArrayList(2048));
//			this.duplicates = Collections.synchronizedSet(new HashSet(256));
//			this.lastSortedItems = Collections.synchronizedList(new ArrayList(
//					2048));
		}

		public void remove(LineItem match) {
			this.items.remove(match);
		}

		/**
		 * Removes all content items and resets progress message.
		 */
		public void reset() {
			this.items.clear();
		}

		/**
		 * Adds filtered item.
		 *
		 * @param match
		 */
		public void add(LineItem match) {
			this.items.add(match);
		}

		/**
		 * Refresh dialog.
		 */
		public void refresh() {
			scheduleRefresh();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
		 */
		@Override
		public Object[] getElements(Object inputElement) {
			return items.toArray();
		}

		public int getNumberOfElements() {
			return items.size();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see org.eclipse.jface.viewers.IContentProvider#dispose()
		 */
		@Override
		public void dispose() {
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer,
		 *     java.lang.Object, java.lang.Object)
		 */
		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see org.eclipse.jface.viewers.ILazyContentProvider#updateElement(int)
		 */
		@Override
		public void updateElement(int index) {

			QuickSearchDialog.this.list.replace((items
					.size() > index) ? items.get(index) : null,
					index);

		}

	}

	/**
	 * Get the control where the search pattern is entered. Any filtering should
	 * be done using an {@link ItemsFilter}. This control should only be
	 * accessed for listeners that wish to handle events that do not affect
	 * filtering such as custom traversal.
	 *
	 * @return Control or <code>null</code> if the pattern control has not
	 *        been created.
	 */
	public Control getPatternControl() {
		return pattern;
	}

	public QuickTextQuery getQuery() {
		return searcher.getQuery();
	}

}
