SemanticCodeSearchPage.java

/*******************************************************************************
 * Copyright (c) 2026 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
 *******************************************************************************/
package org.sandbox.jdt.internal.ui.search;

import org.eclipse.jface.dialogs.DialogPage;
import org.eclipse.search.ui.ISearchPage;
import org.eclipse.search.ui.ISearchPageContainer;
import org.eclipse.search.ui.NewSearchUI;
import org.eclipse.swt.SWT;
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.Label;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;
import org.sandbox.jdt.internal.ui.search.SemanticCodeSearchQuery.SearchMode;
import org.sandbox.jdt.internal.ui.search.gitindex.EmbeddedSearchService;

/**
 * Eclipse search page for semantic code search using local AI embeddings.
 *
 * <p>
 * Provides a UI to search code by natural language description. Uses
 * {@link EmbeddedSearchService} to delegate queries via a
 * {@code SemanticSearchClient} to the semantic search REST backend.
 * </p>
 *
 * <p>
 * UI layout:
 * </p>
 *
 * <pre>
 * ┌─────────────────────────────────────────────────┐
 * │ Search: [Describe what you're looking for...  ] │
 * │                                                 │
 * │ Mode:  ○ Semantic  ○ Hybrid  ○ Similar Code     │
 * │                                                 │
 * │ Repository: [text field                       ] │
 * │ Max results: [10 ▾]                             │
 * │                                                 │
 * │ ℹ Uses local AI embeddings (offline, ~1ms)      │
 * └─────────────────────────────────────────────────┘
 * </pre>
 */
public class SemanticCodeSearchPage extends DialogPage implements ISearchPage {

	/** The search page ID as registered in plugin.xml. */
	public static final String PAGE_ID= "org.sandbox.jdt.ui.SemanticCodeSearchPage"; //$NON-NLS-1$

	private ISearchPageContainer container;

	private Text queryText;
	private Button semanticRadio;
	private Button hybridRadio;
	private Button similarRadio;
	private Text repoText;
	private Spinner maxResultsSpinner;

	@Override
	public void createControl(Composite parent) {
		initializeDialogUnits(parent);

		Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayout(new GridLayout(2, false));
		composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		// Search query field
		Label searchLabel= new Label(composite, SWT.NONE);
		searchLabel.setText("Search:"); //$NON-NLS-1$

		queryText= new Text(composite, SWT.BORDER | SWT.SINGLE);
		queryText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		queryText.setMessage("Natural language query (or blob object ID for Similar mode)"); //$NON-NLS-1$

		// Mode section
		Label modeLabel= new Label(composite, SWT.NONE);
		modeLabel.setText("Mode:"); //$NON-NLS-1$

		Composite modeComposite= new Composite(composite, SWT.NONE);
		modeComposite.setLayout(new GridLayout(3, false));
		modeComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

		semanticRadio= new Button(modeComposite, SWT.RADIO);
		semanticRadio.setText("Semantic"); //$NON-NLS-1$
		semanticRadio.setSelection(true);

		hybridRadio= new Button(modeComposite, SWT.RADIO);
		hybridRadio.setText("Hybrid"); //$NON-NLS-1$

		similarRadio= new Button(modeComposite, SWT.RADIO);
		similarRadio.setText("Similar Code"); //$NON-NLS-1$

		// Repository field
		Label repoLabel= new Label(composite, SWT.NONE);
		repoLabel.setText("Repository:"); //$NON-NLS-1$

		repoText= new Text(composite, SWT.BORDER | SWT.SINGLE);
		repoText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		repoText.setMessage("Repository name (required)"); //$NON-NLS-1$

		// Max results
		Label maxLabel= new Label(composite, SWT.NONE);
		maxLabel.setText("Max results:"); //$NON-NLS-1$

		maxResultsSpinner= new Spinner(composite, SWT.BORDER);
		maxResultsSpinner.setMinimum(1);
		maxResultsSpinner.setMaximum(100);
		maxResultsSpinner.setSelection(10);

		// Info label
		Label infoLabel= new Label(composite, SWT.NONE);
		infoLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
		infoLabel.setText("\u2139 Uses local AI embeddings via local Git index service (requires running backend)"); //$NON-NLS-1$

		setControl(composite);
	}

	@Override
	public boolean performAction() {
		String text= queryText != null ? queryText.getText().trim() : ""; //$NON-NLS-1$
		if (text.isEmpty()) {
			return false;
		}

		if (EmbeddedSearchService.getInstance().getSearchClient() == null) {
			setErrorMessage(Messages.SemanticSearchClient_BackendNotAvailable);
			return false;
		}

		SearchMode mode;
		if (hybridRadio != null && hybridRadio.getSelection()) {
			mode= SearchMode.HYBRID;
		} else if (similarRadio != null && similarRadio.getSelection()) {
			mode= SearchMode.SIMILAR;
		} else {
			mode= SearchMode.SEMANTIC;
		}

		String repo= repoText != null ? repoText.getText().trim() : ""; //$NON-NLS-1$
		int maxResults= maxResultsSpinner != null ? maxResultsSpinner.getSelection() : 10;

		SemanticCodeSearchQuery query= new SemanticCodeSearchQuery(text, repo, maxResults, mode);
		NewSearchUI.runQueryInBackground(query);
		return true;
	}

	@Override
	public void setContainer(ISearchPageContainer container) {
		this.container= container;
	}

	/**
	 * Returns the search page container.
	 *
	 * @return the container
	 */
	protected ISearchPageContainer getContainer() {
		return container;
	}
}