From 579c37eab3bb9f0a0f33a71b99c82a181fa0cc4e Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:24:54 -0400 Subject: [PATCH] Add document drop handler (#2448) --- ext/vscode/.vscode/cspell-dictionary.txt | 3 + .../language/AzureYamlDiagnosticProvider.ts | 43 +++++++------ .../AzureYamlDocumentDropEditProvider.ts | 40 ++++++++++++ .../AzureYamlProjectRenameProvider.ts | 61 +++++++++++-------- ext/vscode/src/language/languageFeatures.ts | 5 ++ ext/vscode/src/telemetry/telemetryId.ts | 21 +++++-- 6 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts diff --git a/ext/vscode/.vscode/cspell-dictionary.txt b/ext/vscode/.vscode/cspell-dictionary.txt index 036203c717..70f49abc7c 100644 --- a/ext/vscode/.vscode/cspell-dictionary.txt +++ b/ext/vscode/.vscode/cspell-dictionary.txt @@ -7,3 +7,6 @@ azureresources walkthrough walkthroughs DEBUGTELEMETRY +appservice +containerapp +staticwebapp \ No newline at end of file diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 3d908ca6d8..3bd706372b 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import { AzExtFsExtra, IActionContext, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { documentDebounce } from './documentDebounce'; import { getAzureYamlProjectInformation } from './azureYamlUtils'; +import { TelemetryId } from '../telemetry/telemetryId'; // Time between when the user stops typing and when we send diagnostics const DiagnosticDelay = 1000; @@ -33,31 +34,33 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { this.diagnosticCollection = diagnosticCollection; } - public async provideDiagnostics(document: vscode.TextDocument, token?: vscode.CancellationToken): Promise { - const results: vscode.Diagnostic[] = []; + public provideDiagnostics(document: vscode.TextDocument, token?: vscode.CancellationToken): Promise { + return callWithTelemetryAndErrorHandling(TelemetryId.AzureYamlProvideDiagnostics, async (context: IActionContext) => { + const results: vscode.Diagnostic[] = []; - try { - const projectInformation = await getAzureYamlProjectInformation(document); + try { + const projectInformation = await getAzureYamlProjectInformation(document); - for (const project of projectInformation) { - if (await AzExtFsExtra.pathExists(project.projectUri)) { - continue; - } + for (const project of projectInformation) { + if (await AzExtFsExtra.pathExists(project.projectUri)) { + continue; + } - const diagnostic = new vscode.Diagnostic( - project.projectValueNodeRange, - vscode.l10n.t('The project path must be an existing folder or file path relative to the azure.yaml file.'), - vscode.DiagnosticSeverity.Error - ); + const diagnostic = new vscode.Diagnostic( + project.projectValueNodeRange, + vscode.l10n.t('The project path must be an existing folder or file path relative to the azure.yaml file.'), + vscode.DiagnosticSeverity.Error + ); - results.push(diagnostic); + results.push(diagnostic); + } + } catch { + // Best effort--the YAML extension will show parsing errors for us if it is present } - } catch { - // Best effort--the YAML extension will show parsing errors for us if it is present - return results; - } - return results; + context.telemetry.measurements.diagnosticCount = results.length; + return results; + }); } private async updateDiagnosticsFor(document: vscode.TextDocument, delay: boolean = true): Promise { diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts new file mode 100644 index 0000000000..19b4c1f30c --- /dev/null +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AzExtFsExtra, IActionContext, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getProjectRelativePath } from './azureYamlUtils'; +import { TelemetryId } from '../telemetry/telemetryId'; + +export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEditProvider { + public provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + return callWithTelemetryAndErrorHandling(TelemetryId.AzureYamlProvideDocumentDropEdits, async (context: IActionContext) => { + const maybeFolder = dataTransfer.get('text/uri-list')?.value; + const maybeFolderUri = vscode.Uri.parse(maybeFolder); + + if (await AzExtFsExtra.pathExists(maybeFolderUri) && await AzExtFsExtra.isDirectory(maybeFolderUri)) { + const basename = path.basename(maybeFolderUri.fsPath); + const newRelativePath = getProjectRelativePath(document.uri, maybeFolderUri); + + const initialWhitespace = position.character === 0 ? '\n\t' : '\n'; + + const snippet = new vscode.SnippetString(initialWhitespace) + .appendPlaceholder(basename).appendText(':\n') + .appendText(`\t\tproject: ${newRelativePath}\n`) + .appendText('\t\tlanguage: ') + .appendChoice(['dotnet', 'csharp', 'fsharp', 'py', 'python', 'js', 'ts', 'java']) + .appendText('\n') + .appendText('\t\thost: ') + .appendChoice(['appservice', 'containerapp', 'function', 'staticwebapp', 'aks']) + .appendText('\n'); + + context.telemetry.properties.editProvided = 'true'; + return new vscode.DocumentDropEdit(snippet); + } + + context.telemetry.properties.editProvided = 'false'; + return undefined; + }); + } +} diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index 04e0050f22..87f96c34b8 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import { AzExtFsExtra, IActionContext, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { getAzureYamlProjectInformation, getProjectRelativePath } from './azureYamlUtils'; +import { TelemetryId } from '../telemetry/telemetryId'; export class AzureYamlProjectRenameProvider extends vscode.Disposable { public constructor() { @@ -15,31 +16,39 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { }); } - public async provideWorkspaceEdits(oldUri: vscode.Uri, newUri: vscode.Uri, token: vscode.CancellationToken): Promise { - // When a folder is renamed, only the folder is passed in as the old URI - // At the time this is called, the rename has not happened yet - if (!await AzExtFsExtra.isDirectory(oldUri)) { - return undefined; - } - - const azureYamlUris = await vscode.workspace.findFiles('**/azure.{yml,yaml}', undefined, 1, token); - if (azureYamlUris.length === 0) { - return undefined; - } - - const azureYamlUri = azureYamlUris[0]; - const azureYaml = await vscode.workspace.openTextDocument(azureYamlUri); - const projectInformation = await getAzureYamlProjectInformation(azureYaml); - - const projectToUpdate = projectInformation.find(pi => pi.projectUri.toString() === oldUri.toString()); - if (!projectToUpdate) { - return undefined; - } - - const newRelativePath = getProjectRelativePath(azureYamlUri, newUri); - const projectUriEdit = new vscode.WorkspaceEdit(); - projectUriEdit.replace(azureYamlUri, projectToUpdate.projectValueNodeRange, newRelativePath); - return projectUriEdit; + public provideWorkspaceEdits(oldUri: vscode.Uri, newUri: vscode.Uri, token: vscode.CancellationToken): Promise { + return callWithTelemetryAndErrorHandling(TelemetryId.AzureYamlProjectRenameProvideWorkspaceEdits, async (context: IActionContext) => { + // When a folder is renamed, only the folder is passed in as the old URI + // At the time this is called, the rename has not happened yet + if (!await AzExtFsExtra.isDirectory(oldUri)) { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.cancelStep = 'SourceNotDirectory'; + return undefined; + } + + const azureYamlUris = await vscode.workspace.findFiles('**/azure.{yml,yaml}', undefined, 1, token); + if (azureYamlUris.length === 0) { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.cancelStep = 'NoAzureYaml'; + return undefined; + } + + const azureYamlUri = azureYamlUris[0]; + const azureYaml = await vscode.workspace.openTextDocument(azureYamlUri); + const projectInformation = await getAzureYamlProjectInformation(azureYaml); + + const projectToUpdate = projectInformation.find(pi => pi.projectUri.toString() === oldUri.toString()); + if (!projectToUpdate) { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.cancelStep = 'ProjectNotInAzureYaml'; + return undefined; + } + + const newRelativePath = getProjectRelativePath(azureYamlUri, newUri); + const projectUriEdit = new vscode.WorkspaceEdit(); + projectUriEdit.replace(azureYamlUri, projectToUpdate.projectValueNodeRange, newRelativePath); + return projectUriEdit; + }); } private handleWillRenameFile(evt: vscode.FileWillRenameEvent): void { diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 247a9b0eee..a0cf429879 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import ext from '../ext'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; import { AzureYamlProjectRenameProvider } from './AzureYamlProjectRenameProvider'; +import { AzureYamlDocumentDropEditProvider } from './AzureYamlDocumentDropEditProvider'; export const AzureYamlSelector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; @@ -16,4 +17,8 @@ export function registerLanguageFeatures(): void { ext.context.subscriptions.push( new AzureYamlProjectRenameProvider() ); + + ext.context.subscriptions.push( + vscode.languages.registerDocumentDropEditProvider(AzureYamlSelector, new AzureYamlDocumentDropEditProvider()) + ); } diff --git a/ext/vscode/src/telemetry/telemetryId.ts b/ext/vscode/src/telemetry/telemetryId.ts index 1cf23e4745..03599b3b47 100644 --- a/ext/vscode/src/telemetry/telemetryId.ts +++ b/ext/vscode/src/telemetry/telemetryId.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. // Defines telemetry IDs for custom telemetry events exposed by the extension. -// Note that command invocations are covered by the AzExtUtils library, +// Note that command invocations are covered by the AzExtUtils library, // which automatically captures the duration and success/failure information for every command. // Every event includes duration and success/failure information. // Some additional data is captured for certain events; see comments for individual enum members. @@ -57,18 +57,27 @@ export enum TelemetryId { // Reported when 'env refresh' CLI command is invoked. EnvRefreshCli = 'azure-dev.commands.cli.env-refresh.task', - + // Reported when the product evaluates whether to prompt the user for a survey. - // We capture - // - whether the user was already offered the survey, + // We capture + // - whether the user was already offered the survey, // - whether the user was prompted during current session (for any survey) // - whether the user is eligible for given survey - // - whether the user is flighted for the survey + // - whether the user is flighted for the survey SurveyCheck = 'azure-dev.survey-check', // Captures the result of a survey prompt SurveyPromptResponse = 'azure-dev.survey-prompt-response', WorkspaceViewApplicationResolve = 'azure-dev.views.workspace.application.resolve', - WorkspaceViewEnvironmentResolve = 'azure-dev.views.workspace.environment.resolve' + WorkspaceViewEnvironmentResolve = 'azure-dev.views.workspace.environment.resolve', + + // Reported when diagnostics are provided on an azure.yaml document + AzureYamlProvideDiagnostics = 'azure-dev.azureYaml.provideDiagnostics', + + // Reported when the document drop edit provider is invoked + AzureYamlProvideDocumentDropEdits = 'azure-dev.azureYaml.provideDocumentDropEdits', + + // Reported when the project rename provider is invoked + AzureYamlProjectRenameProvideWorkspaceEdits = 'azure-dev.azureYaml.projectRename.provideWorkspaceEdits', }