Skip to content

Commit

Permalink
Watch the workspace changes at client (#1244)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdneo authored Jul 29, 2021
1 parent 21cf7f3 commit f453aa8
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 23 deletions.
1 change: 1 addition & 0 deletions java-extension/com.microsoft.java.test.plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<command id="vscode.java.test.findTestPackagesAndTypes" />
<command id="vscode.java.test.findDirectTestChildrenForClass" />
<command id="vscode.java.test.findTestTypesAndMethods" />
<command id="vscode.java.test.resolvePath" />
</delegateCommandHandler>
</extension>
</plugin>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class TestDelegateCommandHandler implements IDelegateCommandHandler {
private static final String FIND_PACKAGES_AND_TYPES = "vscode.java.test.findTestPackagesAndTypes";
private static final String FIND_DIRECT_CHILDREN_FOR_CLASS = "vscode.java.test.findDirectTestChildrenForClass";
private static final String FIND_TYPES_AND_METHODS = "vscode.java.test.findTestTypesAndMethods";
private static final String RESOLVE_PATH = "vscode.java.test.resolvePath";

@Override
public Object executeCommand(String commandId, List<Object> arguments, IProgressMonitor monitor) throws Exception {
Expand All @@ -54,6 +55,8 @@ public Object executeCommand(String commandId, List<Object> arguments, IProgress
return TestSearchUtils.findDirectTestChildrenForClass(arguments, monitor);
case FIND_TYPES_AND_METHODS:
return TestSearchUtils.findTestTypesAndMethods(arguments, monitor);
case RESOLVE_PATH:
return TestSearchUtils.resolvePath(arguments, monitor);
default:
throw new UnsupportedOperationException(
String.format("Java test plugin doesn't support the command '%s'.", commandId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,38 @@ private static void findTestItemsInTypeBinding(ITypeBinding typeBinding, JavaTes
for (final ITypeBinding childTypeBinding : typeBinding.getDeclaredTypes()) {
findTestItemsInTypeBinding(childTypeBinding, classItem, searchers, monitor);
}
}

/**
* Given a file Uri, resolve its belonging project and package node in the test explorer, this is
* used to find the test item node when a test file is changed
* @param arguments A list containing a uri of the file
* @param monitor Progress monitor
* @throws JavaModelException
*/
public static List<JavaTestItem> resolvePath(List<Object> arguments, IProgressMonitor monitor)
throws JavaModelException {
final List<JavaTestItem> result = new LinkedList<>();
final String uriString = (String) arguments.get(0);
if (JavaCore.isJavaLikeFileName(uriString)) {
final ICompilationUnit unit = JDTUtils.resolveCompilationUnit(uriString);
if (unit == null) {
return Collections.emptyList();
}
final IJavaProject project = unit.getJavaProject();
if (project == null) {
return Collections.emptyList();
}
result.add(TestItemUtils.constructJavaTestItem(project, TestLevel.PROJECT, TestKind.None));

final IPackageFragment packageFragment = (IPackageFragment) unit.getParent();
if (packageFragment == null || !(packageFragment instanceof IPackageFragment)) {
return Collections.emptyList();
}
result.add(TestItemUtils.constructJavaTestItem(packageFragment, TestLevel.PACKAGE, TestKind.None));
}

return result;
}

public static ASTNode parseToAst(final ICompilationUnit unit, final boolean fromCache,
Expand Down
71 changes: 68 additions & 3 deletions src/controller/testController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the MIT license.

import * as _ from 'lodash';
import { CancellationToken, TestController, TestItem, tests } from 'vscode';
import { CancellationToken, FileSystemWatcher, RelativePattern, TestController, TestItem, tests, Uri, workspace } from 'vscode';
import { instrumentOperation, sendError } from 'vscode-extension-telemetry-wrapper';
import { isStandardServerReady } from '../extension';
import { extensionContext, isStandardServerReady } from '../extension';
import { testSourceProvider } from '../provider/testSourceProvider';
import { IJavaTestItem, TestLevel } from '../types';
import { dataCache, ITestItemData } from './testItemDataCache';
import { findDirectTestChildrenForClass, findTestPackagesAndTypes, loadJavaProjects, synchronizeItemsRecursively } from './utils';
import { findDirectTestChildrenForClass, findTestPackagesAndTypes, findTestTypesAndMethods, loadJavaProjects, resolvePath, synchronizeItemsRecursively } from './utils';

export let testController: TestController | undefined;

Expand All @@ -21,6 +22,8 @@ export function createTestController(): void {
testController.resolveHandler = async (item: TestItem) => {
await loadChildren(item);
};

startWatchingWorkspace();
}

export const loadChildren: (item: TestItem, token?: CancellationToken) => any = instrumentOperation('java.test.explorer.loadChildren', async (_operationId: string, item: TestItem, token?: CancellationToken) => {
Expand All @@ -47,3 +50,65 @@ export const loadChildren: (item: TestItem, token?: CancellationToken) => any =
synchronizeItemsRecursively(item, testMethods);
}
});

async function startWatchingWorkspace(): Promise<void> {
if (!workspace.workspaceFolders) {
return;
}

for (const workspaceFolder of workspace.workspaceFolders) {
const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern(workspaceFolder);
for (const pattern of patterns) {
const watcher: FileSystemWatcher = workspace.createFileSystemWatcher(pattern);
extensionContext.subscriptions.push(
watcher,
watcher.onDidCreate(async (uri: Uri) => {
const testTypes: IJavaTestItem[] = await findTestTypesAndMethods(uri.toString());
if (testTypes.length === 0) {
return;
}
// todo: await updateNodeForDocumentWithDebounce(uri, testTypes);
}),
watcher.onDidChange(async (uri: Uri) => {
// todo: await updateNodeForDocumentWithDebounce(uri);
}),
watcher.onDidDelete(async (uri: Uri) => {
const pathsData: IJavaTestItem[] = await resolvePath(uri.toString());
if (_.isEmpty(pathsData) || pathsData.length < 2) {
return;
}

const projectData: IJavaTestItem = pathsData[0];
if (projectData.testLevel !== TestLevel.Project) {
return;
}

const belongingProject: TestItem | undefined = testController?.items.get(projectData.id);
if (!belongingProject) {
return;
}

const packageData: IJavaTestItem = pathsData[1];
if (packageData.testLevel !== TestLevel.Package) {
return;
}

const belongingPackage: TestItem | undefined = belongingProject.children.get(packageData.id);
if (!belongingPackage) {
return;
}

belongingPackage.children.forEach((item: TestItem) => {
if (item.uri?.toString() === uri.toString()) {
belongingPackage.children.delete(item.id);
}
});

if (belongingPackage.children.size === 0) {
belongingProject.children.delete(belongingPackage.id);
}
}),
);
}
}
}
10 changes: 10 additions & 0 deletions src/controller/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,13 @@ export async function findDirectTestChildrenForClass(handlerId: string, token?:
return await executeJavaLanguageServerCommand<IJavaTestItem[]>(
JavaTestRunnerDelegateCommands.FIND_DIRECT_CHILDREN_FOR_CLASS, handlerId, token) || [];
}

export async function findTestTypesAndMethods(uri: string, token?: CancellationToken): Promise<IJavaTestItem[]> {
return await executeJavaLanguageServerCommand<IJavaTestItem[]>(
JavaTestRunnerDelegateCommands.FIND_TEST_TYPES_AND_METHODS, uri, token) || [];
}

export async function resolvePath(uri: string): Promise<IJavaTestItem[]> {
return await executeJavaLanguageServerCommand<IJavaTestItem[]>(
JavaTestRunnerDelegateCommands.RESOLVE_PATH, uri) || [];
}
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createTestController, testController } from './controller/testControlle
import { IProgressProvider } from './debugger.api';
import { initExpService } from './experimentationService';
import { disposeCodeActionProvider, registerTestCodeActionProvider } from './provider/codeActionProvider';
import { testSourceProvider } from './provider/testSourceProvider';

export let extensionContext: ExtensionContext;

Expand Down Expand Up @@ -39,6 +40,7 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
if (extensionApi.onDidClasspathUpdate) {
const onDidClasspathUpdate: Event<Uri> = extensionApi.onDidClasspathUpdate;
context.subscriptions.push(onDidClasspathUpdate(async () => {
testSourceProvider.clear();
commands.executeCommand(VSCodeCommands.REFRESH_TESTS);
}));
}
Expand All @@ -52,6 +54,7 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
serverMode = mode;
// Only re-initialize the component when its lightweight -> standard
if (mode === LanguageServerMode.Standard) {
testSourceProvider.clear();
registerTestCodeActionProvider();
createTestController();
}
Expand All @@ -61,6 +64,7 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom
if (extensionApi.onDidProjectsImport) {
const onDidProjectsImport: Event<Uri[]> = extensionApi.onDidProjectsImport;
context.subscriptions.push(onDidProjectsImport(async () => {
testSourceProvider.clear();
commands.executeCommand(VSCodeCommands.REFRESH_TESTS);
}));
}
Expand Down
65 changes: 45 additions & 20 deletions src/provider/testSourceProvider.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as path from 'path';
import { RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode';
import { getTestSourcePaths } from '../utils/commandUtils';
import { JavaTestRunnerDelegateCommands } from '../constants';
import { executeJavaLanguageServerCommand } from '../utils/commandUtils';

class TestSourcePathProvider {
private testSource: ITestSourcePath[];
private testSourceMapping: Map<Uri, ITestSourcePath[]> = new Map();

public async initialize(): Promise<void> {
this.testSource = [];
if (!workspace.workspaceFolders) {
return;
}

this.testSource = await getTestSourcePaths(workspace.workspaceFolders.map((workspaceFolder: WorkspaceFolder) => workspaceFolder.uri.toString()));
}

public async getTestSourcePattern(containsGeneral: boolean = true): Promise<RelativePattern[]> {
public async getTestSourcePattern(workspaceFolder: WorkspaceFolder, containsGeneral: boolean = true): Promise<RelativePattern[]> {
const patterns: RelativePattern[] = [];
const sourcePaths: string[] = await testSourceProvider.getTestSourcePath(containsGeneral);
const sourcePaths: string[] = await testSourceProvider.getTestSourcePath(workspaceFolder, containsGeneral);
for (const sourcePath of sourcePaths) {
const normalizedPath: string = Uri.file(sourcePath).fsPath;
const pattern: RelativePattern = new RelativePattern(normalizedPath, '**/*.java');
Expand All @@ -27,21 +20,53 @@ class TestSourcePathProvider {
return patterns;
}

public async getTestSourcePath(containsGeneral: boolean = true): Promise<string[]> {
if (this.testSource === undefined) {
await this.initialize();
}
public async getTestSourcePath(workspaceFolder: WorkspaceFolder, containsGeneral: boolean = true): Promise<string[]> {
const testPaths: ITestSourcePath[] = await this.getTestPaths(workspaceFolder);

if (containsGeneral) {
return this.testSource.map((s: ITestSourcePath) => s.testSourcePath);
return testPaths.map((s: ITestSourcePath) => s.testSourcePath);
}

return this.testSource.filter((s: ITestSourcePath) => s.isStrict)
return testPaths.filter((s: ITestSourcePath) => s.isStrict)
.map((s: ITestSourcePath) => s.testSourcePath);
}

public async isOnTestSourcePath(uri: Uri): Promise<boolean> {
const workspaceFolder: WorkspaceFolder | undefined = workspace.getWorkspaceFolder(uri);
if (!workspaceFolder) {
return false;
}
const testPaths: ITestSourcePath[] = await this.getTestPaths(workspaceFolder);
const fsPath: string = uri.fsPath;
for (const testPath of testPaths) {
const relativePath: string = path.relative(testPath.testSourcePath, fsPath);
if (!relativePath.startsWith('..')) {
return true;
}
}
return false;
}

public clear(): void {
this.testSourceMapping.clear();
}

private async getTestPaths(workspaceFolder: WorkspaceFolder): Promise<ITestSourcePath[]> {
let testPaths: ITestSourcePath[] | undefined = this.testSourceMapping.get(workspaceFolder.uri);
if (!testPaths) {
testPaths = await getTestSourcePaths([workspaceFolder.uri.toString()]);
this.testSourceMapping.set(workspaceFolder.uri, testPaths);
}
return testPaths;
}
}

async function getTestSourcePaths(uri: string[]): Promise<ITestSourcePath[]> {
return await executeJavaLanguageServerCommand<ITestSourcePath[]>(
JavaTestRunnerDelegateCommands.GET_TEST_SOURCE_PATH, uri) || [];
}

export interface ITestSourcePath {
interface ITestSourcePath {
testSourcePath: string;
/**
* All the source paths from eclipse and invisible project will be treated as test source
Expand Down

0 comments on commit f453aa8

Please sign in to comment.