Merge branch 'main' into robertbrignull/SaveModeledMethods

This commit is contained in:
Robert
2023-10-09 11:35:47 +01:00
26 changed files with 445 additions and 236 deletions

View File

@@ -446,13 +446,20 @@
"type": "boolean",
"default": false,
"scope": "application",
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)"
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the one of the global telemetry settings (`#telemetry.enableTelemetry#` or `#telemetry.telemetryLevel#`) must be enabled for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)",
"tags": [
"telemetry",
"usesOnlineServices"
]
},
"codeQL.telemetry.logTelemetry": {
"type": "boolean",
"default": false,
"scope": "application",
"description": "Specifies whether or not to write telemetry events to the extension log."
"description": "Specifies whether or not to write telemetry events to the extension log.",
"tags": [
"telemetry"
]
}
}
}
@@ -1996,7 +2003,7 @@
"id": "codeQLMethodModeling",
"type": "webview",
"name": "CodeQL Method Modeling",
"when": "config.codeQL.canary && config.codeQL.model.methodModelingView && codeql.modelEditorOpen && !codeql.modelEditorActive"
"when": "config.codeQL.canary"
}
],
"codeql-methods-usage": [

View File

@@ -323,6 +323,7 @@ export type PackagingCommands = {
export type ModelEditorCommands = {
"codeQL.openModelEditor": () => Promise<void>;
"codeQL.openModelEditorFromModelingPanel": () => Promise<void>;
"codeQLModelEditor.jumpToUsageLocation": (
method: Method,
usage: Usage,

View File

@@ -19,7 +19,10 @@ import { ErrorLike } from "../common/errors";
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
import { Method, Usage } from "../model-editor/method";
import { ModeledMethod } from "../model-editor/modeled-method";
import { ModelEditorViewState } from "../model-editor/shared/view-state";
import {
MethodModelingPanelViewState,
ModelEditorViewState,
} from "../model-editor/shared/view-state";
import { Mode } from "../model-editor/shared/mode";
import { QueryLanguage } from "./query-language";
@@ -576,9 +579,14 @@ interface SetModeledMethodMessage {
method: ModeledMethod;
}
interface SetInModelingModeMessage {
t: "setInModelingMode";
inModelingMode: boolean;
}
interface RevealMethodMessage {
t: "revealMethod";
method: Method;
methodSignature: string;
}
export type ToModelEditorMessage =
@@ -609,10 +617,20 @@ interface RevealInEditorMessage {
method: Method;
}
interface StartModelingMessage {
t: "startModeling";
}
export type FromMethodModelingMessage =
| CommonFromViewMessages
| SetModeledMethodMessage
| RevealInEditorMessage;
| RevealInEditorMessage
| StartModelingMessage;
interface SetMethodModelingPanelViewStateMessage {
t: "setMethodModelingPanelViewState";
viewState: MethodModelingPanelViewState;
}
interface SetMethodMessage {
t: "setMethod";
@@ -632,7 +650,9 @@ interface SetSelectedMethodMessage {
}
export type ToMethodModelingMessage =
| SetMethodModelingPanelViewStateMessage
| SetMethodMessage
| SetModeledMethodMessage
| SetMethodModifiedMessage
| SetSelectedMethodMessage;
| SetSelectedMethodMessage
| SetInModelingModeMessage;

View File

@@ -40,10 +40,7 @@ export const PACKS_BY_QUERY_LANGUAGE = {
],
[QueryLanguage.Go]: ["codeql/go-queries"],
[QueryLanguage.Java]: ["codeql/java-queries"],
[QueryLanguage.Javascript]: [
"codeql/javascript-queries",
"codeql/javascript-experimental-atm-queries",
],
[QueryLanguage.Javascript]: ["codeql/javascript-queries"],
[QueryLanguage.Python]: ["codeql/python-queries"],
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
};

View File

@@ -13,7 +13,7 @@ export abstract class AbstractWebviewViewProvider<
private disposables: Disposable[] = [];
constructor(
private readonly app: App,
protected readonly app: App,
private readonly webviewKind: WebviewKind,
) {}

View File

@@ -3,13 +3,13 @@ import {
Extension,
ExtensionContext,
ConfigurationChangeEvent,
env,
} from "vscode";
import TelemetryReporter from "vscode-extension-telemetry";
import {
ConfigListener,
CANARY_FEATURES,
ENABLE_TELEMETRY,
GLOBAL_ENABLE_TELEMETRY,
LOG_TELEMETRY,
isIntegrationTestMode,
isCanary,
@@ -59,8 +59,6 @@ export class ExtensionTelemetryListener
extends ConfigListener
implements AppTelemetry
{
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
private reporter?: TelemetryReporter;
private cliVersionStr = NOT_SET_CLI_VERSION;
@@ -72,6 +70,10 @@ export class ExtensionTelemetryListener
private readonly ctx: ExtensionContext,
) {
super();
env.onDidChangeTelemetryEnabled(async () => {
await this.initialize();
});
}
/**
@@ -91,10 +93,7 @@ export class ExtensionTelemetryListener
async handleDidChangeConfiguration(
e: ConfigurationChangeEvent,
): Promise<void> {
if (
e.affectsConfiguration("codeQL.telemetry.enableTelemetry") ||
e.affectsConfiguration("telemetry.enableTelemetry")
) {
if (e.affectsConfiguration(ENABLE_TELEMETRY.qualifiedName)) {
await this.initialize();
}
@@ -102,7 +101,7 @@ export class ExtensionTelemetryListener
// Re-request if codeQL.canary is being set to `true` and telemetry
// is not currently enabled.
if (
e.affectsConfiguration("codeQL.canary") &&
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) &&
CANARY_FEATURES.getValue() &&
!ENABLE_TELEMETRY.getValue()
) {
@@ -212,7 +211,7 @@ export class ExtensionTelemetryListener
properties.stack = error.stack;
}
this.reporter.sendTelemetryEvent("error", properties, {});
this.reporter.sendTelemetryErrorEvent("error", properties, {});
}
/**
@@ -224,7 +223,7 @@ export class ExtensionTelemetryListener
// if global telemetry is disabled, avoid showing the dialog or making any changes
let result = undefined;
if (
GLOBAL_ENABLE_TELEMETRY.getValue() &&
env.isTelemetryEnabled &&
// Avoid showing the dialog if we are in integration test mode.
!isIntegrationTestMode()
) {

View File

@@ -72,15 +72,8 @@ export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
const ROOT_SETTING = new Setting("codeQL");
// Global configuration
// Telemetry configuration
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
"contextualQueries",
ROOT_SETTING,
);
const GLOBAL_TELEMETRY_SETTING = new Setting("telemetry");
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
export const ENABLE_TELEMETRY = new Setting(
@@ -88,11 +81,6 @@ export const ENABLE_TELEMETRY = new Setting(
TELEMETRY_SETTING,
);
export const GLOBAL_ENABLE_TELEMETRY = new Setting(
"enableTelemetry",
GLOBAL_TELEMETRY_SETTING,
);
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
@@ -475,6 +463,7 @@ export function allowCanaryQueryServer() {
return value === undefined ? true : !!value;
}
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting(
"joinOrderWarningThreshold",
LOG_INSIGHTS_SETTING,
@@ -484,6 +473,7 @@ export function joinOrderWarningThreshold(): number {
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
}
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
/**
* Hidden setting: Avoids caching in the AST viewer if the user is also a canary user.
*/
@@ -492,6 +482,10 @@ export const NO_CACHE_AST_VIEWER = new Setting(
AST_VIEWER_SETTING,
);
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
"contextualQueries",
ROOT_SETTING,
);
/**
* Hidden setting: Avoids caching in jump to def and find refs contextual queries if the user is also a canary user.
*/

View File

@@ -12,6 +12,7 @@ import { DbModelingState, ModelingStore } from "../modeling-store";
import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider";
import { assertNever } from "../../common/helpers-pure";
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
import { showMultipleModels } from "../../config";
export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
ToMethodModelingMessage,
@@ -29,11 +30,20 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
super(app, "method-modeling");
}
protected override onWebViewLoaded(): void {
this.setInitialState();
protected override async onWebViewLoaded(): Promise<void> {
await Promise.all([this.setViewState(), this.setInitialState()]);
this.registerToModelingStoreEvents();
}
private async setViewState(): Promise<void> {
await this.postMessage({
t: "setMethodModelingPanelViewState",
viewState: {
showMultipleModels: showMultipleModels(),
},
});
}
public async setMethod(method: Method): Promise<void> {
this.method = method;
@@ -45,15 +55,17 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
}
}
private setInitialState(): void {
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
if (selectedMethod) {
void this.postMessage({
t: "setSelectedMethod",
method: selectedMethod.method,
modeledMethod: selectedMethod.modeledMethod,
isModified: selectedMethod.isModified,
});
private async setInitialState(): Promise<void> {
if (this.modelingStore.hasStateForActiveDb()) {
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
if (selectedMethod) {
await this.postMessage({
t: "setSelectedMethod",
method: selectedMethod.method,
modeledMethod: selectedMethod.modeledMethod,
isModified: selectedMethod.isModified,
});
}
}
}
@@ -62,7 +74,7 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
): Promise<void> {
switch (msg.t) {
case "viewLoaded":
this.onWebViewLoaded();
await this.onWebViewLoaded();
break;
case "telemetry":
@@ -92,6 +104,12 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
await this.revealInModelEditor(msg.method);
break;
case "startModeling":
await this.app.commands.execute(
"codeQL.openModelEditorFromModelingPanel",
);
break;
default:
assertNever(msg);
}
@@ -159,5 +177,25 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
}
}),
);
this.push(
this.modelingStore.onDbOpened(async () => {
await this.postMessage({
t: "setInModelingMode",
inModelingMode: true,
});
}),
);
this.push(
this.modelingStore.onDbClosed(async () => {
if (!this.modelingStore.anyDbsBeingModeled()) {
await this.postMessage({
t: "setInModelingMode",
inModelingMode: false,
});
}
}),
);
}
}

