From d94aa70971ec074f62e8ac5d862e1c6295ac8dca Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:45:07 +0100 Subject: [PATCH 1/8] fix(nxls): provide autocomplete only for plugins that contain nx (#2365) --- .../src/lib/inference-plugins-completion.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/language-server/capabilities/code-completion/src/lib/inference-plugins-completion.ts b/libs/language-server/capabilities/code-completion/src/lib/inference-plugins-completion.ts index 8cc5d975ac..dc47d3c033 100644 --- a/libs/language-server/capabilities/code-completion/src/lib/inference-plugins-completion.ts +++ b/libs/language-server/capabilities/code-completion/src/lib/inference-plugins-completion.ts @@ -33,9 +33,11 @@ export async function inferencePluginsCompletion( .split('node_modules/') .pop(); - inferencePluginsCompletion.push({ - label: `${dependencyPath}/plugin`, - }); + if (dependencyPath?.includes('nx')) { + inferencePluginsCompletion.push({ + label: `${dependencyPath}/plugin`, + }); + } } } From 0648d527166ce32efdbcc142d5e7537025cea968 Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:35:58 +0100 Subject: [PATCH 2/8] fix(intellij): use refresh service directly and avoid going through actionPerformed (#2366) --- .../dev/nx/console/nx_toolwindow/NxToolWindowPanel.kt | 8 ++++++-- .../project_details/ProjectDetailsEditorWithPreview.kt | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/intellij/src/main/kotlin/dev/nx/console/nx_toolwindow/NxToolWindowPanel.kt b/apps/intellij/src/main/kotlin/dev/nx/console/nx_toolwindow/NxToolWindowPanel.kt index 2cb56b1666..73eb255660 100644 --- a/apps/intellij/src/main/kotlin/dev/nx/console/nx_toolwindow/NxToolWindowPanel.kt +++ b/apps/intellij/src/main/kotlin/dev/nx/console/nx_toolwindow/NxToolWindowPanel.kt @@ -25,7 +25,6 @@ import com.intellij.util.messages.Topic import com.intellij.util.ui.JBUI import dev.nx.console.nx_toolwindow.tree.NxProjectsTree import dev.nx.console.nx_toolwindow.tree.NxTreeStructure -import dev.nx.console.nxls.NxRefreshWorkspaceAction import dev.nx.console.nxls.NxRefreshWorkspaceService import dev.nx.console.nxls.NxWorkspaceRefreshListener import dev.nx.console.nxls.NxlsService @@ -326,7 +325,12 @@ class NxToolWindowPanel(private val project: Project) : SimpleToolWindowPanel(tr } override fun actionPerformed(e: AnActionEvent) { - NxRefreshWorkspaceAction().actionPerformed(e) + TelemetryService.getInstance(project) + .featureUsed( + TelemetryEvent.MISC_REFRESH_WORKSPACE, + mapOf("source" to TelemetryEventSource.PROJECTS_VIEW), + ) + NxRefreshWorkspaceService.getInstance(project).refreshWorkspace() } } diff --git a/apps/intellij/src/main/kotlin/dev/nx/console/project_details/ProjectDetailsEditorWithPreview.kt b/apps/intellij/src/main/kotlin/dev/nx/console/project_details/ProjectDetailsEditorWithPreview.kt index 986584e009..815a843c67 100644 --- a/apps/intellij/src/main/kotlin/dev/nx/console/project_details/ProjectDetailsEditorWithPreview.kt +++ b/apps/intellij/src/main/kotlin/dev/nx/console/project_details/ProjectDetailsEditorWithPreview.kt @@ -11,7 +11,7 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import dev.nx.console.models.NxVersion -import dev.nx.console.nxls.NxRefreshWorkspaceAction +import dev.nx.console.nxls.NxRefreshWorkspaceService import dev.nx.console.telemetry.TelemetryEvent import dev.nx.console.telemetry.TelemetryEventSource import dev.nx.console.telemetry.TelemetryService @@ -59,7 +59,7 @@ class ProjectDetailsEditorWithPreview(private val project: Project, file: Virtua TelemetryEvent.MISC_REFRESH_WORKSPACE, mapOf("source" to TelemetryEventSource.EDITOR_TOOLBAR), ) - NxRefreshWorkspaceAction().actionPerformed(e) + NxRefreshWorkspaceService.getInstance(project).refreshWorkspace() } } From ce5c889d197dfdb8843a77232f1b052f634e7cf4 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Mon, 16 Dec 2024 14:33:01 +0000 Subject: [PATCH 3/8] chore(repo): upgrade agent setup steps version (#2367) --- .nx/workflows/agents.yaml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.nx/workflows/agents.yaml b/.nx/workflows/agents.yaml index 488f10dca4..3bd89a5a2a 100644 --- a/.nx/workflows/agents.yaml +++ b/.nx/workflows/agents.yaml @@ -4,24 +4,24 @@ launch-templates: image: 'windows-2022' init-steps: - name: Checkout - uses: 'nrwl/nx-cloud-workflows/v3.6/workflow-steps/checkout/main.yaml' + uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/checkout/main.yaml' - name: Restore Node Modules Cache - uses: 'nrwl/nx-cloud-workflows/v3.6/workflow-steps/cache/main.yaml' - env: - KEY: 'package-lock.json|yarn.lock|pnpm-lock.yaml' - PATHS: 'node_modules' - BASE_BRANCH: 'main' + uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/cache/main.yaml' + inputs: + key: 'package-lock.json|yarn.lock|pnpm-lock.yaml' + paths: 'node_modules' + base-branch: 'main' - name: Restore Browser Binary Cache - uses: 'nrwl/nx-cloud-workflows/v3.6/workflow-steps/cache/main.yaml' - env: - KEY: 'package-lock.json|yarn.lock|pnpm-lock.yaml|"browsers"' - PATHS: | + uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/cache/main.yaml' + inputs: + key: 'package-lock.json|yarn.lock|pnpm-lock.yaml|"browsers"' + paths: | '../.cache/Cypress' '../.cache/ms-playwright' - BASE_BRANCH: 'main' + base-branch: 'main' - name: Install Node Modules - uses: 'nrwl/nx-cloud-workflows/v3.6/workflow-steps/install-node-modules/main.yaml' + uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/install-node-modules/main.yaml' - name: Install Browsers (if needed) - uses: 'nrwl/nx-cloud-workflows/v3.6/workflow-steps/install-browsers/main.yaml' + uses: 'nrwl/nx-cloud-workflows/v4/workflow-steps/install-browsers/main.yaml' - name: Install cypress with --force script: npx cypress install --force From 6a18b6814a057b89719f268c463f70d08973a0ce Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:31:06 +0100 Subject: [PATCH 4/8] feat(nxls): add namedInputs target links & fix namedInputs completion in nx.json (#2368) --- ...amed-input-link-completion-default.test.ts | 174 ++++++++++++++++++ .../src/lib/get-completion-items.ts | 4 + .../src/lib/get-document-links.ts | 9 + .../src/lib/named-input-link.ts | 80 ++++++++ .../document-links/src/lib/target-link.ts | 5 +- .../json-schema/src/lib/common-json-schema.ts | 5 +- .../json-schema/src/lib/nx-json-schema.ts | 3 +- 7 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 apps/nxls-e2e/src/document-links/named-input-link-completion-default.test.ts create mode 100644 libs/language-server/capabilities/document-links/src/lib/named-input-link.ts diff --git a/apps/nxls-e2e/src/document-links/named-input-link-completion-default.test.ts b/apps/nxls-e2e/src/document-links/named-input-link-completion-default.test.ts new file mode 100644 index 0000000000..66cfc0ea7e --- /dev/null +++ b/apps/nxls-e2e/src/document-links/named-input-link-completion-default.test.ts @@ -0,0 +1,174 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { Position } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; +import { NxlsWrapper } from '../nxls-wrapper'; +import { e2eCwd, modifyJsonFile, newWorkspace, uniq } from '../utils'; + +let nxlsWrapper: NxlsWrapper; +const workspaceName = uniq('workspace'); + +const projectJsonPath = join( + e2eCwd, + workspaceName, + 'apps', + workspaceName, + 'project.json' +); + +describe('namedInput link completion - default', () => { + beforeAll(async () => { + newWorkspace({ + name: workspaceName, + options: { + preset: 'next', + }, + }); + nxlsWrapper = new NxlsWrapper(true); + await nxlsWrapper.startNxls(join(e2eCwd, workspaceName)); + + nxlsWrapper.sendNotification({ + method: 'textDocument/didOpen', + params: { + textDocument: { + uri: URI.file(projectJsonPath).toString(), + languageId: 'JSON', + version: 1, + text: readFileSync(projectJsonPath, 'utf-8'), + }, + }, + }); + }); + afterAll(async () => { + await nxlsWrapper.stopNxls(); + }); + describe('named input links', () => { + it('should return correct target link for input if it is a namedInput in nx.json', async () => { + modifyJsonFile(projectJsonPath, (data) => ({ + ...data, + targets: { + build: { + inputs: ['default'], + }, + }, + })); + + nxlsWrapper.sendNotification({ + method: 'textDocument/didChange', + params: { + textDocument: { + uri: URI.file(projectJsonPath).toString(), + languageId: 'JSON', + version: 2, + }, + contentChanges: [ + { + text: readFileSync(projectJsonPath, 'utf-8'), + }, + ], + }, + }); + + const linkResponse = await nxlsWrapper.sendRequest({ + method: 'textDocument/documentLink', + params: { + textDocument: { + uri: URI.file(projectJsonPath).toString(), + }, + position: Position.create(0, 1), + }, + }); + + const defaultLine = + readFileSync(join(e2eCwd, workspaceName, 'nx.json'), 'utf-8') + .split('\n') + .findIndex((line) => line.includes('"default":')) + 1; + + const targetLink = (linkResponse.result as any[])[0].target; + expect(targetLink).toMatch(new RegExp(`#${defaultLine}$`)); + expect(decodeURI(targetLink)).toContain(join(workspaceName, 'nx.json')); + }); + + it('should not return target link for input if it is not a namedInput in nx.json', async () => { + modifyJsonFile(projectJsonPath, (data) => ({ + ...data, + targets: { + build: { + inputs: ['src/file.js', 'other'], + }, + }, + })); + + nxlsWrapper.sendNotification({ + method: 'textDocument/didChange', + params: { + textDocument: { + uri: URI.file(projectJsonPath).toString(), + languageId: 'JSON', + version: 2, + }, + contentChanges: [ + { + text: readFileSync(projectJsonPath, 'utf-8'), + }, + ], + }, + }); + + const linkResponse = await nxlsWrapper.sendRequest({ + method: 'textDocument/documentLink', + params: { + textDocument: { + uri: URI.file(projectJsonPath).toString(), + }, + position: Position.create(0, 1), + }, + }); + + const targetLinks = linkResponse.result as any[]; + expect(targetLinks.length).toBe(0); + }); + + it('should return correct target link for named input within nx.json', async () => { + const nxJsonPath = join(e2eCwd, workspaceName, 'nx.json'); + modifyJsonFile(nxJsonPath, (data) => ({ + ...data, + namedInputs: { + default: ['one', 'two'], + one: ['src/file.js'], + }, + })); + + nxlsWrapper.sendNotification({ + method: 'textDocument/didOpen', + params: { + textDocument: { + uri: URI.file(nxJsonPath).toString(), + languageId: 'JSON', + version: 0, + text: readFileSync(nxJsonPath, 'utf-8'), + }, + }, + }); + + const linkResponse = await nxlsWrapper.sendRequest({ + method: 'textDocument/documentLink', + params: { + textDocument: { + uri: URI.file(nxJsonPath).toString(), + }, + position: Position.create(0, 1), + }, + }); + + const oneLine = + readFileSync(nxJsonPath, 'utf-8') + .split('\n') + .findIndex((line) => line.includes('"one": [')) + 1; + + const targetLink = (linkResponse.result as any[])[0].target; + expect(targetLink).toMatch(new RegExp(`#${oneLine}$`)); + expect(decodeURI(targetLink)).toContain(join(workspaceName, 'nx.json')); + }); + }); +}); diff --git a/libs/language-server/capabilities/code-completion/src/lib/get-completion-items.ts b/libs/language-server/capabilities/code-completion/src/lib/get-completion-items.ts index be2614aff3..eae3a300c5 100644 --- a/libs/language-server/capabilities/code-completion/src/lib/get-completion-items.ts +++ b/libs/language-server/capabilities/code-completion/src/lib/get-completion-items.ts @@ -1,6 +1,7 @@ import { getDefaultCompletionType, isArrayNode, + lspLogger, } from '@nx-console/language-server/utils'; import { CompletionType, @@ -132,12 +133,15 @@ function completionItems( return targetsCompletion(workingPath, node, document, true); } case CompletionType.inputName: { + lspLogger.log(`inputName completion ${node.value}`); return inputNameCompletion(workingPath, node, document); } case CompletionType.inputNameWithDeps: { + lspLogger.log(`inputNameWithDeps completion ${node.value}`); return inputNameCompletion(workingPath, node, document, true); } case CompletionType.inferencePlugins: { + lspLogger.log(`inferencePlugins completion ${node.value}`); return inferencePluginsCompletion(workingPath); } default: { diff --git a/libs/language-server/capabilities/document-links/src/lib/get-document-links.ts b/libs/language-server/capabilities/document-links/src/lib/get-document-links.ts index 0e0b9c0246..89ea4ee8a2 100644 --- a/libs/language-server/capabilities/document-links/src/lib/get-document-links.ts +++ b/libs/language-server/capabilities/document-links/src/lib/get-document-links.ts @@ -19,6 +19,7 @@ import { } from 'vscode-json-languageservice'; import { createRange } from './create-range'; import { targetLink } from './target-link'; +import { namedInputLink } from './named-input-link'; export async function getDocumentLinks( workingPath: string | undefined, @@ -78,6 +79,14 @@ export async function getDocumentLinks( } break; } + case CompletionType.inputName: + case CompletionType.inputNameWithDeps: { + const link = await namedInputLink(workingPath, node); + if (link) { + links.push(DocumentLink.create(range, link)); + } + break; + } case 'projectTarget': { const link = await targetLink(workingPath, node); if (link) { diff --git a/libs/language-server/capabilities/document-links/src/lib/named-input-link.ts b/libs/language-server/capabilities/document-links/src/lib/named-input-link.ts new file mode 100644 index 0000000000..00f802449b --- /dev/null +++ b/libs/language-server/capabilities/document-links/src/lib/named-input-link.ts @@ -0,0 +1,80 @@ +import { + findProperty, + getLanguageModelCache, +} from '@nx-console/language-server/utils'; +import { readNxJson } from '@nx-console/shared/npm'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { + ASTNode, + JSONDocument, + Range, + TextDocument, +} from 'vscode-json-languageservice'; +import { URI } from 'vscode-uri'; +import { createRange } from './create-range'; + +let versionNumber = 0; + +export async function namedInputLink( + workingPath: string, + node: ASTNode +): Promise { + const nxJson = await readNxJson(workingPath); + + const namedInput = Object.keys(nxJson.namedInputs ?? {}).find( + (input) => input === node.value + ); + + if (!namedInput) { + return; + } + + const nxJsonPath = join(workingPath, 'nx.json'); + + const nxJsonContent = readFileSync(join(workingPath, 'nx.json'), 'utf8'); + + const languageModelCache = getLanguageModelCache(); + const { document, jsonAst } = languageModelCache.retrieve( + TextDocument.create(nxJsonPath, 'json', versionNumber, nxJsonContent), + false + ); + languageModelCache.dispose(); + versionNumber++; + + const range = findNamedInputRange(document, jsonAst, namedInput); + + if (!range) { + return; + } + + return URI.from({ + scheme: 'file', + path: nxJsonPath, + fragment: `${range.start.line + 1}`, + }).toString(); +} + +function findNamedInputRange( + document: TextDocument, + jsonAst: JSONDocument, + namedInput: string +): Range | undefined { + if (!jsonAst.root) { + return; + } + + const namedInputNode = findProperty(jsonAst.root, 'namedInputs'); + + if (!namedInputNode) { + return; + } + + const namedInputProperty = findProperty(namedInputNode, namedInput); + + if (!namedInputProperty) { + return; + } + + return createRange(document, namedInputProperty); +} diff --git a/libs/language-server/capabilities/document-links/src/lib/target-link.ts b/libs/language-server/capabilities/document-links/src/lib/target-link.ts index 728aa66f99..5c460bde44 100644 --- a/libs/language-server/capabilities/document-links/src/lib/target-link.ts +++ b/libs/language-server/capabilities/document-links/src/lib/target-link.ts @@ -4,10 +4,7 @@ import { isStringNode, lspLogger, } from '@nx-console/language-server/utils'; -import { - getNxVersion, - nxWorkspace, -} from '@nx-console/language-server/workspace'; +import { nxWorkspace } from '@nx-console/language-server/workspace'; import { fileExists, readFile } from '@nx-console/shared/file-system'; import { parseTargetString } from '@nx-console/shared/utils'; import { join } from 'path'; diff --git a/libs/shared/json-schema/src/lib/common-json-schema.ts b/libs/shared/json-schema/src/lib/common-json-schema.ts index 6c6128937c..d0b7ede4de 100644 --- a/libs/shared/json-schema/src/lib/common-json-schema.ts +++ b/libs/shared/json-schema/src/lib/common-json-schema.ts @@ -60,7 +60,10 @@ export const inputs = (nxVersion: NxVersion): JSONSchema[] => [ export const namedInputs = (nxVersion: NxVersion): JSONSchema => ({ type: 'object', additionalProperties: { - oneOf: inputs(nxVersion), + type: 'array', + items: { + oneOf: inputs(nxVersion), + }, }, }); diff --git a/libs/shared/json-schema/src/lib/nx-json-schema.ts b/libs/shared/json-schema/src/lib/nx-json-schema.ts index 068a40cec2..8d7421bea5 100644 --- a/libs/shared/json-schema/src/lib/nx-json-schema.ts +++ b/libs/shared/json-schema/src/lib/nx-json-schema.ts @@ -4,7 +4,7 @@ import type { ProjectGraphProjectNode, } from 'nx/src/devkit-exports'; import type { JSONSchema } from 'vscode-json-languageservice'; -import { targets } from './common-json-schema'; +import { namedInputs, targets } from './common-json-schema'; import { CompletionType } from './completion-type'; import { createBuildersAndExecutorsSchema } from './create-builders-and-executors-schema'; import { NxVersion } from '@nx-console/shared/nx-version'; @@ -105,6 +105,7 @@ function createJsonSchema( }, }, }, + namedInputs: namedInputs(nxVersion), }, }; } From 68fccde92133475e264b61ccbe67f8314f3046f3 Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:46:39 +0100 Subject: [PATCH 5/8] feat(vscode): add atomizer codelenses (#2370) --- .../lib/atomized-file-codelens-provider.ts | 173 ++++++++++++++++++ .../src/lib/config-file-codelens-provider.ts | 2 +- .../src/lib/init-vscode-project-details.ts | 2 + 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts diff --git a/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts new file mode 100644 index 0000000000..b75d60f0ee --- /dev/null +++ b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts @@ -0,0 +1,173 @@ +import { getNxWorkspacePath } from '@nx-console/vscode/configuration'; +import { getNxWorkspace } from '@nx-console/vscode/nx-workspace'; +import { getOutputChannel } from '@nx-console/vscode/output-channels'; +import { + NxCodeLensProvider, + registerCodeLensProvider, +} from '@nx-console/vscode/utils'; +import { join } from 'path'; +import { + CallExpression, + createSourceFile, + ExpressionStatement, + Identifier, + isImportDeclaration, + Node, + ScriptTarget, + SyntaxKind, +} from 'typescript'; +import { + CancellationToken, + CodeLens, + Event, + EventEmitter, + ExtensionContext, + ProviderResult, + Range, + TextDocument, +} from 'vscode'; +import { CODELENS_RUN_TARGET_COMMAND } from './config-file-codelens-provider'; + +export class AtomizedFileCodelensProvider implements NxCodeLensProvider { + constructor( + public workspaceRoot: string, + public sourceFilesToAtomizedTargetsMap: Record< + string, + [project: string, target: string] + > + ) {} + CODELENS_PATTERN = { + scheme: 'file', + }; + private changeEvent = new EventEmitter(); + + public get onDidChangeCodeLenses(): Event { + return this.changeEvent.event; + } + + public refresh(): void { + this.changeEvent.fire(); + } + + provideCodeLenses( + document: TextDocument, + token: CancellationToken + ): ProviderResult { + const path = document.uri.fsPath; + const [project, target] = this.sourceFilesToAtomizedTargetsMap[path]; + if (project && target) { + const location = this.getCodeLensLocation(document); + return [ + new CodeLens(location, { + title: `$(play) Run ${project}:${target} via nx`, + command: CODELENS_RUN_TARGET_COMMAND, + arguments: [project, target], + }), + ]; + } + } + + private getCodeLensLocation(document: TextDocument): Range { + try { + if (['typescript', 'javascript'].includes(document.languageId)) { + const configFile = createSourceFile( + document.fileName, + document.getText(), + { + languageVersion: ScriptTarget.Latest, + } + ); + let firstNonImportNode: Node | undefined = undefined; + + for (const node of configFile.statements) { + if (node.kind === SyntaxKind.ExpressionStatement) { + const expr = (node as ExpressionStatement).expression; + if (expr.kind === SyntaxKind.CallExpression) { + const call = expr as CallExpression; + if ( + call.expression.kind === SyntaxKind.Identifier && + (call.expression as Identifier).text === 'describe' + ) { + const pos = document.positionAt(node.getStart(configFile)); + return new Range(pos, pos); + } + } + } + + if (!firstNonImportNode && !isImportDeclaration(node)) { + firstNonImportNode = node; + } + } + + if (firstNonImportNode) { + const pos = document.positionAt( + firstNonImportNode.getStart(configFile) + ); + return new Range(pos, pos); + } + } + return new Range(0, 0, 0, 0); + } catch (e) { + return new Range(0, 0, 0, 0); + } + } + + static async register(context: ExtensionContext) { + const workspacePath = getNxWorkspacePath(); + const sourceFilesToAtomizedTargetsMap = + await getSourceFilesToAtomizedTargetsMap(workspacePath); + + getOutputChannel().appendLine( + JSON.stringify(sourceFilesToAtomizedTargetsMap, null, 2) + ); + + const provider = new AtomizedFileCodelensProvider( + workspacePath, + sourceFilesToAtomizedTargetsMap + ); + + registerCodeLensProvider(provider); + } +} + +export async function getSourceFilesToAtomizedTargetsMap( + workspacePath: string +): Promise> { + const sourceFilesToAtomizedTargetsMap: Record< + string, + [project: string, target: string] + > = {}; + const nxWorkspace = await getNxWorkspace(); + if (!nxWorkspace) { + return {}; + } + const { projectGraph } = nxWorkspace; + + for (const projectNode of Object.values(projectGraph.nodes)) { + const targetGroups = projectNode.data.metadata?.targetGroups; + if (!targetGroups) { + continue; + } + for (const targetGroup of Object.values(targetGroups)) { + const atomizerRootTarget = targetGroup.find( + (target) => + projectNode.data.targets?.[target]?.metadata?.nonAtomizedTarget + ); + if (!atomizerRootTarget) { + continue; + } + for (const target of targetGroup) { + if (target === atomizerRootTarget) { + continue; + } + const fileName = join( + workspacePath, + projectNode.data.root, + target.replace(`${atomizerRootTarget}--`, '') + ); + sourceFilesToAtomizedTargetsMap[fileName] = [projectNode.name, target]; + } + } + } + return sourceFilesToAtomizedTargetsMap; +} diff --git a/libs/vscode/project-details/src/lib/config-file-codelens-provider.ts b/libs/vscode/project-details/src/lib/config-file-codelens-provider.ts index bf62ef28b2..56834ed78f 100644 --- a/libs/vscode/project-details/src/lib/config-file-codelens-provider.ts +++ b/libs/vscode/project-details/src/lib/config-file-codelens-provider.ts @@ -32,7 +32,7 @@ import { window, } from 'vscode'; -const CODELENS_RUN_TARGET_COMMAND = 'nxConsole.config-codelens.run'; +export const CODELENS_RUN_TARGET_COMMAND = 'nxConsole.config-codelens.run'; const CODELENS_OPEN_RUN_QUICKPICK_COMMAND = 'nxConsole.config-codelens.open-run-quickpick'; diff --git a/libs/vscode/project-details/src/lib/init-vscode-project-details.ts b/libs/vscode/project-details/src/lib/init-vscode-project-details.ts index 57a949a59d..68a439533b 100644 --- a/libs/vscode/project-details/src/lib/init-vscode-project-details.ts +++ b/libs/vscode/project-details/src/lib/init-vscode-project-details.ts @@ -26,6 +26,7 @@ import { ProjectDetailsCodelensProvider } from './project-details-codelens-provi import { ProjectDetailsManager } from './project-details-manager'; import { ProjectDetailsProvider } from './project-details-provider'; import { gte } from '@nx-console/shared/nx-version'; +import { AtomizedFileCodelensProvider } from './atomized-file-codelens-provider'; export function initVscodeProjectDetails(context: ExtensionContext) { const nxWorkspacePath = getNxWorkspacePath(); @@ -38,6 +39,7 @@ export function initVscodeProjectDetails(context: ExtensionContext) { ProjectDetailsCodelensProvider.register(context); ConfigFileCodelensProvider.register(context); + AtomizedFileCodelensProvider.register(context); } function registerCommand(context: ExtensionContext) { From 4949292c5953358ba39af8a628d2b47e52518864 Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:47:58 +0100 Subject: [PATCH 6/8] fix(vscode): subscribe to project graph updates in atomizer codelens provider (#2372) --- .../lib/atomized-file-codelens-provider.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts index b75d60f0ee..09fb1e262f 100644 --- a/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts +++ b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts @@ -1,6 +1,5 @@ import { getNxWorkspacePath } from '@nx-console/vscode/configuration'; import { getNxWorkspace } from '@nx-console/vscode/nx-workspace'; -import { getOutputChannel } from '@nx-console/vscode/output-channels'; import { NxCodeLensProvider, registerCodeLensProvider, @@ -27,6 +26,7 @@ import { TextDocument, } from 'vscode'; import { CODELENS_RUN_TARGET_COMMAND } from './config-file-codelens-provider'; +import { onWorkspaceRefreshed } from '@nx-console/vscode/lsp-client'; export class AtomizedFileCodelensProvider implements NxCodeLensProvider { constructor( @@ -117,15 +117,25 @@ export class AtomizedFileCodelensProvider implements NxCodeLensProvider { const sourceFilesToAtomizedTargetsMap = await getSourceFilesToAtomizedTargetsMap(workspacePath); - getOutputChannel().appendLine( - JSON.stringify(sourceFilesToAtomizedTargetsMap, null, 2) - ); - const provider = new AtomizedFileCodelensProvider( workspacePath, sourceFilesToAtomizedTargetsMap ); + context.subscriptions.push( + onWorkspaceRefreshed(async () => { + const updatedWorkspacePath = getNxWorkspacePath(); + const updatedSourceFilesToAtomizedTargetsMap = + await getSourceFilesToAtomizedTargetsMap(updatedWorkspacePath); + + provider.workspaceRoot = updatedWorkspacePath; + provider.sourceFilesToAtomizedTargetsMap = + updatedSourceFilesToAtomizedTargetsMap; + + provider.refresh(); + }) + ); + registerCodeLensProvider(provider); } } From b37f4dae35caae45b9a455f994a2f540c5ae39b0 Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:46:28 +0100 Subject: [PATCH 7/8] fix(vscode): handle non-atomized file paths gracefully (#2373) --- .../project-details/src/lib/atomized-file-codelens-provider.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts index 09fb1e262f..3c6efcd991 100644 --- a/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts +++ b/libs/vscode/project-details/src/lib/atomized-file-codelens-provider.ts @@ -54,6 +54,9 @@ export class AtomizedFileCodelensProvider implements NxCodeLensProvider { token: CancellationToken ): ProviderResult { const path = document.uri.fsPath; + if (!this.sourceFilesToAtomizedTargetsMap[path]) { + return []; + } const [project, target] = this.sourceFilesToAtomizedTargetsMap[path]; if (project && target) { const location = this.getCodeLensLocation(document); From 89352120a6b887edb850e1c09a3027656973b637 Mon Sep 17 00:00:00 2001 From: MaxKless <34165455+MaxKless@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:34:39 +0100 Subject: [PATCH 8/8] feat(vscode): show project graph error message in projects view (#2371) --- apps/vscode/package.json | 11 +++++ .../src/lib/init-nx-project-view.ts | 27 +++++------ .../src/lib/nx-project-tree-provider.ts | 3 +- .../nx-project-view/src/lib/nx-tree-item.ts | 10 ++++ .../lib/project-graph-error-decorations.ts | 38 +++++++++++++++ .../src/lib/views/nx-project-base-view.ts | 17 ++++++- .../src/lib/views/nx-project-list-view.ts | 17 +++++-- .../src/lib/views/nx-project-tree-view.ts | 47 ++++++++++++------- 8 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 libs/vscode/nx-project-view/src/lib/project-graph-error-decorations.ts diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 574cb19807..a7942620fb 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -212,6 +212,11 @@ "command": "nxCloud.showRunInApp", "when": "view == nxCloudRecentCIPE && viewItem == run", "group": "inline@1" + }, + { + "command": "nxConsole.showProblems", + "when": "view == nxProjects && viewItem == projectGraphError", + "group": "inline@1" } ], "editor/title": [ @@ -652,6 +657,12 @@ "title": "Open Workspace in Browser", "command": "nxCloud.openApp", "icon": "$(cloud)" + }, + { + "category": "Nx", + "title": "Show Nx Errors in Problems View", + "command": "nxConsole.showProblems", + "icon": "$(eye)" } ], "configuration": { diff --git a/libs/vscode/nx-project-view/src/lib/init-nx-project-view.ts b/libs/vscode/nx-project-view/src/lib/init-nx-project-view.ts index b22dacf379..c0c4003a09 100644 --- a/libs/vscode/nx-project-view/src/lib/init-nx-project-view.ts +++ b/libs/vscode/nx-project-view/src/lib/init-nx-project-view.ts @@ -1,19 +1,14 @@ -import { ExtensionContext, commands, window } from 'vscode'; -import { NxProjectTreeProvider } from './nx-project-tree-provider'; -import { NxTreeItem } from './nx-tree-item'; -import { getTelemetry } from '@nx-console/vscode/telemetry'; -import { revealNxProject } from '@nx-console/vscode/nx-config-decoration'; +import { showRefreshLoadingAtLocation } from '@nx-console/vscode/lsp-client'; import { selectProject } from '@nx-console/vscode/nx-cli-quickpicks'; +import { revealNxProject } from '@nx-console/vscode/nx-config-decoration'; import { getNxWorkspaceProjects } from '@nx-console/vscode/nx-workspace'; +import { getTelemetry } from '@nx-console/vscode/telemetry'; +import { ExtensionContext, commands, window } from 'vscode'; import { AtomizerDecorationProvider } from './atomizer-decorations'; -import { - getNxlsClient, - showRefreshLoadingAtLocation, -} from '@nx-console/vscode/lsp-client'; -import { - NxWorkspaceRefreshNotification, - NxWorkspaceRefreshStartedNotification, -} from '@nx-console/language-server/types'; +import { NxProjectTreeProvider } from './nx-project-tree-provider'; +import { NxTreeItem } from './nx-tree-item'; +import { ProjectGraphErrorDecorationProvider } from './project-graph-error-decorations'; +import { getOutputChannel } from '@nx-console/vscode/output-channels'; export function initNxProjectView( context: ExtensionContext @@ -32,6 +27,7 @@ export function initNxProjectView( ); AtomizerDecorationProvider.register(context); + ProjectGraphErrorDecorationProvider.register(context); context.subscriptions.push( showRefreshLoadingAtLocation({ viewId: 'nxProjects' }) @@ -52,7 +48,10 @@ export async function showProjectConfiguration(selection: NxTreeItem) { return; } const viewItem = selection.item; - if (viewItem.contextValue === 'folder') { + if ( + viewItem.contextValue === 'folder' || + viewItem.contextValue === 'projectGraphError' + ) { return; } diff --git a/libs/vscode/nx-project-view/src/lib/nx-project-tree-provider.ts b/libs/vscode/nx-project-view/src/lib/nx-project-tree-provider.ts index 3341abd979..c0ba4ef638 100644 --- a/libs/vscode/nx-project-view/src/lib/nx-project-tree-provider.ts +++ b/libs/vscode/nx-project-view/src/lib/nx-project-tree-provider.ts @@ -116,7 +116,8 @@ export class NxProjectTreeProvider extends AbstractTreeProvider { !viewItem || viewItem.contextValue === 'project' || viewItem.contextValue === 'folder' || - viewItem.contextValue === 'targetGroup' + viewItem.contextValue === 'targetGroup' || + viewItem.contextValue === 'projectGraphError' ) { // can not run a task on a project, folder or target group return; diff --git a/libs/vscode/nx-project-view/src/lib/nx-tree-item.ts b/libs/vscode/nx-project-view/src/lib/nx-tree-item.ts index c968c8be38..3e7ad54e02 100644 --- a/libs/vscode/nx-project-view/src/lib/nx-tree-item.ts +++ b/libs/vscode/nx-project-view/src/lib/nx-tree-item.ts @@ -7,6 +7,7 @@ import { TargetViewItem, } from './views/nx-project-base-view'; import { ATOMIZED_SCHEME } from './atomizer-decorations'; +import { PROJECT_GRAPH_ERROR_DECORATION_SCHEME } from './project-graph-error-decorations'; export class NxTreeItem extends TreeItem { id: string; @@ -25,6 +26,12 @@ export class NxTreeItem extends TreeItem { path: item.nxTarget.name, }); this.contextValue = 'target-atomized'; + } else if (item.contextValue === 'projectGraphError') { + this.resourceUri = Uri.from({ + scheme: PROJECT_GRAPH_ERROR_DECORATION_SCHEME, + path: item.errorCount.toString(), + }); + this.tooltip = `${item.errorCount} errors detected. The project graph may be missing some information`; } this.setIcons(); @@ -46,6 +53,9 @@ export class NxTreeItem extends TreeItem { ) { this.iconPath = new ThemeIcon('symbol-property'); } + if (this.contextValue === 'projectGraphError') { + this.iconPath = new ThemeIcon('error'); + } } public getProject(): NxProject | undefined { diff --git a/libs/vscode/nx-project-view/src/lib/project-graph-error-decorations.ts b/libs/vscode/nx-project-view/src/lib/project-graph-error-decorations.ts new file mode 100644 index 0000000000..beb8bbd4ee --- /dev/null +++ b/libs/vscode/nx-project-view/src/lib/project-graph-error-decorations.ts @@ -0,0 +1,38 @@ +import { + commands, + ExtensionContext, + FileDecoration, + FileDecorationProvider, + ProviderResult, + ThemeColor, + Uri, + window, +} from 'vscode'; + +export const PROJECT_GRAPH_ERROR_DECORATION_SCHEME = 'nx-project-graph-error'; + +export class ProjectGraphErrorDecorationProvider + implements FileDecorationProvider +{ + provideFileDecoration(uri: Uri): ProviderResult { + if (uri.scheme === PROJECT_GRAPH_ERROR_DECORATION_SCHEME) { + const errorCount = uri.path; + return { + badge: errorCount, + propagate: false, + color: new ThemeColor('errorForeground'), + }; + } + } + + static register(context: ExtensionContext) { + context.subscriptions.push( + window.registerFileDecorationProvider( + new ProjectGraphErrorDecorationProvider() + ), + commands.registerCommand('nxConsole.showProblems', () => { + commands.executeCommand('workbench.actions.view.problems'); + }) + ); + } +} diff --git a/libs/vscode/nx-project-view/src/lib/views/nx-project-base-view.ts b/libs/vscode/nx-project-view/src/lib/views/nx-project-base-view.ts index fcad838061..072299d2fa 100644 --- a/libs/vscode/nx-project-view/src/lib/views/nx-project-base-view.ts +++ b/libs/vscode/nx-project-view/src/lib/views/nx-project-base-view.ts @@ -8,7 +8,7 @@ import type { ProjectGraphProjectNode, TargetConfiguration, } from 'nx/src/devkit-exports'; -import { TreeItemCollapsibleState } from 'vscode'; +import { ThemeIcon, TreeItemCollapsibleState } from 'vscode'; interface BaseViewItem { id: string; @@ -39,6 +39,11 @@ export interface TargetGroupViewItem extends BaseViewItem<'targetGroup'> { targetGroupName: string; } +export interface ProjectGraphErrorViewItem + extends BaseViewItem<'projectGraphError'> { + errorCount: number; +} + export interface NxProject { project: string; root: string; @@ -233,6 +238,16 @@ export abstract class BaseView { ); } + createProjectGraphErrorViewItem(count: number): ProjectGraphErrorViewItem { + return { + id: 'projectGraphError', + contextValue: 'projectGraphError', + errorCount: count, + label: `Project Graph Error`, + collapsible: TreeItemCollapsibleState.None, + }; + } + protected async getProjectData() { if (this.workspaceData?.projectGraph.nodes) { return this.workspaceData.projectGraph.nodes; diff --git a/libs/vscode/nx-project-view/src/lib/views/nx-project-list-view.ts b/libs/vscode/nx-project-view/src/lib/views/nx-project-list-view.ts index f34cc2b8a3..09c87c4d56 100644 --- a/libs/vscode/nx-project-view/src/lib/views/nx-project-list-view.ts +++ b/libs/vscode/nx-project-view/src/lib/views/nx-project-list-view.ts @@ -1,6 +1,6 @@ -import { getNxWorkspaceProjects } from '@nx-console/vscode/nx-workspace'; import { BaseView, + ProjectGraphErrorViewItem, ProjectViewItem, TargetGroupViewItem, TargetViewItem, @@ -9,13 +9,21 @@ import { export type ListViewItem = | ProjectViewItem | TargetViewItem - | TargetGroupViewItem; + | TargetGroupViewItem + | ProjectGraphErrorViewItem; export class ListView extends BaseView { async getChildren(element?: ListViewItem) { if (!element) { + const items: ListViewItem[] = []; + if (this.workspaceData?.errors) { + items.push( + this.createProjectGraphErrorViewItem(this.workspaceData.errors.length) + ); + } // should return root elements if no element was passed - return this.createProjects(); + items.push(...(await this.createProjects())); + return items; } if (element.contextValue === 'project') { return this.createTargetsAndGroupsFromProject(element); @@ -23,6 +31,9 @@ export class ListView extends BaseView { if (element.contextValue === 'targetGroup') { return this.createTargetsFromTargetGroup(element); } + if (element.contextValue === 'projectGraphError') { + return []; + } return this.createConfigurationsFromTarget(element); } diff --git a/libs/vscode/nx-project-view/src/lib/views/nx-project-tree-view.ts b/libs/vscode/nx-project-view/src/lib/views/nx-project-tree-view.ts index a843d2ab74..2ee520f10c 100644 --- a/libs/vscode/nx-project-view/src/lib/views/nx-project-tree-view.ts +++ b/libs/vscode/nx-project-view/src/lib/views/nx-project-tree-view.ts @@ -6,6 +6,7 @@ import { TreeItemCollapsibleState } from 'vscode'; import { BaseView, FolderViewItem, + ProjectGraphErrorViewItem, ProjectViewItem, TargetGroupViewItem, TargetViewItem, @@ -15,7 +16,8 @@ export type TreeViewItem = | FolderViewItem | ProjectViewItem | TargetViewItem - | TargetGroupViewItem; + | TargetGroupViewItem + | ProjectGraphErrorViewItem; export type ProjectInfo = { dir: string; @@ -31,25 +33,36 @@ export class TreeView extends BaseView { element?: TreeViewItem ): Promise { if (!element) { + const items: TreeViewItem[] = []; + + if (this.workspaceData?.errors) { + items.push( + this.createProjectGraphErrorViewItem(this.workspaceData.errors.length) + ); + } + // if there's only a single root, start with it expanded const isSingleProject = this.roots.length === 1; - return this.roots - .sort((a, b) => { - // the VSCode tree view looks chaotic when folders and projects are on the same level - // so we sort the nodes to have folders first and projects after - if (!!a.projectName == !!b.projectName) { - return a.dir.localeCompare(b.dir); - } - return a.projectName ? 1 : -1; - }) - .map((root) => - this.createFolderOrProjectTreeItemFromNode( - root, - isSingleProject - ? TreeItemCollapsibleState.Expanded - : TreeItemCollapsibleState.Collapsed + items.push( + ...this.roots + .sort((a, b) => { + // the VSCode tree view looks chaotic when folders and projects are on the same level + // so we sort the nodes to have folders first and projects after + if (!!a.projectName == !!b.projectName) { + return a.dir.localeCompare(b.dir); + } + return a.projectName ? 1 : -1; + }) + .map((root) => + this.createFolderOrProjectTreeItemFromNode( + root, + isSingleProject + ? TreeItemCollapsibleState.Expanded + : TreeItemCollapsibleState.Collapsed + ) ) - ); + ); + return items; } if (element.contextValue === 'project') {