Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: workspace codelens #1075

Merged
merged 12 commits into from
May 28, 2021
14 changes: 12 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@
{
"version": "0.2.0",
"configurations": [
{
"args": [
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}/dist/apps/vscode"
],
"name": "Launch Extension",
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"request": "launch",
"type": "pwa-extensionHost"
},
{
"name": "Run Extension In Dev Mode",
"type": "extensionHost",
"type": "pwa-extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}/dist/apps/vscode"
],
"trace": "false",
"trace": false,
"internalConsoleOptions": "openOnFirstSessionStart",
"outFiles": [
"${workspaceFolder}/dist/apps/vscode",
Expand Down
48 changes: 46 additions & 2 deletions apps/vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { existsSync } from 'fs';
import { dirname, join, parse } from 'path';
import {
commands,
ConfigurationChangeEvent,
Disposable,
ExtensionContext,
FileSystemWatcher,
languages,
tasks,
TreeView,
Uri,
Expand Down Expand Up @@ -33,7 +36,7 @@ import {
RunTargetTreeItem,
RunTargetTreeProvider,
} from '@nx-console/vscode/nx-run-target-view';
import { verifyNodeModules, verifyWorkspace } from '@nx-console/vscode/verify';
import { verifyNodeModules } from '@nx-console/vscode/verify';
import {
NxCommandsTreeItem,
NxCommandsTreeProvider,
Expand All @@ -42,6 +45,10 @@ import {
NxProjectTreeItem,
NxProjectTreeProvider,
} from '@nx-console/vscode/nx-project-view';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved verifyWorkspace from from the verify lib to the nx-workspace lib to resolve a circular dependency between the two.

import {
verifyWorkspace,
WorkspaceCodeLensProvider,
} from '@nx-console/vscode/nx-workspace';
import { environment } from './environments/environment';

let runTargetTreeView: TreeView<RunTargetTreeItem>;
Expand Down Expand Up @@ -99,7 +106,7 @@ export function activate(c: ExtensionContext) {
);

const manuallySelectWorkspaceDefinitionCommand = commands.registerCommand(
LOCATE_YOUR_WORKSPACE.command!.command,
LOCATE_YOUR_WORKSPACE.command?.command || '',
async () => {
return manuallySelectWorkspaceDefinition();
}
Expand All @@ -117,6 +124,9 @@ export function activate(c: ExtensionContext) {
manuallySelectWorkspaceDefinitionCommand
);

registerWorkspaceCodeLensProvider(context);
watchWorkspaceCodeLensConfigChange(context);

getTelemetry().extensionActivated((Date.now() - startTime) / 1000);
} catch (e) {
window.showErrorMessage(
Expand Down Expand Up @@ -259,3 +269,37 @@ function registerWorkspaceFileWatcher(

context.subscriptions.push(workspaceFileWatcher);
}

let codeLensProvider: Disposable | null;
function registerWorkspaceCodeLensProvider(context: ExtensionContext) {
if (GlobalConfigurationStore.instance.get('enableWorkspaceConfigCodeLens')) {
codeLensProvider = languages.registerCodeLensProvider(
{ pattern: '**/{workspace,angular}.json' },
new WorkspaceCodeLensProvider()
);
context.subscriptions.push(codeLensProvider);
}
}

function watchWorkspaceCodeLensConfigChange(context: ExtensionContext) {
context.subscriptions.push(
workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
// if the `nxConsole` config changes, check enableWorkspaceConfigCodeLens and register or dispose
if (
event.affectsConfiguration(
GlobalConfigurationStore.configurationSection
)
) {
const enableWorkspaceConfigCodeLens = GlobalConfigurationStore.instance.get(
'enableWorkspaceConfigCodeLens'
);
if (enableWorkspaceConfigCodeLens && !codeLensProvider) {
registerWorkspaceCodeLensProvider(context);
} else if (!enableWorkspaceConfigCodeLens && codeLensProvider) {
codeLensProvider.dispose();
codeLensProvider = null;
}
}
})
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This looks like it works properly 😄

Do you think this logic should be handled in the workspace lib? Like have a main entry to that lib, and then do all this logic in there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Can't keep throwing everything in main/activate.

}
5 changes: 5 additions & 0 deletions apps/vscode/src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,11 @@
"type": "boolean",
"default": true,
"description": "Shows or hides Nx Generate ui option from the file explorer context menu."
},
"nxConsole.enableWorkspaceConfigCodeLens": {
"type": "boolean",
"default": true,
"description": "Shows or hides CodeLens for running targets from the Nx workspace config file."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions libs/vscode/configuration/src/lib/configuration-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const GLOBAL_CONFIG_KEYS = [
'enableTelemetry',
'useNVM',
'enableGenerateFromContextMenu',
'enableWorkspaceConfigCodeLens',
] as const;

/**
Expand Down
2 changes: 2 additions & 0 deletions libs/vscode/nx-workspace/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './lib/find-workspace-json-target';
export * from './lib/reveal-workspace-json';
export * from './lib/workspace-codelens-provider';
export * from './lib/verify-workspace';
93 changes: 93 additions & 0 deletions libs/vscode/nx-workspace/src/lib/find-workspace-json-target.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TextDocument } from 'vscode';
import { JSONVisitor, visit } from 'jsonc-parser';
import * as typescript from 'typescript';

export function findWorkspaceJsonTarget(
document: TextDocument,
Expand Down Expand Up @@ -61,3 +62,95 @@ export function findWorkspaceJsonTarget(

return scriptOffset;
}

export interface ProjectLocations {
[projectName: string]: ProjectTargetLocation;
}
export interface ProjectTargetLocation {
[target: string]: {
position: number;
configurations?: ProjectTargetLocation;
};
}

export function getProjectLocations(document: TextDocument) {
const projectLocations: ProjectLocations = {};
const json = typescript.parseJsonText('workspace.json', document.getText());
const statement = json.statements[0];
const projects = getProperties(statement.expression)?.find(
(property) => getPropertyName(property) === 'projects'
) as typescript.PropertyAssignment | undefined;

if (!projects) {
return projectLocations;
}

getProperties(projects.initializer)?.forEach((project) => {
const projectName = getPropertyName(project);
if (!projectName) {
return;
}
projectLocations[projectName] =
getPositions(project, ['architect', 'targets'], json) ?? {};
});

return projectLocations;
}

function getProperties(
objectLiteral: typescript.Node
): typescript.NodeArray<typescript.ObjectLiteralElementLike> | undefined {
if (typescript.isObjectLiteralExpression(objectLiteral)) {
return objectLiteral.properties;
} else if (typescript.isPropertyAssignment(objectLiteral)) {
return getProperties(objectLiteral.initializer);
}
}

function getPropertyName(property: typescript.ObjectLiteralElementLike) {
if (
typescript.isPropertyAssignment(property) &&
typescript.isStringLiteral(property.name)
) {
return property.name.text;
}
}

function getPositions(
property: typescript.Node,
properties: string[],
document: typescript.JsonSourceFile
): ProjectTargetLocation | undefined {
const objectLike = getProperties(property)?.find((prop) => {
const propName = getPropertyName(prop);
return properties.some((value) => propName === value);
});

if (!objectLike) {
return undefined;
}

return getProperties(objectLike)?.reduce<ProjectTargetLocation>(
(acc, prop) => {
const propName = getPropertyName(prop);

if (!propName) {
return acc;
}

acc[propName] = {
position: prop.getStart(document),
};

// get configuration positions
const configs = getPositions(prop, ['configurations'], document);

if (configs) {
acc[propName].configurations = configs;
}

return acc;
},
{}
);
}
89 changes: 89 additions & 0 deletions libs/vscode/nx-workspace/src/lib/workspace-codelens-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { CodeLens, CodeLensProvider, Command, Range } from 'vscode';
import { TextDocument } from 'vscode';
import { verifyWorkspace } from './verify-workspace';
import { getProjectLocations } from './find-workspace-json-target';

export class ProjectCodeLens extends CodeLens {
constructor(
range: Range,
public workspaceType: 'nx'|'ng',
public project: string,
public target: string,
public configuration?: string
) {
super(range);
}
}
export class WorkspaceCodeLensProvider implements CodeLensProvider {

/**
* Provides a CodeLens set for a matched document
* @param document a document matched by the pattern passed to registerCodeLensProvider
* @returns ProjectCodeLens Range locations and properties for the document
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took out async on this function because it was no longer returning a Promise

provideCodeLenses(document: TextDocument): CodeLens[] | undefined {
const lens: CodeLens[] = [];

const projectLocations = getProjectLocations(document);
const { validWorkspaceJson, workspaceType } = verifyWorkspace();
if (!validWorkspaceJson) {
return;
}

for (const projectName in projectLocations) {
const project = projectLocations[projectName];
for (const target in project) {
const position = document.positionAt(project[target].position);

lens.push(
new ProjectCodeLens(
new Range(position, position),
workspaceType,
projectName,
target
)
);
const configurations = project[target].configurations;
if (configurations) {
for (const configuration in configurations) {
const configurationPosition = document.positionAt(
configurations[configuration].position
);

lens.push(
new ProjectCodeLens(
new Range(configurationPosition, configurationPosition),
workspaceType,
projectName,
target,
configuration
)
);
}
}
}
}
return lens;
}

/**
* Resolves and sets the command on visible CodeLens
* @param lens lens to be resolve
* @returns ProjectCodeLens with command
*/
// https://github.com/microsoft/vscode-extension-samples/blob/main/codelens-sample/src/CodelensProvider.ts
resolveCodeLens(lens: CodeLens): CodeLens | Promise<CodeLens> | null {
if (lens instanceof ProjectCodeLens) {
const command: Command = {
command: 'nx.run',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to change to 'ng.run' for angular workspaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't actually implement ng.run 😬 because I didn't realize at the time we'd also be able to use that with angular cli workspaces as well. I should add it and use it here as well though; won't be too much. Good point.

title: lens.configuration
? `${lens.workspaceType} run ${lens.project}:${lens.target}:${lens.configuration}`
: `${lens.workspaceType} run ${lens.project}:${lens.target}`,
arguments: [lens.project, lens.target, lens.configuration],
};
lens.command = command;
return lens;
}
return null;
}
}
Loading