feat: Support 'go to test' command (#1332)

This commit is contained in:
Sheng Chen 2021-11-16 11:19:38 +08:00 коммит произвёл GitHub
Родитель 40e6dee508
Коммит f7a04fc692
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 858 добавлений и 211 удалений

Просмотреть файл

@ -12,6 +12,7 @@
<command id="vscode.java.test.findTestTypesAndMethods" />
<command id="vscode.java.test.resolvePath" />
<command id="vscode.java.test.findTestLocation" />
<command id="vscode.java.test.navigateToTestOrTarget" />
</delegateCommandHandler>
</extension>
</plugin>

Просмотреть файл

@ -14,6 +14,7 @@ package com.microsoft.java.test.plugin.handler;
import com.microsoft.java.test.plugin.launchers.JUnitLaunchUtils;
import com.microsoft.java.test.plugin.util.ProjectTestUtils;
import com.microsoft.java.test.plugin.util.TestGenerationUtils;
import com.microsoft.java.test.plugin.util.TestNavigationUtils;
import com.microsoft.java.test.plugin.util.TestSearchUtils;
import org.eclipse.core.runtime.IProgressMonitor;
@ -34,6 +35,7 @@ public class TestDelegateCommandHandler implements IDelegateCommandHandler {
private static final String FIND_TYPES_AND_METHODS = "vscode.java.test.findTestTypesAndMethods";
private static final String RESOLVE_PATH = "vscode.java.test.resolvePath";
private static final String FIND_TEST_LOCATION = "vscode.java.test.findTestLocation";
private static final String NAVIGATE_TO_TEST_OR_TARGET = "vscode.java.test.navigateToTestOrTarget";
@Override
public Object executeCommand(String commandId, List<Object> arguments, IProgressMonitor monitor) throws Exception {
@ -60,6 +62,8 @@ public class TestDelegateCommandHandler implements IDelegateCommandHandler {
return TestSearchUtils.resolvePath(arguments, monitor);
case FIND_TEST_LOCATION:
return TestSearchUtils.findTestLocation(arguments, monitor);
case NAVIGATE_TO_TEST_OR_TARGET:
return TestNavigationUtils.findTestOrTarget(arguments, monitor);
default:
throw new UnsupportedOperationException(
String.format("Java test plugin doesn't support the command '%s'.", commandId));

Просмотреть файл

@ -0,0 +1,224 @@
/*******************************************************************************
* Copyright (c) 2021 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/
package com.microsoft.java.test.plugin.util;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeNameRequestor;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType;
import org.eclipse.jdt.ls.core.internal.ProjectUtils;
import org.eclipse.lsp4j.Location;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Utils for test navigation features
*/
public class TestNavigationUtils {
/**
* find test or test target according to the given java source file uri.
* @param arguments arguments
* @param monitor monitor
* @return the search result for test navigation
* @throws JavaModelException
*/
public static TestNavigationResult findTestOrTarget(List<Object> arguments, IProgressMonitor monitor)
throws JavaModelException {
if (arguments == null || arguments.size() < 2) {
throw new IllegalArgumentException("Wrong arguments passed to findTestOrTarget().");
}
final String typeUri = (String) arguments.get(0);
final ICompilationUnit unit = JDTUtils.resolveCompilationUnit(typeUri);
if (unit == null) {
JUnitPlugin.logError("Failed to resolve compilation unit from " + typeUri);
return null;
}
final boolean goToTest = (boolean) arguments.get(1);
final IType primaryType = unit.findPrimaryType();
final String typeName;
Location location = null;
if (primaryType != null) {
typeName = primaryType.getElementName();
location = JDTUtils.toLocation(primaryType, LocationType.NAME_RANGE);
} else {
typeName = unit.getElementName().substring(0, unit.getElementName().lastIndexOf(".java"));
}
final SearchEngine searchEngine = new SearchEngine();
final IJavaProject javaProject = unit.getJavaProject();
final IJavaSearchScope scope = goToTest ? getSearchScopeForTest() :
getSearchScopeForTarget();
final Set<TestNavigationItem> items = new HashSet<>();
searchEngine.searchAllTypeNames(
null,
SearchPattern.R_EXACT_MATCH,
("*" + typeName + "*").toCharArray(),
SearchPattern.R_PREFIX_MATCH,
IJavaSearchConstants.CLASS,
scope,
new TestNavigationNameRequestor(items, javaProject, typeName),
IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH,
monitor
);
return new TestNavigationResult(items, location);
}
private static IJavaSearchScope getSearchScopeForTarget() {
// TODO: unimplemented
return null;
}
private static IJavaSearchScope getSearchScopeForTest() throws JavaModelException {
final List<IJavaElement> javaElements = new LinkedList<>();
final IJavaProject[] javaProjects = ProjectUtils.getJavaProjects();
for (final IJavaProject project : javaProjects) {
final List<IClasspathEntry> testEntries = ProjectTestUtils.getTestEntries(project);
for (final IClasspathEntry entry : testEntries) {
javaElements.addAll(Arrays.asList(project.findPackageFragmentRoots(entry)));
}
}
return SearchEngine.createJavaSearchScope(javaElements.toArray(new IJavaElement[0]));
}
static final class TestNavigationNameRequestor extends TypeNameRequestor {
private final Set<TestNavigationItem> results;
private final IJavaProject javaProject;
private final String typeName;
private TestNavigationNameRequestor(Set<TestNavigationItem> results, IJavaProject javaProject,
String typeName) {
this.results = results;
this.javaProject = javaProject;
this.typeName = typeName;
}
@Override
public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName,
char[][] enclosingTypeNames, String path) {
if (!path.endsWith(".java")) {
return;
}
final IPath fullPath = new Path(path);
final IFile file = javaProject.getProject().getFile(
fullPath.makeRelativeTo(javaProject.getProject().getFullPath()));
if (!file.exists()) {
return;
}
final String uri = file.getLocation().toFile().toURI().toString();
final String simpleName;
if (enclosingTypeNames.length == 0) {
simpleName = String.valueOf(simpleTypeName);
} else {
// All the nested classes are ignored.
simpleName = String.valueOf(enclosingTypeNames[0]);
}
final String fullyQualifiedName = String.valueOf(packageName) + "." + simpleName;
int relevance;
if (Objects.equals(simpleName, this.typeName + "Test") ||
Objects.equals(simpleName, this.typeName + "Tests")) {
// mark this as most relevance
relevance = Integer.MIN_VALUE;
} else {
relevance = simpleName.indexOf(this.typeName);
if (relevance < 0) {
// todo: better relevance calculation
relevance = Integer.MAX_VALUE;
}
}
final boolean outOfBelongingProject;
if (Objects.equals(this.javaProject.getElementName(), fullPath.segment(0))) {
outOfBelongingProject = false;
} else {
outOfBelongingProject = true;
}
results.add(new TestNavigationItem(simpleName, fullyQualifiedName, uri, relevance, outOfBelongingProject));
}
}
static final class TestNavigationResult {
public Collection<TestNavigationItem> items;
public Location location;
public TestNavigationResult(Collection<TestNavigationItem> items, Location location) {
this.items = items;
this.location = location;
}
}
static final class TestNavigationItem {
public String simpleName;
public String fullyQualifiedName;
public String uri;
public int relevance;
public boolean outOfBelongingProject;
public TestNavigationItem(String simpleName, String fullyQualifiedName, String uri,
int relevance, boolean outOfBelongingProject) {
this.simpleName = simpleName;
this.fullyQualifiedName = fullyQualifiedName;
this.uri = uri;
this.relevance = relevance;
this.outOfBelongingProject = outOfBelongingProject;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((uri == null) ? 0 : uri.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final TestNavigationItem other = (TestNavigationItem) obj;
if (uri == null) {
if (other.uri != null) {
return false;
}
} else if (!uri.equals(other.uri)) {
return false;
}
return true;
}
}
}

Просмотреть файл

@ -120,10 +120,26 @@
{
"command": "java.test.editor.debug",
"when": "java:serverMode != LightWeight"
},
{
"command": "java.test.goToTest",
"when": "java:testRunnerActivated && resourceExtname == .java"
}
],
"editor/context": [
{
"command": "java.test.goToTest",
"when": "java:testRunnerActivated && resourcePath =~ /.*src[/|\\\\]main[/|\\\\]java[/|\\\\].*\\.java/",
"group": "navigation@100"
}
]
},
"commands": [
{
"command": "java.test.goToTest",
"title": "%contributes.commands.java.test.goToTest.title%",
"category": "Java"
},
{
"command": "java.test.runFromJavaProjectExplorer",
"title": "%contributes.commands.java.test.runFromJavaProjectExplorer.title%",
@ -179,23 +195,23 @@
"classPaths": {
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.classPaths.auto.description%",
"%configuration.java.test.config.classPaths.runtime.description%",
"%configuration.java.test.config.classPaths.test.description%",
"%configuration.java.test.config.classPaths.exclude.description%"
]
},
"string"
]
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.classPaths.auto.description%",
"%configuration.java.test.config.classPaths.runtime.description%",
"%configuration.java.test.config.classPaths.test.description%",
"%configuration.java.test.config.classPaths.exclude.description%"
]
},
"string"
]
},
"description": "%configuration.java.test.config.classPaths.description%",
"default": []
@ -203,23 +219,23 @@
"modulePaths": {
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.modulePaths.auto.description%",
"%configuration.java.test.config.modulePaths.runtime.description%",
"%configuration.java.test.config.modulePaths.test.description%",
"%configuration.java.test.config.modulePaths.exclude.description%"
]
},
"string"
]
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.modulePaths.auto.description%",
"%configuration.java.test.config.modulePaths.runtime.description%",
"%configuration.java.test.config.modulePaths.test.description%",
"%configuration.java.test.config.modulePaths.exclude.description%"
]
},
"string"
]
},
"description": "%configuration.java.test.config.modulePaths.description%",
"default": []
@ -283,23 +299,23 @@
"classPaths": {
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.classPaths.auto.description%",
"%configuration.java.test.config.classPaths.runtime.description%",
"%configuration.java.test.config.classPaths.test.description%",
"%configuration.java.test.config.classPaths.exclude.description%"
]
},
"string"
]
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.classPaths.auto.description%",
"%configuration.java.test.config.classPaths.runtime.description%",
"%configuration.java.test.config.classPaths.test.description%",
"%configuration.java.test.config.classPaths.exclude.description%"
]
},
"string"
]
},
"description": "%configuration.java.test.config.classPaths.description%",
"default": []
@ -307,23 +323,23 @@
"modulePaths": {
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.modulePaths.auto.description%",
"%configuration.java.test.config.modulePaths.runtime.description%",
"%configuration.java.test.config.modulePaths.test.description%",
"%configuration.java.test.config.modulePaths.exclude.description%"
]
},
"string"
]
"anyOf": [
{
"enum": [
"$Auto",
"$Runtime",
"$Test",
"!<path>"
],
"enumDescriptions": [
"%configuration.java.test.config.modulePaths.auto.description%",
"%configuration.java.test.config.modulePaths.runtime.description%",
"%configuration.java.test.config.modulePaths.test.description%",
"%configuration.java.test.config.modulePaths.exclude.description%"
]
},
"string"
]
},
"description": "%configuration.java.test.config.modulePaths.description%",
"default": []

