Project Outline update (#3695)
* move code, switch to flat targets list, lots still TODO * more efficient update * update to match VS more, still need to do references * fix getTreeItem for ReferenceNode * fix id * loc * fix collapse/expand, and no backtrace * avoid adding isGeneratorProvided targets * update changelog * remove unused
This commit is contained in:
Родитель
548be9ae0b
Коммит
6b6c07bb90
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Node extends BaseNode> 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<string, ReferenceNode>();
|
||||
|
||||
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<string, ReferenceNode>();
|
||||
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<SourceFileNode>;
|
||||
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<TargetNode>(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<codeModel.CodeModelTarget> = {
|
||||
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() {
|
|
@ -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 };
|
||||
}
|
|
@ -17,7 +17,8 @@
|
|||
"experimentalDecorators": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitThis": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
Загрузка…
Ссылка в новой задаче