diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d7535e..41ec5f82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Improvements: - Add option to disable kit scan by default when a kit isn't selected. [#1461](https://github.com/microsoft/vscode-cmake-tools/issues/1461) - Show cmake output when version probe fails. [#3650](https://github.com/microsoft/vscode-cmake-tools/issues/3650) - Improve various settings scopes [#3601](https://github.com/microsoft/vscode-cmake-tools/issues/3601) +- Refactor the Project Outline view to show a flat list of targets [#491](https://github.com/microsoft/vscode-cmake-tools/issues/491), [#3684](https://github.com/microsoft/vscode-cmake-tools/issues/3684) - Add the ability to pin CMake Commands to the sidebar [#2984](https://github.com/microsoft/vscode-cmake-tools/issues/2984) & [#3296](https://github.com/microsoft/vscode-cmake-tools/issues/3296) Bug Fixes: diff --git a/src/drivers/cmakeFileApi.ts b/src/drivers/cmakeFileApi.ts index 7227dd88..18b5e153 100644 --- a/src/drivers/cmakeFileApi.ts +++ b/src/drivers/cmakeFileApi.ts @@ -149,6 +149,15 @@ export namespace CodeModelKind { isGenerated?: boolean; } + export interface Dependency { + backtrace: number; + id: string; + } + + export interface Folder { + name: string; + } + export interface TargetObject { name: string; type: string; @@ -157,6 +166,9 @@ export namespace CodeModelKind { paths: PathInfo; sources: TargetSourcefile[]; compileGroups?: CompileGroup[]; + dependencies?: Dependency[]; + folder?: Folder; + isGeneratorProvided?: boolean; } } @@ -490,7 +502,10 @@ async function loadCodeModelTarget(rootPaths: CodeModelKind.PathInfo, jsonFile: a => convertToAbsolutePath(path.join(targetObject.paths.build, a.path), rootPaths.build)) : [], fileGroups, - sysroot + sysroot, + folder: targetObject.folder, + dependencies: targetObject.dependencies, + isGeneratorProvided: targetObject.isGeneratorProvided } as CodeModelTarget; } diff --git a/src/drivers/codeModel.ts b/src/drivers/codeModel.ts index 451e1517..17817c4e 100644 --- a/src/drivers/codeModel.ts +++ b/src/drivers/codeModel.ts @@ -10,7 +10,8 @@ export type CodeModelContent = api.CodeModel.Content; // TODO: Move framework definitions to the public API repo to avoid this intersection type. export type CodeModelFileGroup = api.CodeModel.FileGroup & { frameworks?: { path: string }[] }; export type CodeModelProject = api.CodeModel.Project; -export type CodeModelTarget = api.CodeModel.Target; +// TODO: If requested, move folder, dependencies, and isGeneratorProvided definition to the public API repo to avoid this intersection type. +export type CodeModelTarget = api.CodeModel.Target & { folder?: { name: string }; dependencies?: { backtrace: number; id: string }[]; isGeneratorProvided?: boolean}; export type CodeModelToolchain = api.CodeModel.Toolchain; export type TargetTypeString = api.CodeModel.TargetType; diff --git a/src/extension.ts b/src/extension.ts index 7561ba14..9878589f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,7 +31,7 @@ import rollbar from '@cmt/rollbar'; import { StateManager } from './state'; import { cmakeTaskProvider, CMakeTaskProvider } from '@cmt/cmakeTaskProvider'; import * as telemetry from '@cmt/telemetry'; -import { ProjectOutline, ProjectNode, TargetNode, SourceFileNode, WorkspaceFolderNode } from '@cmt/projectOutline'; +import { ProjectOutline, ProjectNode, TargetNode, SourceFileNode, WorkspaceFolderNode } from '@cmt/projectOutline/projectOutline'; import * as util from '@cmt/util'; import { ProgressHandle, DummyDisposable, reportProgress, runCommand } from '@cmt/util'; import { DEFAULT_VARIANTS } from '@cmt/variant'; diff --git a/src/projectOutline.ts b/src/projectOutline/projectOutline.ts similarity index 73% rename from src/projectOutline.ts rename to src/projectOutline/projectOutline.ts index 1773985d..79083922 100644 --- a/src/projectOutline.ts +++ b/src/projectOutline/projectOutline.ts @@ -5,6 +5,9 @@ import * as codeModel from '@cmt/drivers/codeModel'; import rollbar from '@cmt/rollbar'; import { lexicographicalCompare, splitPath } from '@cmt/util'; import CMakeProject from '@cmt/cmakeProject'; +import { populateViewCodeModel } from './targetsViewCodeModel'; +import { fs } from '@cmt/pr'; +import { CodeModelKind } from '@cmt/drivers/cmakeFileApi'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @@ -116,23 +119,6 @@ function iconForTargetType(type: codeModel.TargetTypeString): string { } } -function sortStringForType(type: codeModel.TargetTypeString): string { - switch (type) { - case 'EXECUTABLE': - return 'aaa'; - case 'MODULE_LIBRARY': - case 'SHARED_LIBRARY': - case 'STATIC_LIBRARY': - return 'baa'; - case 'UTILITY': - return 'caa'; - case 'OBJECT_LIBRARY': - return 'daa'; - case 'INTERFACE_LIBRARY': - return 'eaa'; - } -} - export class DirectoryNode extends BaseNode { constructor(readonly prefix: string, readonly parent: string, readonly pathPart: string) { super(`${prefix}${path.sep}${path.normalize(pathPart)}`); @@ -247,6 +233,76 @@ export class SourceFileNode extends BaseNode { } } +export class ReferencesNode extends BaseNode { + constructor(targetId: string) { + super(`${targetId}-references`); + } + + get name() { + return this.id; + } + + private _references = new Map(); + + getChildren(): BaseNode[] { + return [...this._references.values()]; + } + getTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.id); + item.id = this.id; + if (this.getChildren().length) { + item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } + item.label = localize('references', 'References'); + item.contextValue = ['nodeType=references', `compilable=${false}`, `cmakelists=${false}`].join(','); + item.iconPath = new vscode.ThemeIcon('references'); + return item; + } + getOrderTuple(): string[] { + return [this.id]; + } + + update(dependencies: CodeModelKind.Dependency[], targetId: string) { + const new_refs = new Map(); + for (const ref of dependencies) { + // filter out dependecies that are found and don't have a defined backtrace + if (ref.backtrace !== undefined) { + new_refs.set(ref.id, new ReferenceNode(ref.id, targetId)); + } + } + this._references = new_refs; + } +} + +export class ReferenceNode extends BaseNode { + constructor(id: string = "", parentTargetId: string) { + const name = id.split("::")[0]; + super(`${name}-${parentTargetId}`); + this.name = name; + } + + readonly name: string; + + getChildren(): BaseNode[] { + return []; + } + getTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.id); + item.id = this.id; + item.label = this.name; + item.contextValue = [ + "nodeType=reference", + `compilable=${false}`, + `cmakelists=${false}` + ].join(","); + item.iconPath = new vscode.ThemeIcon("references"); + return item; + } + getOrderTuple(): string[] { + return [this.id]; + } +} + export class TargetNode extends BaseNode { constructor(readonly prefix: string, readonly projectName: string, cm: codeModel.CodeModelTarget, readonly folder: vscode.WorkspaceFolder) { // id: {prefix}::target_name:artifact_name:target_path @@ -265,13 +321,14 @@ export class TargetNode extends BaseNode { private _fsPath: string = ''; getOrderTuple() { - return [sortStringForType(this._type), this.name]; + return [this.name]; } private readonly _rootDir: DirectoryNode; + private readonly _referencesNode = new ReferencesNode(this.id); getChildren() { - return this._rootDir.getChildren(); + return [this._referencesNode, ...this._rootDir.getChildren()]; } getTreeItem() { try { @@ -285,16 +342,23 @@ export class TargetNode extends BaseNode { if (this._isLaunch) { item.label += ' 🚀'; } - if (this._fullName !== this.name && this._fullName) { - item.label += ` [${this._fullName}]`; - } - if (this._type === 'INTERFACE_LIBRARY') { - item.label += ` — ${localize('interface.library', 'Interface library')}`; - } else if (this._type === 'UTILITY') { - item.label += ` — ${localize('utility', 'Utility')}`; + + if (this._type === "STATIC_LIBRARY") { + item.label += ` (${localize('static.library', 'Static library')})`; + } else if (this._type === "MODULE_LIBRARY") { + item.label += ` (${localize('module.library', 'Module library')})`; + } else if (this._type === "SHARED_LIBRARY") { + item.label += ` (${localize('shared.library', 'Shared library')})`; } else if (this._type === 'OBJECT_LIBRARY') { - item.label += ` — ${localize('object.library', 'Object library')}`; + item.label += ` (${localize('object.library', 'Object library')})`; + } else if (this._type === "EXECUTABLE") { + item.label += ` (${localize('executable', 'Executable')})`; + } else if (this._type === 'UTILITY') { + item.label += ` (${localize('utility', 'Utility')})`; + } else if (this._type === 'INTERFACE_LIBRARY') { + item.label += ` (${localize('interface.library', 'Interface library')})`; } + item.resourceUri = vscode.Uri.file(this._fsPath); item.tooltip = localize('target.tooltip', 'Target {0}', this.name); if (this._isLaunch) { @@ -355,13 +419,15 @@ export class TargetNode extends BaseNode { }; for (const grp of cm.fileGroups || []) { - for (let src of grp.sources) { - if (!path.isAbsolute(src)) { - src = path.join(this.sourceDir, src); + if (!grp.isGenerated) { + for (let src of grp.sources) { + if (!path.isAbsolute(src)) { + src = path.join(this.sourceDir, src); + } + const src_dir = path.dirname(src); + const relpath = path.relative(this.sourceDir, src_dir); + addToTree(tree, relpath, new SourceFileNode(this.id, this.folder, this.sourceDir, src, grp.language)); } - const src_dir = path.dirname(src); - const relpath = path.relative(this.sourceDir, src_dir); - addToTree(tree, relpath, new SourceFileNode(this.id, this.folder, this.sourceDir, src, grp.language)); } } @@ -375,6 +441,8 @@ export class TargetNode extends BaseNode { update: (_src, _cm) => {}, create: newNode => newNode }); + + this._referencesNode.update(cm.dependencies || [], this.id); } async openInCMakeLists() { @@ -393,56 +461,103 @@ export class TargetNode extends BaseNode { } export class ProjectNode extends BaseNode { - constructor(readonly name: string, readonly folder: vscode.WorkspaceFolder, readonly sourceDirectory: string) { + constructor( + readonly name: string, + readonly folder: vscode.WorkspaceFolder, + readonly sourceDirectory: string + ) { // id: project_name:project_directory super(`${name}:${sourceDirectory}`); } private readonly _rootDir = new DirectoryNode(this.id, '', ''); + private sortProjectChildren(children: BaseNode[]): BaseNode[] { + return children.sort((a, b) => { + if (a instanceof TargetNode && b instanceof TargetNode) { + return lexicographicalCompare(a.getOrderTuple(), b.getOrderTuple()); + } else if (a instanceof TargetNode && b instanceof DirectoryNode) { + return -1; + } else if (a instanceof DirectoryNode && b instanceof TargetNode) { + return 1; + } + + return 0; + }); + } + getOrderTuple() { return [this.sourceDirectory, this.name]; } getChildren() { - return this._rootDir.getChildren(); + const children: BaseNode[] = this.sortProjectChildren(this._rootDir.getChildren()); + + const cmakelists = new SourceFileNode(this.id, this.folder, this.sourceDirectory, path.join(this.sourceDirectory, 'CMakeLists.txt')); + children.push(cmakelists); + + const possiblePreset = path.join(this.sourceDirectory, 'CMakePresets.json'); + if (fs.existsSync(possiblePreset)) { + children.push(new SourceFileNode(this.id, this.folder, this.sourceDirectory, possiblePreset)); + } + + return children; } getTreeItem() { - const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Expanded); + const item = new vscode.TreeItem( + this.name, + vscode.TreeItemCollapsibleState.Expanded + ); if (this.getChildren().length === 0) { - item.label += ` — (${localize('empty.project', 'Empty project')})`; + item.label += ` — (${localize("empty.project", "Empty project")})`; } item.tooltip = `${this.name}\n${this.sourceDirectory}`; - item.contextValue = 'nodeType=project'; + item.contextValue = "nodeType=project"; return item; } update(pr: codeModel.CodeModelProject, ctx: TreeUpdateContext) { + // TODO: Update. We need to + if (pr.name !== this.name) { - rollbar.error(localize('update.project.with.mismatch', 'Update project with mismatching name property'), { newName: pr.name, oldName: this.name }); + rollbar.error( + localize( + "update.project.with.mismatch", + "Update project with mismatching name property" + ), + { newName: pr.name, oldName: this.name } + ); } const tree: PathedTree = { pathPart: '', - children: [], - items: [] + items: [], + children: [] }; - for (const target of pr.targets) { - const srcdir = target.sourceDirectory || ''; - const relpath = path.relative(pr.sourceDirectory, srcdir); - addToTree(tree, relpath, target); + for (const t of pr.targets) { + const target = t as codeModel.CodeModelTarget; + + // Skip targets that the generator auto-created. + if (target.isGeneratorProvided) { + continue; + } + + if (target.folder) { + addToTree(tree, target.folder.name, target); + } else { + addToTree(tree, '', target); + } } - collapseTreeInplace(tree); this._rootDir.update({ tree, context: ctx, update: (tgt, cm) => tgt.update(cm, ctx), - create: cm => { - const node = new TargetNode(this.id, this.name, cm, this.folder); - node.update(cm, ctx); + create: newTgt => { + const node = new TargetNode(this.id, this.name, newTgt, this.folder); + node.update(newTgt, ctx); return node; } }); @@ -507,14 +622,20 @@ export class WorkspaceFolderNode extends BaseNode { return; } - for (const modelProj of model.configurations[0].projects) { - let item = this.getNode(cmakeProject, modelProj.name); - if (!item) { - item = new ProjectNode(modelProj.name, this.wsFolder, cmakeProject.folderPath); - this.setNode(cmakeProject, modelProj.name, item); - } - item.update(modelProj, ctx); + if (model.configurations[0].projects.length === 0) { + this.removeNodes(cmakeProject); + ctx.nodesToUpdate.push(this); + return; } + + const projectOutlineModel = populateViewCodeModel(model); + const rootProject = projectOutlineModel.project; + let item = this.getNode(cmakeProject, rootProject.name); + if (!item) { + item = new ProjectNode(rootProject.name, this.wsFolder, cmakeProject.folderPath); + this.setNode(cmakeProject, rootProject.name, item); + } + item.update(rootProject, ctx); } getChildren() { diff --git a/src/projectOutline/targetsViewCodeModel.ts b/src/projectOutline/targetsViewCodeModel.ts new file mode 100644 index 00000000..e7f32ecc --- /dev/null +++ b/src/projectOutline/targetsViewCodeModel.ts @@ -0,0 +1,35 @@ +import { CodeModelContent } from "@cmt/drivers/codeModel"; +import { CodeModel } from "vscode-cmake-tools"; + +interface ProjectOutlineCodeModel { + project: CodeModel.Project; +} + +/** + * Construct and populate the view model for the Project Outline view. + * We are constructing a flat list of all of the targets in the project. + * @param model The code model from the CMake FileAPI. + */ +export function populateViewCodeModel(model: CodeModelContent): ProjectOutlineCodeModel { + const configuration = model.configurations[0]; + + // The first project in the list is the root project. + const originalProject = configuration.projects[0]; + + // Flatten the list of targets into a single list. + const targets: CodeModel.Target[] = []; + for (const projects of configuration.projects) { + for (const t of projects.targets) { + targets.push(t); + } + } + + // Construct the new project object. Everything will be the same except for the newly constructed flat list of targets. + const project: CodeModel.Project = { + name: originalProject.name, + targets: targets, + sourceDirectory: originalProject.sourceDirectory, + hasInstallRule: originalProject.hasInstallRule + }; + return { project }; +} diff --git a/tsconfig.json b/tsconfig.json index 15347637..f14f0f7b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "experimentalDecorators": true, "noUnusedParameters": true, "noImplicitThis": true, - "skipLibCheck": true + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true }, "exclude": [ "node_modules",