View File

@@ -73,115 +73,9 @@ export class ModelEditorModule extends DisposableObject {
public getCommands(): ModelEditorCommands {
return {
"codeQL.openModelEditor": async () => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage(this.app.logger, "No database selected");
return;
}
const language = db.language;
if (
!SUPPORTED_LANGUAGES.includes(language) ||
!isQueryLanguage(language)
) {
void showAndLogErrorMessage(
this.app.logger,
`The CodeQL Model Editor is not supported for ${language} databases.`,
);
return;
}
return withProgress(
async (progress) => {
const maxStep = 4;
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
);
return;
}
if (
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
);
return;
}
const modelFile = await pickExtensionPack(
this.cliServer,
db,
this.app.logger,
progress,
maxStep,
);
if (!modelFile) {
return;
}
progress({
message: "Installing dependencies...",
step: 3,
maxStep,
});
// Create new temporary directory for query files and pack dependencies
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
unsafeCleanup: true,
});
const success = await setUpPack(this.cliServer, queryDir, language);
if (!success) {
await cleanupQueryDir();
return;
}
progress({
message: "Opening editor...",
step: 4,
maxStep,
});
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.editorViewTracker,
this.databaseManager,
this.cliServer,
this.queryRunner,
this.queryStorageDir,
queryDir,
db,
modelFile,
Mode.Application,
);
this.modelingStore.onDbClosed(async (dbUri) => {
if (dbUri === db.databaseUri.toString()) {
await cleanupQueryDir();
}
});
this.push(view);
this.push({
dispose(): void {
void cleanupQueryDir();
},
});
await view.openView();
},
{
title: "Opening CodeQL Model Editor",
},
);
},
"codeQL.openModelEditor": this.openModelEditor.bind(this),
"codeQL.openModelEditorFromModelingPanel":
this.openModelEditor.bind(this),
"codeQLModelEditor.jumpToUsageLocation": async (
method: Method,
usage: Usage,
@@ -213,4 +107,116 @@ export class ModelEditorModule extends DisposableObject {
await this.methodModelingPanel.setMethod(method);
await showResolvableLocation(usage.url, databaseItem, this.app.logger);
}
private async openModelEditor(): Promise<void> {
{
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage(this.app.logger, "No database selected");
return;
}
const language = db.language;
if (
!SUPPORTED_LANGUAGES.includes(language) ||
!isQueryLanguage(language)
) {
void showAndLogErrorMessage(
this.app.logger,
`The CodeQL Model Editor is not supported for ${language} databases.`,
);
return;
}
return withProgress(
async (progress) => {
const maxStep = 4;
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
);
return;
}
if (
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
) {
void showAndLogErrorMessage(
this.app.logger,
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
);
return;
}
const modelFile = await pickExtensionPack(
this.cliServer,
db,
this.app.logger,
progress,
maxStep,
);
if (!modelFile) {
return;
}
progress({
message: "Installing dependencies...",
step: 3,
maxStep,
});
// Create new temporary directory for query files and pack dependencies
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
unsafeCleanup: true,
});
const success = await setUpPack(this.cliServer, queryDir, language);
if (!success) {
await cleanupQueryDir();
return;
}
progress({
message: "Opening editor...",
step: 4,
maxStep,
});
const view = new ModelEditorView(
this.app,
this.modelingStore,
this.editorViewTracker,
this.databaseManager,
this.cliServer,
this.queryRunner,
this.queryStorageDir,
queryDir,
db,
modelFile,
Mode.Application,
);
this.modelingStore.onDbClosed(async (dbUri) => {
if (dbUri === db.databaseUri.toString()) {
await cleanupQueryDir();
}
});
this.push(view);
this.push({
dispose(): void {
void cleanupQueryDir();
},
});
await view.openView();
},
{
title: "Opening CodeQL Model Editor",
},
);
}
}
}