Просмотреть файл

@ -5,6 +5,7 @@
"contributes.commands.java.test.runFromJavaProjectExplorer.title": "Run Tests",
"contributes.commands.java.test.debugFromJavaProjectExplorer.title": "Debug Tests",
"contributes.commands.java.test.refreshExplorer.title": "Refresh",
"contributes.commands.java.test.goToTest.title": "Go to Test",
"configuration.java.test.defaultConfig.description": "Specify the name of the default test configuration",
"configuration.java.test.config.description": "Specify the configurations for running the tests",
"configuration.java.test.config.item.description": "Specify the configuration item for running the tests",

Просмотреть файл

@ -5,6 +5,7 @@
"contributes.commands.java.test.runFromJavaProjectExplorer.title": "运行测试",
"contributes.commands.java.test.debugFromJavaProjectExplorer.title": "调试测试",
"contributes.commands.java.test.refreshExplorer.title": "刷新",
"contributes.commands.java.test.goToTest.title": "转到测试",
"configuration.java.test.defaultConfig.description": "设定默认测试配置项的名称",
"configuration.java.test.config.description": "设定运行测试的配置信息",
"configuration.java.test.config.item.description": "设定运行测试时所用的配置项",

Просмотреть файл

@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { ExtensionContext, commands, window, Disposable, QuickPick, QuickInputButton, ThemeIcon, QuickPickItem } from 'vscode';
import { JavaTestRunnerCommands } from '../constants';
export async function registerAskForChoiceCommand(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand(JavaTestRunnerCommands.ASK_CLIENT_FOR_CHOICE, async (placeHolder: string, choices: IOption[], canPickMany: boolean) => {
const ans: any = await window.showQuickPick(choices, {
placeHolder,
canPickMany,
ignoreFocusOut: true,
});
if (!ans) {
return undefined;
} else if (Array.isArray(ans)) {
return ans.map((a: IOption) => a.value || a.label);
}
return ans.value || ans.label;
}));
}
export async function registerAdvanceAskForChoice(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand(JavaTestRunnerCommands.ADVANCED_ASK_CLIENT_FOR_CHOICE, async (placeHolder: string, choices: IOption[], advancedAction: string, canPickMany: boolean) => {
let result: string[] | undefined;
const disposables: Disposable[] = [];
try {
result = await new Promise<string[] | undefined>((resolve: (value: string[] | undefined) => void) => {
const quickPick: QuickPick<IOption> = window.createQuickPick<IOption>();
// if all the items are advanced item, show them directly
let showAdvancedItem: boolean = choices.filter((c: IOption) => {
return !c.isAdvanced;
}).length === 0;
quickPick.title = placeHolder;
quickPick.placeholder = placeHolder;
quickPick.items = filterOptions(showAdvancedItem, choices);
quickPick.buttons = getActionButtons(showAdvancedItem, advancedAction);
quickPick.canSelectMany = canPickMany;
quickPick.ignoreFocusOut = true;
disposables.push(quickPick.onDidTriggerButton((btn: QuickInputButton) => {
if (btn.tooltip?.endsWith(advancedAction)) {
showAdvancedItem = !showAdvancedItem;
quickPick.items = filterOptions(showAdvancedItem, choices);
quickPick.buttons = getActionButtons(showAdvancedItem, advancedAction);
}
}));
disposables.push(quickPick.onDidHide(() => {
return resolve(undefined);
}));
disposables.push(quickPick.onDidAccept(() => {
return resolve(quickPick.selectedItems.map((o: IOption) => o.value));
}));
disposables.push(quickPick);
quickPick.show();
});
} finally {
for (const d of disposables) {
d.dispose();
}
}
return result;
}));
function filterOptions(showAdvancedItem: boolean, choices: IOption[]): IOption[] {
return choices.filter((c: IOption) => {
return !c.isAdvanced || showAdvancedItem && c.isAdvanced;
});
}
function getActionButtons(showAdvancedItem: boolean, advancedAction: string): QuickInputButton[] {
if (showAdvancedItem) {
return [{
iconPath: new ThemeIcon('collapse-all'),
tooltip: `Hide ${advancedAction}`,
}];
}
return [{
iconPath: new ThemeIcon('expand-all'),
tooltip: `Show ${advancedAction}`,
}];
}
}
/**
* A command that server side can call to get a input value.
* Currently it's only used to get a Java qualified name, so the check is on the client side.
* @param context
*/
export async function registerAskForInputCommand(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand(JavaTestRunnerCommands.ASK_CLIENT_FOR_INPUT, async (prompt: string, value: string) => {
const ans: string | undefined = await window.showInputBox({
value,
prompt,
validateInput: checkJavaQualifiedName,
});
return ans;
}));
}
function checkJavaQualifiedName(value: string): string {
if (!value || !value.trim()) {
return 'Input cannot be empty.';
}
for (const part of value.split('.')) {
if (isKeyword(part)) {
return `Keyword '${part}' cannot be used.`;
}
if (!isJavaIdentifier(part)) {
return `Invalid Java qualified name.`;
}
}
return '';
}
// Copied from https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-Keyword
const keywords: Set<string> = new Set([
'abstract', 'continue', 'for', 'new', 'switch',
'assert', 'default', 'if', 'package', 'synchronized',
'boolean', 'do', 'goto', 'private', 'this',
'break', 'double', 'implements', 'protected', 'throw',
'byte', 'else', 'import', 'public', 'throws',
'case', 'enum', 'instanceof', 'return', 'transient',
'catch', 'extends', 'int', 'short', 'try',
'char', 'final', 'interface', 'static', 'void',
'class', 'finally', 'long', 'strictfp', 'volatile',
'const', 'float', 'native', 'super', 'while',
]);
export function isKeyword(identifier: string): boolean {
return keywords.has(identifier);
}
const identifierRegExp: RegExp = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/;
export function isJavaIdentifier(identifier: string): boolean {
return identifierRegExp.test(identifier);
}
export interface IOption extends QuickPickItem {
value: string;
isAdvanced?: boolean;
}

Просмотреть файл

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { commands, Disposable, ExtensionContext, QuickInputButton, QuickPick, QuickPickItem, TextEdit, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { TextEdit, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import * as protocolConverter from 'vscode-languageclient/lib/protocolConverter';
import { JavaTestRunnerDelegateCommands } from '../constants';
import { executeJavaLanguageServerCommand } from '../utils/commandUtils';
@ -20,147 +20,6 @@ export async function generateTests(uri: Uri, cursorOffset: number): Promise<voi
}
}
export async function registerAskForChoiceCommand(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand('_java.test.askClientForChoice', async (placeHolder: string, choices: IOption[], canPickMany: boolean) => {
const ans: any = await window.showQuickPick(choices, {
placeHolder,
canPickMany,
ignoreFocusOut: true,
});
if (!ans) {
return undefined;
} else if (Array.isArray(ans)) {
return ans.map((a: IOption) => a.value || a.label);
}
return ans.value || ans.label;
}));
}
export async function registerAdvanceAskForChoice(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand('_java.test.advancedAskClientForChoice', async (placeHolder: string, choices: IOption[], advancedAction: string, canPickMany: boolean) => {
let result: string[] | undefined;
const disposables: Disposable[] = [];
try {
result = await new Promise<string[] | undefined>((resolve: (value: string[] | undefined) => void) => {
const quickPick: QuickPick<IOption> = window.createQuickPick<IOption>();
// if all the items are advanced item, show them directly
let showAdvancedItem: boolean = choices.filter((c: IOption) => {
return !c.isAdvanced;
}).length === 0;
quickPick.title = placeHolder;
quickPick.placeholder = placeHolder;
quickPick.items = filterOptions(showAdvancedItem, choices);
quickPick.buttons = getActionButtons(showAdvancedItem, advancedAction);
quickPick.canSelectMany = canPickMany;
quickPick.ignoreFocusOut = true;
disposables.push(quickPick.onDidTriggerButton((btn: QuickInputButton) => {
if (btn.tooltip?.endsWith(advancedAction)) {
showAdvancedItem = !showAdvancedItem;
quickPick.items = filterOptions(showAdvancedItem, choices);
quickPick.buttons = getActionButtons(showAdvancedItem, advancedAction);
}
}));
disposables.push(quickPick.onDidHide(() => {
return resolve(undefined);
}));
disposables.push(quickPick.onDidAccept(() => {
return resolve(quickPick.selectedItems.map((o: IOption) => o.value));
}));
disposables.push(quickPick);
quickPick.show();
});
} finally {
for (const d of disposables) {
d.dispose();
}
}
return result;
}));
function filterOptions(showAdvancedItem: boolean, choices: IOption[]): IOption[] {
return choices.filter((c: IOption) => {
return !c.isAdvanced || showAdvancedItem && c.isAdvanced;
});
}
function getActionButtons(showAdvancedItem: boolean, advancedAction: string): QuickInputButton[] {
if (showAdvancedItem) {
return [{
iconPath: new ThemeIcon('collapse-all'),
tooltip: `Hide ${advancedAction}`,
}];
}
return [{
iconPath: new ThemeIcon('expand-all'),
tooltip: `Show ${advancedAction}`,
}];
}
}
/**
* A command that server side can call to get a input value.
* Currently it's only used to get a Java qualified name, so the check is on the client side.
* @param context
*/
export async function registerAskForInputCommand(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand('_java.test.askClientForInput', async (prompt: string, value: string) => {
const ans: string | undefined = await window.showInputBox({
value,
prompt,
validateInput: checkJavaQualifiedName,
});
return ans;
}));
}
async function askServerToGenerateTests(uri: Uri, cursorOffset: number): Promise<any> {
return await executeJavaLanguageServerCommand<any>(JavaTestRunnerDelegateCommands.GENERATE_TESTS, uri.toString(), cursorOffset);
}
function checkJavaQualifiedName(value: string): string {
if (!value || !value.trim()) {
return 'Input cannot be empty.';
}
for (const part of value.split('.')) {
if (isKeyword(part)) {
return `Keyword '${part}' cannot be used.`;
}
if (!isJavaIdentifier(part)) {
return `Invalid Java qualified name.`;
}
}
return '';
}
// Copied from https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-Keyword
const keywords: Set<string> = new Set([
'abstract', 'continue', 'for', 'new', 'switch',
'assert', 'default', 'if', 'package', 'synchronized',
'boolean', 'do', 'goto', 'private', 'this',
'break', 'double', 'implements', 'protected', 'throw',
'byte', 'else', 'import', 'public', 'throws',
'case', 'enum', 'instanceof', 'return', 'transient',
'catch', 'extends', 'int', 'short', 'try',
'char', 'final', 'interface', 'static', 'void',
'class', 'finally', 'long', 'strictfp', 'volatile',
'const', 'float', 'native', 'super', 'while',
]);
export function isKeyword(identifier: string): boolean {
return keywords.has(identifier);
}
const identifierRegExp: RegExp = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/;
export function isJavaIdentifier(identifier: string): boolean {
return identifierRegExp.test(identifier);
}
interface IOption extends QuickPickItem {
value: string;
isAdvanced?: boolean;
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { Uri, Position, Location } from 'vscode';
import { SymbolItemNavigation } from '../../references-view';
import { ITestNavigationItem } from './navigationCommands';
export class TestNavigationModel implements SymbolItemNavigation<ITestNavigationItem> {
nearest(): ITestNavigationItem | undefined {
return undefined
}
next(from: ITestNavigationItem): ITestNavigationItem {
return from;
}
previous(from: ITestNavigationItem): ITestNavigationItem {
return from;
}
location(item: ITestNavigationItem): Location | undefined {
return new Location(Uri.file(item.uri), new Position(0, 0));
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { ProviderResult, TreeDataProvider, TreeItem, Uri } from 'vscode';
import { ITestNavigationItem } from './navigationCommands';
export class TestNavigationTreeDataProvider implements TreeDataProvider<ITestNavigationItem> {
items: ITestNavigationItem[]
constructor(items: ITestNavigationItem[]) {
this.items = items;
}
getTreeItem(element: ITestNavigationItem): TreeItem | Thenable<TreeItem> {
const treeItem: TreeItem = new TreeItem(element.simpleName);
treeItem.resourceUri = Uri.file(element.uri);
treeItem.description = element.fullyQualifiedName;
treeItem.command = {
command: 'vscode.open',
title: 'Open Type Location',
arguments: [
Uri.parse(element.uri)
]
}
return treeItem;
}
getChildren(element?: ITestNavigationItem): ProviderResult<ITestNavigationItem[]> {
if (!element) {
return this.items;
}
return undefined;
}
}

Просмотреть файл

@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import * as path from 'path';
import { commands, extensions, Location, Range, Uri, window } from 'vscode';
import { JavaTestRunnerCommands, JavaTestRunnerDelegateCommands, VSCodeCommands } from '../../constants';
import { testSourceProvider } from '../../provider/testSourceProvider';
import { SymbolTree } from '../../references-view';
import { executeJavaLanguageServerCommand } from '../../utils/commandUtils';
import { IOption } from '../askForOptionCommands';
import { TestNavigationInput } from './testNavigationInput';
const GENERATE_TESTS: string = 'Generate tests...';
const SEARCH_TEST_FILES: string = 'Search test files...';
const REFERENCES_VIEW_EXTENSION: string = 'ms-vscode.references-view';
export async function goToTest(): Promise<void> {
if (!window.activeTextEditor) {
return;
}
const uri: Uri = window.activeTextEditor.document.uri;
if (await testSourceProvider.isOnTestSourcePath(uri)) {
return;
}
const result: ITestNavigationResult | undefined = await searchTests(uri.toString());
if (!result?.items?.length) {
window.showQuickPick([
GENERATE_TESTS,
SEARCH_TEST_FILES,
], {
placeHolder: 'No tests found for current source file'
}).then((choice: string | undefined) => {
if (choice === SEARCH_TEST_FILES) {
const fileName: string = path.basename(window.activeTextEditor!.document.fileName);
commands.executeCommand(VSCodeCommands.WORKBENCH_ACTION_QUICK_OPEN, fileName.substring(0, fileName.lastIndexOf('.')));
} else if (choice === GENERATE_TESTS) {
commands.executeCommand(JavaTestRunnerCommands.JAVA_TEST_GENERATE_TESTS, uri, 0);
}
});
} else if (result.items.length === 1) {
window.showTextDocument(Uri.parse(result.items[0].uri));
} else {
const sortedResults: ITestNavigationItem[] = result.items.sort((a: ITestNavigationItem, b: ITestNavigationItem) => {
if (a.outOfBelongingProject && !b.outOfBelongingProject) {
return Number.MAX_SAFE_INTEGER;
} else if (!a.outOfBelongingProject && b.outOfBelongingProject) {
return Number.MIN_SAFE_INTEGER;
} else {
if (a.relevance === b.relevance) {
return a.simpleName.localeCompare(b.simpleName);
}
return a.relevance - b.relevance;
}
});
const api: SymbolTree | undefined = await extensions.getExtension<SymbolTree>(REFERENCES_VIEW_EXTENSION)?.activate();
if (api) {
const input: TestNavigationInput = new TestNavigationInput(
'Related Tests',
new Location(uri, new Range(
result.location.range.start.line,
result.location.range.start.character,
result.location.range.end.line,
result.location.range.end.line,
)),
sortedResults
);
api.setInput(input);
} else {
goToTestFallback(sortedResults);
}
}
}
async function goToTestFallback(results: ITestNavigationItem[]): Promise<void> {
const items: IOption[] = results.map((r: ITestNavigationItem) => {
return {
label: r.simpleName,
detail: r.fullyQualifiedName,
value: r.uri,
isAdvanced: r.outOfBelongingProject,
};
});
const choice: string[] | undefined = await commands.executeCommand(
JavaTestRunnerCommands.ADVANCED_ASK_CLIENT_FOR_CHOICE,
'Choose a test class to open',
items,
'tests in other projects',
false,
);
if (choice?.length) {
window.showTextDocument(Uri.parse(choice[0]));
}
}
async function searchTests(uri: string): Promise<ITestNavigationResult | undefined> {
return await executeJavaLanguageServerCommand<ITestNavigationResult | undefined>(
JavaTestRunnerDelegateCommands.NAVIGATE_TO_TEST_OR_TARGET, uri, true);
}
export interface ITestNavigationResult {
items: ITestNavigationItem[];
location: Location;
}
export interface ITestNavigationItem {
simpleName: string;
fullyQualifiedName: string;
uri: string;
relevance: number;
outOfBelongingProject: boolean;
}

Просмотреть файл

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { Location, ProviderResult } from 'vscode';
import { SymbolTreeInput, SymbolTreeModel } from '../../references-view';
import { ITestNavigationItem } from './navigationCommands';
import { TestNavigationTreeDataProvider } from './TestNavigationTreeDataProvider';
export class TestNavigationInput implements SymbolTreeInput<ITestNavigationItem> {
readonly title: string;
readonly location: Location;
readonly contextValue: string = 'javaTestNavigation';
items: ITestNavigationItem[];
constructor(title: string, location: Location, items: ITestNavigationItem[]) {
this.title = title;
this.location = location;
this.items = items;
}
resolve(): ProviderResult<SymbolTreeModel<ITestNavigationItem>> {
const provider: TestNavigationTreeDataProvider = new TestNavigationTreeDataProvider(this.items);
const treeModel: SymbolTreeModel<ITestNavigationItem> = {
message: undefined,
provider,
};
return treeModel;
}
with(location: Location): SymbolTreeInput<ITestNavigationItem> {
return new TestNavigationInput(this.title, location, this.items);
}
}

Просмотреть файл

@ -15,6 +15,7 @@ export namespace JavaTestRunnerDelegateCommands {
export const FIND_DIRECT_CHILDREN_FOR_CLASS: string = 'vscode.java.test.findDirectTestChildrenForClass';
export const FIND_TEST_TYPES_AND_METHODS: string = 'vscode.java.test.findTestTypesAndMethods';
export const RESOLVE_PATH: string = 'vscode.java.test.resolvePath';
export const NAVIGATE_TO_TEST_OR_TARGET: string = 'vscode.java.test.navigateToTestOrTarget';
}
export namespace JavaTestRunnerCommands {
@ -27,12 +28,17 @@ export namespace JavaTestRunnerCommands {
export const REFRESH_TEST_EXPLORER: string = 'java.test.refreshExplorer';
export const JAVA_TEST_GENERATE_TESTS: string = 'java.test.generateTests';
export const FIND_TEST_LOCATION: string = 'vscode.java.test.findTestLocation';
export const GO_TO_TEST: string = 'java.test.goToTest';
export const JAVA_TEST_OPEN_STACKTRACE: string = '_java.test.openStackTrace';
export const ASK_CLIENT_FOR_CHOICE: string = '_java.test.askClientForChoice';
export const ASK_CLIENT_FOR_INPUT: string = '_java.test.askClientForInput'
export const ADVANCED_ASK_CLIENT_FOR_CHOICE: string = '_java.test.advancedAskClientForChoice';
}
export namespace VSCodeCommands {
export const RUN_TESTS_IN_CURRENT_FILE: string = 'testing.runCurrentFile';
export const DEBUG_TESTS_IN_CURRENT_FILE: string = 'testing.debugCurrentFile';
export const WORKBENCH_ACTION_QUICK_OPEN: string = 'workbench.action.quickOpen';
}
export namespace Configurations {

Просмотреть файл

@ -4,7 +4,8 @@
import * as path from 'path';
import { commands, DebugConfiguration, Event, Extension, ExtensionContext, extensions, TestItem, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, workspace, WorkspaceFoldersChangeEvent } from 'vscode';
import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand } from 'vscode-extension-telemetry-wrapper';
import { generateTests, registerAdvanceAskForChoice, registerAskForChoiceCommand, registerAskForInputCommand } from './commands/generationCommands';
import { goToTest } from './commands/navigation/navigationCommands';
import { generateTests } from './commands/generationCommands';
import { runTestsFromJavaProjectExplorer } from './commands/projectExplorerCommands';
import { refresh, runTestsFromTestExplorer } from './commands/testExplorerCommands';
import { openStackTrace } from './commands/testReportCommands';
@ -15,6 +16,7 @@ import { IProgressProvider } from './debugger.api';
import { initExpService } from './experimentationService';
import { disposeCodeActionProvider, registerTestCodeActionProvider } from './provider/codeActionProvider';
import { testSourceProvider } from './provider/testSourceProvider';
import { registerAskForChoiceCommand, registerAdvanceAskForChoice, registerAskForInputCommand } from './commands/askForOptionCommands';
export let extensionContext: ExtensionContext;
@ -98,6 +100,7 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
instrumentOperationAsVsCodeCommand(JavaTestRunnerCommands.REFRESH_TEST_EXPLORER, async () => await refresh()),
instrumentOperationAsVsCodeCommand(JavaTestRunnerCommands.RUN_TEST_FROM_JAVA_PROJECT_EXPLORER, async (node: any) => await runTestsFromJavaProjectExplorer(node, false /* isDebug */)),
instrumentOperationAsVsCodeCommand(JavaTestRunnerCommands.DEBUG_TEST_FROM_JAVA_PROJECT_EXPLORER, async (node: any) => await runTestsFromJavaProjectExplorer(node, true /* isDebug */)),
instrumentOperationAsVsCodeCommand(JavaTestRunnerCommands.GO_TO_TEST, async () => await goToTest()),
window.onDidChangeActiveTextEditor(async (e: TextEditor | undefined) => {
if (await isTestJavaFile(e?.document)) {
await updateItemForDocumentWithDebounce(e!.document.uri);

139
src/references-view.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
/**
* This interface describes the shape for the references viewlet API. It consists
* of a single `setInput` function which must be called with a full implementation
* of the `SymbolTreeInput`-interface. To acquire this API use the default mechanics, e.g:
*
* ```ts
* // get references viewlet API
* const api = await vscode.extensions.getExtension<SymbolTree>('ms-vscode.references-view').activate();
*
* // instantiate and set input which updates the view
* const myInput: SymbolTreeInput<MyItems> = ...
* api.setInput(myInput)
* ```
*/
export interface SymbolTree {
/**
* Set the contents of the references viewlet.
*
* @param input A symbol tree input object
*/
setInput(input: SymbolTreeInput<unknown>): void;
}
/**
* A symbol tree input is the entry point for populating the references viewlet.
* Inputs must be anchored at a code location, they must have a title, and they
* must resolve to a model.
*/
export interface SymbolTreeInput<T> {
/**
* The value of the `reference-list.source` context key. Use this to control
* input dependent commands.
*/
readonly contextValue: string;
/**
* The (short) title of this input, like "Implementations" or "Callers Of"
*/
readonly title: string;
/**
* The location at which this position is anchored. Locations are validated and inputs
* with "funny" locations might be ignored
*/
readonly location: vscode.Location;
/**
* Resolve this input to a model that contains the actual data. When there are no result
* than `undefined` or `null` should be returned.
*/
resolve(): vscode.ProviderResult<SymbolTreeModel<T>>;
/**
* This function is called when re-running from history. The symbols tree has tracked
* the original location of this input and that is now passed to this input. The
* implementation of this function should return a clone where the `location`-property
* uses the provided `location`
*
* @param location The location at which the new input should be anchored.
* @returns A new input which location is anchored at the position.
*/
with(location: vscode.Location): SymbolTreeInput<T>;
}
/**
* A symbol tree model which is used to populate the symbols tree.
*/
export interface SymbolTreeModel<T> {
/**
* A tree data provider which is used to populate the symbols tree.
*/
provider: vscode.TreeDataProvider<T>;
/**
* An optional message that is displayed above the tree. Whenever the provider
* fires a change event this message is read again.
*/
message: string | undefined;
/**
* Optional support for symbol navigation. When implemented, navigation commands like
* "Go to Next" and "Go to Previous" will be working with this model.
*/
navigation?: SymbolItemNavigation<T>;
/**
* Optional support for editor highlights. WHen implemented, the editor will highlight
* symbol ranges in the source code.
*/
highlights?: SymbolItemEditorHighlights<T>;
/**
* Optional dispose function which is invoked when this model is
* needed anymore
*/
dispose?(): void;
}
/**
* Interface to support the built-in symbol navigation.
*/
export interface SymbolItemNavigation<T> {
/**
* Return the item that is the nearest to the given location or `undefined`
*/
nearest(uri: vscode.Uri, position: vscode.Position): T | undefined;
/**
* Return the next item from the given item or the item itself.
*/
next(from: T): T;
/**
* Return the previous item from the given item or the item itself.
*/
previous(from: T): T;
/**
* Return the location of the given item.
*/
location(item: T): vscode.Location | undefined;
}
/**
* Interface to support the built-in editor highlights.
*/
export interface SymbolItemEditorHighlights<T> {
/**
* Given an item and an uri return an array of ranges to highlight.
*/
getEditorHighlights(item: T, uri: vscode.Uri): vscode.Range[] | undefined;
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
'use strict';
import assert = require('assert');
import * as path from 'path';
import { Uri, window } from 'vscode';
import { ITestNavigationResult } from '../../src/commands/navigation/navigationCommands';
import { JavaTestRunnerDelegateCommands } from '../../src/constants';
import { executeJavaLanguageServerCommand } from '../../src/utils/commandUtils';
import { setupTestEnv } from "./utils";
// tslint:disable: only-arrow-functions
// tslint:disable: no-object-literal-type-assertion
const PROJECT_PATH: string = path.join(__dirname, '../../..', 'test', 'test-projects','junit');
suite('Test Navigation Tests', () => {
suiteSetup(async function() {
await setupTestEnv();
});
test('test go to test', async () => {
const filePath: string = path.join(PROJECT_PATH, 'src', 'main', 'java', 'junit', 'App.java');
await window.showTextDocument(Uri.file(filePath));
const uri: Uri = window.activeTextEditor!.document.uri;
const searchResult = await executeJavaLanguageServerCommand<ITestNavigationResult>(
JavaTestRunnerDelegateCommands.NAVIGATE_TO_TEST_OR_TARGET, uri.toString(), true);
assert.strictEqual(searchResult?.items.length, 1);
assert.strictEqual(searchResult?.items[0].simpleName, 'AppTest');
assert.strictEqual(searchResult?.items[0].fullyQualifiedName, 'junit5.AppTest');
});
});

Просмотреть файл

@ -0,0 +1,10 @@
package junit5;
import org.junit.jupiter.api.Test;
public class AppTest {
@Test
void testGetGreeting() {
}
}