View File

@@ -47,6 +47,10 @@ import { AutoModeler } from "./auto-modeler";
import { telemetryListener } from "../common/vscode/telemetry";
import { ModelingStore } from "./modeling-store";
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
import {
convertFromLegacyModeledMethods,
convertToLegacyModeledMethods,
} from "./modeled-methods-legacy";
export class ModelEditorView extends AbstractWebview<
ToModelEditorMessage,
@@ -100,9 +104,6 @@ export class ModelEditorView extends AbstractWebview<
panel.onDidChangeViewState(async () => {
if (panel.active) {
this.modelingStore.setActiveDb(this.databaseItem);
await this.markModelEditorAsActive();
} else {
await this.updateModelEditorActiveContext();
}
});
@@ -126,36 +127,12 @@ export class ModelEditorView extends AbstractWebview<
);
}
private async markModelEditorAsActive(): Promise<void> {
void this.app.commands.execute(
"setContext",
"codeql.modelEditorActive",
true,
);
}
private async updateModelEditorActiveContext(): Promise<void> {
await this.app.commands.execute(
"setContext",
"codeql.modelEditorActive",
this.isAModelEditorActive(),
);
}
private isAModelEditorOpen(): boolean {
return window.tabGroups.all.some((tabGroup) =>
tabGroup.tabs.some((tab) => this.isTabModelEditorView(tab)),
);
}
private isAModelEditorActive(): boolean {
return window.tabGroups.all.some((tabGroup) =>
tabGroup.tabs.some(
(tab) => this.isTabModelEditorView(tab) && tab.isActive,
),
);
}
private isTabModelEditorView(tab: Tab): boolean {
if (!(tab.input instanceof TabInputWebview)) {
return false;
@@ -249,7 +226,7 @@ export class ModelEditorView extends AbstractWebview<
this.extensionPack,
this.databaseItem.language,
methods,
modeledMethods,
convertFromLegacyModeledMethods(modeledMethods),
this.mode,
this.cliServer,
this.app.logger,
@@ -366,7 +343,7 @@ export class ModelEditorView extends AbstractWebview<
await this.postMessage({
t: "revealMethod",
method,
methodSignature: method.signature,
});
}
@@ -397,7 +374,10 @@ export class ModelEditorView extends AbstractWebview<
this.cliServer,
this.app.logger,
);
this.modelingStore.setModeledMethods(this.databaseItem, modeledMethods);
this.modelingStore.setModeledMethods(
this.databaseItem,
convertToLegacyModeledMethods(modeledMethods),
);
} catch (e: unknown) {
void showAndLogErrorMessage(
this.app.logger,

View File

@@ -10,17 +10,12 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { load as loadYaml } from "js-yaml";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { pathsEqual } from "../common/files";
import {
convertFromLegacyModeledMethods,
convertFromLegacyModeledMethodsFiles,
convertToLegacyModeledMethods,
} from "./modeled-methods-legacy";
export async function saveModeledMethods(
extensionPack: ExtensionPack,
language: string,
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
modeledMethods: Record<string, ModeledMethod[]>,
mode: Mode,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
@@ -34,8 +29,8 @@ export async function saveModeledMethods(
const yamls = createDataExtensionYamls(
language,
methods,
convertFromLegacyModeledMethods(modeledMethods),
convertFromLegacyModeledMethodsFiles(existingModeledMethods),
modeledMethods,
existingModeledMethods,
mode,
);
@@ -50,12 +45,12 @@ async function loadModeledMethodFiles(
extensionPack: ExtensionPack,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<Record<string, Record<string, ModeledMethod>>> {
): Promise<Record<string, Record<string, ModeledMethod[]>>> {
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
const modeledMethodsByFile: Record<
string,
Record<string, ModeledMethod>
Record<string, ModeledMethod[]>
> = {};
for (const modelFile of modelFiles) {
@@ -73,8 +68,7 @@ async function loadModeledMethodFiles(
);
continue;
}
modeledMethodsByFile[modelFile] =
convertToLegacyModeledMethods(modeledMethods);
modeledMethodsByFile[modelFile] = modeledMethods;
}
return modeledMethodsByFile;
@@ -84,8 +78,8 @@ export async function loadModeledMethods(
extensionPack: ExtensionPack,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<Record<string, ModeledMethod>> {
const existingModeledMethods: Record<string, ModeledMethod> = {};
): Promise<Record<string, ModeledMethod[]>> {
const existingModeledMethods: Record<string, ModeledMethod[]> = {};
const modeledMethodsByFile = await loadModeledMethodFiles(
extensionPack,
@@ -94,7 +88,11 @@ export async function loadModeledMethods(
);
for (const modeledMethods of Object.values(modeledMethodsByFile)) {
for (const [key, value] of Object.entries(modeledMethods)) {
existingModeledMethods[key] = value;
if (!(key in existingModeledMethods)) {
existingModeledMethods[key] = [];
}
existingModeledMethods[key].push(...value);
}
}

View File

@@ -21,13 +21,3 @@ export function convertToLegacyModeledMethods(
}),
);
}
export function convertFromLegacyModeledMethodsFiles(
modeledMethods: Record<string, Record<string, ModeledMethod>>,
): Record<string, Record<string, ModeledMethod[]>> {
return Object.fromEntries(
Object.entries(modeledMethods).map(([filename, modeledMethods]) => {
return [filename, convertFromLegacyModeledMethods(modeledMethods)];
}),
);
}

View File

@@ -49,6 +49,7 @@ interface SelectedMethodChangedEvent {
export class ModelingStore extends DisposableObject {
public readonly onActiveDbChanged: AppEvent<void>;
public readonly onDbOpened: AppEvent<string>;
public readonly onDbClosed: AppEvent<string>;
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
@@ -60,6 +61,7 @@ export class ModelingStore extends DisposableObject {
private activeDb: string | undefined;
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
private readonly onDbOpenedEventEmitter: AppEventEmitter<string>;
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
@@ -79,6 +81,9 @@ export class ModelingStore extends DisposableObject {
);
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
this.onDbOpenedEventEmitter = this.push(app.createEventEmitter<string>());
this.onDbOpened = this.onDbOpenedEventEmitter.event;
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
this.onDbClosed = this.onDbClosedEventEmitter.event;
@@ -123,6 +128,8 @@ export class ModelingStore extends DisposableObject {
selectedMethod: undefined,
selectedUsage: undefined,
});
this.onDbOpenedEventEmitter.fire(dbUri);
}
public setActiveDb(databaseItem: DatabaseItem) {
@@ -154,6 +161,14 @@ export class ModelingStore extends DisposableObject {
return this.state.get(this.activeDb);
}
public hasStateForActiveDb(): boolean {
return !!this.getStateForActiveDb();
}
public anyDbsBeingModeled(): boolean {
return this.state.size > 0;
}
/**
* Returns the methods for the given database item and method signatures.
* If the `methodSignatures` argument not provided or is undefined, returns all methods.

View File

@@ -8,3 +8,7 @@ export interface ModelEditorViewState {
showMultipleModels: boolean;
mode: Mode;
}
export interface MethodModelingPanelViewState {
showMultipleModels: boolean;
}

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { ResponsiveContainer as ResponsiveContainerComponent } from "../../view/common/ResponsiveContainer";
export default {
title: "Responsive Container",
component: ResponsiveContainerComponent,
} as Meta<typeof ResponsiveContainerComponent>;
const Template: StoryFn<typeof ResponsiveContainerComponent> = (args) => (
<ResponsiveContainerComponent>
<span>Hello</span>
</ResponsiveContainerComponent>
);
export const ResponsiveContainer = Template.bind({});

View File

@@ -0,0 +1,16 @@
import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { NoMethodSelected as NoMethodSelectedComponent } from "../../view/method-modeling/NoMethodSelected";
export default {
title: "Method Modeling/No Method Selected",
component: NoMethodSelectedComponent,
} as Meta<typeof NoMethodSelectedComponent>;
const Template: StoryFn<typeof NoMethodSelectedComponent> = () => (
<NoMethodSelectedComponent />
);
export const NoMethodSelected = Template.bind({});

View File

@@ -0,0 +1,16 @@
import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { NotInModelingMode as NotInModelingModeComponent } from "../../view/method-modeling/NotInModelingMode";
export default {
title: "Method Modeling/Not In Modeling Mode",
component: NotInModelingModeComponent,
} as Meta<typeof NotInModelingModeComponent>;
const Template: StoryFn<typeof NotInModelingModeComponent> = () => (
<NotInModelingModeComponent />
);
export const NotInModelingMode = Template.bind({});

View File

@@ -0,0 +1,15 @@
import { styled } from "styled-components";
export const ResponsiveContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
height: 100vh;
@media (min-height: 300px) {
align-items: center;
justify-content: center;
text-align: center;
}
`;

View File

@@ -10,17 +10,18 @@ import { VSCodeTag } from "@vscode/webview-ui-toolkit/react";
import { ReviewInEditorButton } from "./ReviewInEditorButton";
const Container = styled.div`
padding: 0.3rem;
padding-top: 0.5rem;
margin-bottom: 1rem;
width: 100%;
`;
const Title = styled.div`
padding-bottom: 0.3rem;
font-size: 0.7rem;
padding-bottom: 0.5rem;
font-size: 0.9rem;
text-transform: uppercase;
display: flex;
justify-content: space-between;
align-items: center;
`;
const DependencyContainer = styled.div`
@@ -34,12 +35,28 @@ const DependencyContainer = styled.div`
padding: 0.5rem;
word-wrap: break-word;
word-break: break-all;
margin-bottom: 0.8rem;
`;
const StyledMethodModelingInputs = styled(MethodModelingInputs)`
padding-bottom: 0.5rem;
`;
const StyledVSCodeTag = styled(VSCodeTag)<{ visible: boolean }>`
visibility: ${(props) => (props.visible ? "visible" : "hidden")};
`;
const UnsavedTag = ({ modelingStatus }: { modelingStatus: ModelingStatus }) => (
<StyledVSCodeTag visible={modelingStatus === "unsaved"}>
Unsaved
</StyledVSCodeTag>
);
export type MethodModelingProps = {
modelingStatus: ModelingStatus;
method: Method;
modeledMethod: ModeledMethod | undefined;
showMultipleModels?: boolean;
onChange: (modeledMethod: ModeledMethod) => void;
};
@@ -54,13 +71,13 @@ export const MethodModeling = ({
<Title>
{method.packageName}
{method.libraryVersion && <>@{method.libraryVersion}</>}
{modelingStatus === "unsaved" ? <VSCodeTag>Unsaved</VSCodeTag> : null}
<UnsavedTag modelingStatus={modelingStatus} />
</Title>
<DependencyContainer>
<ModelingStatusIndicator status={modelingStatus} />
<MethodName {...method} />
</DependencyContainer>
<MethodModelingInputs
<StyledMethodModelingInputs
method={method}
modeledMethod={modeledMethod}
onChange={onChange}

View File

@@ -11,11 +11,14 @@ const Container = styled.div`
padding-top: 0.5rem;
`;
const Input = styled.label``;
const Input = styled.label`
display: block;
padding-bottom: 0.3rem;
`;
const Name = styled.span`
display: block;
padding-bottom: 0.3rem;
padding-bottom: 0.5rem;
`;
export type MethodModelingInputsProps = {

View File

@@ -7,8 +7,20 @@ import { ToMethodModelingMessage } from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { vscode } from "../vscode-api";
import { NotInModelingMode } from "./NotInModelingMode";
import { NoMethodSelected } from "./NoMethodSelected";
import { MethodModelingPanelViewState } from "../../model-editor/shared/view-state";
type Props = {
initialViewState?: MethodModelingPanelViewState;
};
export function MethodModelingView({ initialViewState }: Props): JSX.Element {
const [viewState, setViewState] = useState<
MethodModelingPanelViewState | undefined
>(initialViewState);
const [inModelingMode, setInModelingMode] = useState<boolean>(false);
export function MethodModelingView(): JSX.Element {
const [method, setMethod] = useState<Method | undefined>(undefined);
const [modeledMethod, setModeledMethod] = React.useState<
@@ -27,6 +39,12 @@ export function MethodModelingView(): JSX.Element {
if (evt.origin === window.origin) {
const msg: ToMethodModelingMessage = evt.data;
switch (msg.t) {
case "setMethodModelingPanelViewState":
setViewState(msg.viewState);
break;
case "setInModelingMode":
setInModelingMode(msg.inModelingMode);
break;
case "setMethod":
setMethod(msg.method);
break;
@@ -57,8 +75,12 @@ export function MethodModelingView(): JSX.Element {
};
}, []);
if (!inModelingMode) {
return <NotInModelingMode />;
}
if (!method) {
return <>Select method to model</>;
return <NoMethodSelected />;
}
const onChange = (modeledMethod: ModeledMethod) => {
@@ -73,6 +95,7 @@ export function MethodModelingView(): JSX.Element {
modelingStatus={modelingStatus}
method={method}
modeledMethod={modeledMethod}
showMultipleModels={viewState?.showMultipleModels}
onChange={onChange}
/>
);

View File

@@ -0,0 +1,8 @@
import * as React from "react";
import { ResponsiveContainer } from "../common/ResponsiveContainer";
export const NoMethodSelected = () => {
return (
<ResponsiveContainer>Select an API or method to model</ResponsiveContainer>
);
};

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { useCallback } from "react";
import { vscode } from "../vscode-api";
import { styled } from "styled-components";
import TextButton from "../common/TextButton";
import { ResponsiveContainer } from "../common/ResponsiveContainer";
const Button = styled(TextButton)`
margin-top: 0.2rem;
`;
export const NotInModelingMode = () => {
const handleClick = useCallback(() => {
vscode.postMessage({
t: "startModeling",
});
}, []);
return (
<ResponsiveContainer>
<span>Not in modeling mode</span>
<Button onClick={handleClick}>Start modeling</Button>
</ResponsiveContainer>
);
};

View File

@@ -6,7 +6,7 @@ import TextButton from "../common/TextButton";
import { Method } from "../../model-editor/method";
const Button = styled(TextButton)`
margin-top: 0.5rem;
margin-top: 0.7rem;
`;
type Props = {

View File

@@ -142,7 +142,7 @@ export function ModelEditor({
);
break;
case "revealMethod":
setRevealedMethodSignature(msg.method.signature);
setRevealedMethodSignature(msg.methodSignature);
break;
default:

View File

@@ -4,6 +4,7 @@ import {
workspace,
ConfigurationTarget,
window,
env,
} from "vscode";
import {
ExtensionTelemetryListener,
@@ -30,13 +31,18 @@ describe("telemetry reporting", () => {
let sendTelemetryEventSpy: jest.SpiedFunction<
typeof TelemetryReporter.prototype.sendTelemetryEvent
>;
let sendTelemetryExceptionSpy: jest.SpiedFunction<
typeof TelemetryReporter.prototype.sendTelemetryException
let sendTelemetryErrorEventSpy: jest.SpiedFunction<
typeof TelemetryReporter.prototype.sendTelemetryErrorEvent
>;
let disposeSpy: jest.SpiedFunction<
typeof TelemetryReporter.prototype.dispose
>;
let isTelemetryEnabledSpy: jest.SpyInstance<
typeof env.isTelemetryEnabled,
[]
>;
let showInformationMessageSpy: jest.SpiedFunction<
typeof window.showInformationMessage
>;
@@ -56,8 +62,8 @@ describe("telemetry reporting", () => {
sendTelemetryEventSpy = jest
.spyOn(TelemetryReporter.prototype, "sendTelemetryEvent")
.mockReturnValue(undefined);
sendTelemetryExceptionSpy = jest
.spyOn(TelemetryReporter.prototype, "sendTelemetryException")
sendTelemetryErrorEventSpy = jest
.spyOn(TelemetryReporter.prototype, "sendTelemetryErrorEvent")
.mockReturnValue(undefined);
disposeSpy = jest
.spyOn(TelemetryReporter.prototype, "dispose")
@@ -78,6 +84,9 @@ describe("telemetry reporting", () => {
.get<boolean>("codeQL.canary")).toString();
// each test will default to telemetry being enabled
isTelemetryEnabledSpy = jest
.spyOn(env, "isTelemetryEnabled", "get")
.mockReturnValue(true);
await enableTelemetry("telemetry", true);
await enableTelemetry("codeQL.telemetry", true);
@@ -116,6 +125,7 @@ describe("telemetry reporting", () => {
});
it("should initialize telemetry when global option disabled", async () => {
isTelemetryEnabledSpy.mockReturnValue(false);
await enableTelemetry("telemetry", false);
await telemetryListener.initialize();
expect(telemetryListener._reporter).toBeDefined();
@@ -133,6 +143,7 @@ describe("telemetry reporting", () => {
it("should not initialize telemetry when both options disabled", async () => {
await enableTelemetry("codeQL.telemetry", false);
isTelemetryEnabledSpy.mockReturnValue(false);
await enableTelemetry("telemetry", false);
await telemetryListener.initialize();
expect(telemetryListener._reporter).toBeUndefined();
@@ -179,6 +190,7 @@ describe("telemetry reporting", () => {
const reporter: any = telemetryListener._reporter;
expect(reporter.userOptIn).toBe(true); // enabled
isTelemetryEnabledSpy.mockReturnValue(false);
await enableTelemetry("telemetry", false);
expect(reporter.userOptIn).toBe(false); // disabled
});
@@ -198,8 +210,7 @@ describe("telemetry reporting", () => {
},
{ executionTime: 1234 },
);
expect(sendTelemetryExceptionSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should send a command usage event with an error", async () => {
@@ -221,8 +232,7 @@ describe("telemetry reporting", () => {
},
{ executionTime: 1234 },
);
expect(sendTelemetryExceptionSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should send a command usage event with a cli version", async () => {
@@ -245,8 +255,7 @@ describe("telemetry reporting", () => {
},
{ executionTime: 1234 },
);
expect(sendTelemetryExceptionSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
// Verify that if the cli version is not set, then the telemetry falls back to "not-set"
sendTelemetryEventSpy.mockClear();
@@ -268,6 +277,7 @@ describe("telemetry reporting", () => {
},
{ executionTime: 5678 },
);
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should avoid sending an event when telemetry is disabled", async () => {
@@ -278,7 +288,7 @@ describe("telemetry reporting", () => {
telemetryListener.sendCommandUsage("command-id", 1234, new Error());
expect(sendTelemetryEventSpy).not.toBeCalled();
expect(sendTelemetryExceptionSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should send an event when telemetry is re-enabled", async () => {
@@ -298,6 +308,7 @@ describe("telemetry reporting", () => {
},
{ executionTime: 1234 },
);
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should filter undesired properties from telemetry payload", async () => {
@@ -345,6 +356,8 @@ describe("telemetry reporting", () => {
resolveArg(3 /* "yes" item */),
);
await ctx.globalState.update("telemetry-request-viewed", false);
expect(env.isTelemetryEnabled).toBe(true);
await enableTelemetry("codeQL.telemetry", false);
await telemetryListener.initialize();
@@ -411,6 +424,7 @@ describe("telemetry reporting", () => {
// If the user ever turns global telemetry back on, then we can
// show the dialog.
isTelemetryEnabledSpy.mockReturnValue(false);
await enableTelemetry("telemetry", false);
await ctx.globalState.update("telemetry-request-viewed", false);
@@ -455,6 +469,7 @@ describe("telemetry reporting", () => {
},
{},
);
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should send a ui-interaction telementry event with a cli version", async () => {
@@ -472,6 +487,7 @@ describe("telemetry reporting", () => {
},
{},
);
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
});
it("should send an error telementry event", async () => {
@@ -479,7 +495,8 @@ describe("telemetry reporting", () => {
telemetryListener.sendError(redactableError`test`);
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
expect(sendTelemetryEventSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
"error",
{
message: "test",
@@ -497,7 +514,8 @@ describe("telemetry reporting", () => {
telemetryListener.sendError(redactableError`test`);
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
expect(sendTelemetryEventSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
"error",
{
message: "test",
@@ -516,7 +534,8 @@ describe("telemetry reporting", () => {
redactableError`test message with secret information: ${42} and more ${"secret"} parts`,
);
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
expect(sendTelemetryEventSpy).not.toBeCalled();
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
"error",
{
message: