From 929d71d2ef46adab5e4cc8da84e35af64d828313 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:07:14 -0400 Subject: [PATCH 01/28] Add language service features to extension --- ext/vscode/package-lock.json | 11 +++- ext/vscode/package.json | 7 ++- ext/vscode/src/extension.ts | 2 + .../language/AzureYamlCodeActionProvider.ts | 10 ++++ .../AzureYamlCompletionItemProvider.ts | 53 +++++++++++++++++++ .../language/AzureYamlDiagnosticCollection.ts | 32 +++++++++++ ext/vscode/src/language/languageFeatures.ts | 38 +++++++++++++ 7 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 ext/vscode/src/language/AzureYamlCodeActionProvider.ts create mode 100644 ext/vscode/src/language/AzureYamlCompletionItemProvider.ts create mode 100644 ext/vscode/src/language/AzureYamlDiagnosticCollection.ts create mode 100644 ext/vscode/src/language/languageFeatures.ts diff --git a/ext/vscode/package-lock.json b/ext/vscode/package-lock.json index 9e65f4e5c11..fa4b0ff82d1 100644 --- a/ext/vscode/package-lock.json +++ b/ext/vscode/package-lock.json @@ -14,7 +14,8 @@ "dayjs": "~1.11", "dotenv": "~16.0", "rxjs": "~7.6", - "semver": "~7" + "semver": "~7", + "yaml": "~2" }, "devDependencies": { "@types/chai": "~4.3", @@ -5469,6 +5470,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/ext/vscode/package.json b/ext/vscode/package.json index c3e8757bdab..fe01dfdf6e7 100644 --- a/ext/vscode/package.json +++ b/ext/vscode/package.json @@ -26,7 +26,9 @@ "url": "https://github.com/azure/azure-dev" }, "activationEvents": [ - "onTaskType:dotenv" + "onTaskType:dotenv", + "workspaceContains:azure.yml", + "workspaceContains:azure.yaml" ], "main": "./main.js", "contributes": { @@ -503,6 +505,7 @@ "dayjs": "~1.11", "dotenv": "~16.0", "rxjs": "~7.6", - "semver": "~7" + "semver": "~7", + "yaml": "~2" } } diff --git a/ext/vscode/src/extension.ts b/ext/vscode/src/extension.ts index 7e7cd6ca5b3..98a441e9c76 100644 --- a/ext/vscode/src/extension.ts +++ b/ext/vscode/src/extension.ts @@ -12,6 +12,7 @@ import { ActivityStatisticsService } from './telemetry/activityStatisticsService import { scheduleAzdSignInCheck, scheduleAzdVersionCheck, scheduleAzdYamlCheck } from './utils/azureDevCli'; import { activeSurveys } from './telemetry/activeSurveys'; import { scheduleRegisterWorkspaceComponents } from './views/workspace/scheduleRegisterWorkspaceComponents'; +import { registerLanguageFeatures } from './language/languageFeatures'; type LoadStats = { // Both are the values returned by Date.now()==milliseconds since Unix epoch. @@ -46,6 +47,7 @@ export async function activateInternal(vscodeCtx: vscode.ExtensionContext, loadS ext.activitySvc = new ActivityStatisticsService(vscodeCtx.globalState); registerCommands(); registerDisposable(vscode.tasks.registerTaskProvider('dotenv', new DotEnvTaskProvider())); + registerLanguageFeatures(); scheduleRegisterWorkspaceComponents(vscodeCtx); scheduleSurveys(vscodeCtx.globalState, activeSurveys); scheduleAzdVersionCheck(); // Temporary diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts new file mode 100644 index 00000000000..6126ae82f85 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export class AzureYamlCodeActionProvider implements vscode.CodeActionProvider { + public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { + throw new Error('Method not implemented.'); + } +} \ No newline at end of file diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts new file mode 100644 index 00000000000..9ed7453df86 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { + public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { + // if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter || + // context.triggerCharacter !== '/') { + // // Shouldn't have been triggered, return an empty set + // return []; + // } + + const pathPrefix = this.getPathPrefix(document, position); + const matchingPaths = await this.getMatchingPaths(document, pathPrefix, token); + + return matchingPaths.map((path) => { + const completionItem = new vscode.CompletionItem(path); + completionItem.insertText = path; + completionItem.kind = vscode.CompletionItemKind.Folder; + return completionItem; + }); + } + + private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { + const prefixRange = document.getWordRangeAtPosition(position); + + if (!prefixRange) { + return undefined; + } + + return document.getText(prefixRange); + } + + private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { + if (!pathPrefix || pathPrefix[0] !== '.') { + return []; + } + + const currentFolder = vscode.Uri.joinPath(document.uri, '.' + pathPrefix); + const results: string[] = []; + + for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { + if (type !== vscode.FileType.Directory) { + continue; + } + + results.push(file); + } + + return results; + } +} \ No newline at end of file diff --git a/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts b/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts new file mode 100644 index 00000000000..0e6e0f701ae --- /dev/null +++ b/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import ext from '../ext'; +import { IActionContext, registerEvent } from '@microsoft/vscode-azext-utils'; + +export let azureYamlDiagnosticCollection: vscode.DiagnosticCollection; + +export function registerDiagnosticCollection(): void { + ext.context.subscriptions.push( + azureYamlDiagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yml') + ); + + registerEvent('onDidChangeTextDocument', vscode.workspace.onDidChangeTextDocument, async (context: IActionContext, e: vscode.TextDocumentChangeEvent) => { + if (e.document.languageId !== 'yaml' || !/azure\.ya?ml$/i.test(e.document.fileName)) { + return; + } + + context.telemetry.suppressAll = true; + context.errorHandling.suppressReportIssue = true; + + // Rerun diagnostics + }); + + registerEvent('onDidRenameFiles', vscode.workspace.onDidRenameFiles, async (context: IActionContext, e: vscode.FileRenameEvent) => { + context.telemetry.suppressAll = true; + context.errorHandling.suppressReportIssue = true; + + // Noop + }); +} \ No newline at end of file diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts new file mode 100644 index 00000000000..6c4aba353a0 --- /dev/null +++ b/ext/vscode/src/language/languageFeatures.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import ext from '../ext'; +import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; +import { IActionContext, registerEvent } from '@microsoft/vscode-azext-utils'; + +export function registerLanguageFeatures(): void { + const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; + + ext.context.subscriptions.push( + vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), + ); + + let azureYamlDiagnosticCollection: vscode.DiagnosticCollection; + ext.context.subscriptions.push( + azureYamlDiagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yml') + ); + + registerEvent('onDidChangeTextDocument', vscode.workspace.onDidChangeTextDocument, async (context: IActionContext, e: vscode.TextDocumentChangeEvent) => { + if (e.document.languageId !== 'yaml' || !/azure\.ya?ml$/i.test(e.document.fileName)) { + return; + } + + context.telemetry.suppressAll = true; + context.errorHandling.suppressReportIssue = true; + + // Rerun diagnostics + }); + + registerEvent('onDidRenameFiles', vscode.workspace.onDidRenameFiles, async (context: IActionContext, e: vscode.FileRenameEvent) => { + context.telemetry.suppressAll = true; + context.errorHandling.suppressReportIssue = true; + + // Noop + }); +} \ No newline at end of file From 93097ba38cb6aed35b89c63c68fa49cbd18ea7db Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:08:11 -0400 Subject: [PATCH 02/28] Update to version 2 of `@microsoft/vscode-azext-utils` --- ext/vscode/package-lock.json | 8 ++++---- ext/vscode/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/vscode/package-lock.json b/ext/vscode/package-lock.json index 9e65f4e5c11..021ec9983e5 100644 --- a/ext/vscode/package-lock.json +++ b/ext/vscode/package-lock.json @@ -9,7 +9,7 @@ "version": "0.7.0-alpha.1", "license": "MIT", "dependencies": { - "@microsoft/vscode-azext-utils": "~1", + "@microsoft/vscode-azext-utils": "~2", "@microsoft/vscode-azureresources-api": "~2", "dayjs": "~1.11", "dotenv": "~16.0", @@ -248,9 +248,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-1.2.2.tgz", - "integrity": "sha512-mOTcJF8IMsz+Xn8QTUP1AC3K5tPl/3f17L2xGTTtLeV/HJ2sTh/3712NFuN58tnOwdISdazId4tHwaqUta8HEA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.0.0.tgz", + "integrity": "sha512-I75IMSo1CvrF8J7S/RoHrvl4yQRWufvFGHSdlGfjBPdYFJ7O9J5hjeDCmv9/WIa8+qbSUmFPPyNZIXibbplSUQ==", "dependencies": { "@microsoft/vscode-azureresources-api": "^2.0.4", "@vscode/extension-telemetry": "^0.6.2", diff --git a/ext/vscode/package.json b/ext/vscode/package.json index c3e8757bdab..8791197f206 100644 --- a/ext/vscode/package.json +++ b/ext/vscode/package.json @@ -498,7 +498,7 @@ "webpack-cli": "~4.10" }, "dependencies": { - "@microsoft/vscode-azext-utils": "~1", + "@microsoft/vscode-azext-utils": "~2", "@microsoft/vscode-azureresources-api": "~2", "dayjs": "~1.11", "dotenv": "~16.0", From ab8b3ccf5b0ac37cb4f28ce5c431b411c211f55f Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:16:24 -0400 Subject: [PATCH 03/28] Make a diagnostic provider --- .../AzureYamlCompletionItemProvider.ts | 15 +-- .../language/AzureYamlDiagnosticCollection.ts | 32 ------ .../language/AzureYamlDiagnosticProvider.ts | 104 ++++++++++++++++++ ext/vscode/src/language/documentDebounce.ts | 38 +++++++ ext/vscode/src/language/languageFeatures.ts | 26 +---- 5 files changed, 156 insertions(+), 59 deletions(-) delete mode 100644 ext/vscode/src/language/AzureYamlDiagnosticCollection.ts create mode 100644 ext/vscode/src/language/AzureYamlDiagnosticProvider.ts create mode 100644 ext/vscode/src/language/documentDebounce.ts diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts index 9ed7453df86..4031d156977 100644 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -23,13 +23,11 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro } private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { - const prefixRange = document.getWordRangeAtPosition(position); + const line = document.lineAt(position.line); + const lineRegex = /\s+project:\s*(?\S*)/i; + const match = lineRegex.exec(line.text); - if (!prefixRange) { - return undefined; - } - - return document.getText(prefixRange); + return match?.groups?.['project']; } private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { @@ -37,12 +35,15 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro return []; } - const currentFolder = vscode.Uri.joinPath(document.uri, '.' + pathPrefix); + const currentFolder = vscode.Uri.joinPath(document.uri, '..', pathPrefix); const results: string[] = []; for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { if (type !== vscode.FileType.Directory) { continue; + } else if (file[0] === '.') { + // Ignore folders that start with '.' + continue; } results.push(file); diff --git a/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts b/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts deleted file mode 100644 index 0e6e0f701ae..00000000000 --- a/ext/vscode/src/language/AzureYamlDiagnosticCollection.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import ext from '../ext'; -import { IActionContext, registerEvent } from '@microsoft/vscode-azext-utils'; - -export let azureYamlDiagnosticCollection: vscode.DiagnosticCollection; - -export function registerDiagnosticCollection(): void { - ext.context.subscriptions.push( - azureYamlDiagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yml') - ); - - registerEvent('onDidChangeTextDocument', vscode.workspace.onDidChangeTextDocument, async (context: IActionContext, e: vscode.TextDocumentChangeEvent) => { - if (e.document.languageId !== 'yaml' || !/azure\.ya?ml$/i.test(e.document.fileName)) { - return; - } - - context.telemetry.suppressAll = true; - context.errorHandling.suppressReportIssue = true; - - // Rerun diagnostics - }); - - registerEvent('onDidRenameFiles', vscode.workspace.onDidRenameFiles, async (context: IActionContext, e: vscode.FileRenameEvent) => { - context.telemetry.suppressAll = true; - context.errorHandling.suppressReportIssue = true; - - // Noop - }); -} \ No newline at end of file diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts new file mode 100644 index 00000000000..500b591d4da --- /dev/null +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import * as yaml from 'yaml'; +import { documentDebounce } from './documentDebounce'; + +// Time between when the user stops typing and when we send diagnostics +const DiagnosticDelay = 1000; + +export class AzureYamlDiagnosticProvider extends vscode.Disposable { + private readonly diagnosticCollection: vscode.DiagnosticCollection; + + public constructor( + private readonly selector: vscode.DocumentSelector + ) { + const disposables: vscode.Disposable[] = []; + + const diagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yaml'); + disposables.push(diagnosticCollection); + + disposables.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => this.updateDiagnosticsFor(e.document))); // TODO: do a delayed debounce + disposables.push(vscode.workspace.onDidRenameFiles(() => this.updateDiagnosticsForOpenTabs())); + disposables.push(vscode.window.onDidChangeVisibleTextEditors(() => this.updateDiagnosticsForOpenTabs())); + + super(() => { + vscode.Disposable.from(...disposables).dispose(); + }); + + this.diagnosticCollection = diagnosticCollection; + } + + public async provideDiagnostics(document: vscode.TextDocument, token?: vscode.CancellationToken): Promise { + const results: vscode.Diagnostic[] = []; + + try { + // Parse the document + const yamlDocument = yaml.parseDocument(document.getText()) as yaml.Document; + if (!yamlDocument || yamlDocument.errors.length > 0) { + throw new Error(vscode.l10n.t('Unable to parse {0}', document.uri.toString())); + } + + const services = yamlDocument.get('services') as yaml.YAMLMap; + + // For each service, ensure that a directory exists matching the relative path specified for the service + for (const service of services?.items || []) { + const projectNode = service.value?.get('project', true) as yaml.Scalar; + const projectPath = projectNode?.value; + + if (!projectPath) { + continue; + } else { + const projectFolder = vscode.Uri.joinPath(document.uri, '..', projectPath); + + try { + const fstat = await vscode.workspace.fs.stat(projectFolder); + + if (fstat.type === vscode.FileType.Directory) { + continue; + } + } catch { + // Suppress the error--we'll emit our diagnostic below + } + } + + // If not, then emit an error diagnostic about it + results.push(new vscode.Diagnostic( + new vscode.Range(document.positionAt(projectNode.range?.[0] ?? 0), document.positionAt(projectNode.range?.[1] ?? 0)), + vscode.l10n.t('The project path must be an existing folder path relative to the azure.yaml file.'), + vscode.DiagnosticSeverity.Error + )); + } + } catch { + // Best effort--the YAML extension will show parsing errors for us if it is present + return results; + } + + return results; + } + + private async updateDiagnosticsFor(document: vscode.TextDocument, delay: boolean = true): Promise { + if (!vscode.languages.match(this.selector, document)) { + return; + } + + const method = async () => { + this.diagnosticCollection.delete(document.uri); + this.diagnosticCollection.set(document.uri, await this.provideDiagnostics(document)); + }; + + if (delay) { + documentDebounce(DiagnosticDelay, { uri: document.uri, callId: 'updateDiagnosticsFor' }, method, this); + } else { + await method(); + } + + } + + private async updateDiagnosticsForOpenTabs(): Promise { + await Promise.all(vscode.window.visibleTextEditors.map(async (editor: vscode.TextEditor) => { + await this.updateDiagnosticsFor(editor.document, false); + })); + } +} diff --git a/ext/vscode/src/language/documentDebounce.ts b/ext/vscode/src/language/documentDebounce.ts new file mode 100644 index 00000000000..d0bcb230f71 --- /dev/null +++ b/ext/vscode/src/language/documentDebounce.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Adapted from https://github.com/microsoft/compose-language-service/blob/main/src/service/utils/debounce.ts with slight changes + +import * as vscode from 'vscode'; + +export interface DebounceId { + uri: vscode.Uri; + callId: string; +} + +const activeDebounces: { [key: string]: vscode.Disposable } = {}; + +export function documentDebounce(delay: number, id: DebounceId, callback: () => Promise | void, thisArg?: unknown): void { + const idString = `${id.uri}/${id.callId}`; + + // If there's an existing call queued up, wipe it out (can't simply refresh as the inputs to the callback may be different) + if (activeDebounces[idString]) { + activeDebounces[idString].dispose(); + delete activeDebounces[idString]; + } + + // Schedule the callback + const timeout = setTimeout(() => { + // Clear the callback since we're about to fire it + activeDebounces[idString].dispose(); + delete activeDebounces[idString]; + + // Fire it + void callback.call(thisArg); + }, delay); + + // Keep track of the active debounce + activeDebounces[idString] = { + dispose: () => clearTimeout(timeout), + }; +} \ No newline at end of file diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 6c4aba353a0..a21534ef611 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -4,7 +4,8 @@ import * as vscode from 'vscode'; import ext from '../ext'; import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; -import { IActionContext, registerEvent } from '@microsoft/vscode-azext-utils'; +import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; +import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; export function registerLanguageFeatures(): void { const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; @@ -13,26 +14,11 @@ export function registerLanguageFeatures(): void { vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), ); - let azureYamlDiagnosticCollection: vscode.DiagnosticCollection; ext.context.subscriptions.push( - azureYamlDiagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yml') + vscode.languages.registerCodeActionsProvider(selector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorMove] }) ); - registerEvent('onDidChangeTextDocument', vscode.workspace.onDidChangeTextDocument, async (context: IActionContext, e: vscode.TextDocumentChangeEvent) => { - if (e.document.languageId !== 'yaml' || !/azure\.ya?ml$/i.test(e.document.fileName)) { - return; - } - - context.telemetry.suppressAll = true; - context.errorHandling.suppressReportIssue = true; - - // Rerun diagnostics - }); - - registerEvent('onDidRenameFiles', vscode.workspace.onDidRenameFiles, async (context: IActionContext, e: vscode.FileRenameEvent) => { - context.telemetry.suppressAll = true; - context.errorHandling.suppressReportIssue = true; - - // Noop - }); + ext.context.subscriptions.push( + new AzureYamlDiagnosticProvider(selector) + ); } \ No newline at end of file From 4fa504f29859781410434daa5c5e82c7a6678219 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:04:29 -0400 Subject: [PATCH 04/28] This TODO is fixed --- ext/vscode/src/language/AzureYamlDiagnosticProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 500b591d4da..fd445d8731c 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -19,7 +19,7 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { const diagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yaml'); disposables.push(diagnosticCollection); - disposables.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => this.updateDiagnosticsFor(e.document))); // TODO: do a delayed debounce + disposables.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => this.updateDiagnosticsFor(e.document))); disposables.push(vscode.workspace.onDidRenameFiles(() => this.updateDiagnosticsForOpenTabs())); disposables.push(vscode.window.onDidChangeVisibleTextEditors(() => this.updateDiagnosticsForOpenTabs())); From b48329191f5bb1962ecfa835967cbb2710b61b01 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:11:22 -0400 Subject: [PATCH 05/28] Add trailing newlines --- ext/vscode/src/language/documentDebounce.ts | 2 +- ext/vscode/src/language/languageFeatures.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/vscode/src/language/documentDebounce.ts b/ext/vscode/src/language/documentDebounce.ts index d0bcb230f71..62787fe1101 100644 --- a/ext/vscode/src/language/documentDebounce.ts +++ b/ext/vscode/src/language/documentDebounce.ts @@ -35,4 +35,4 @@ export function documentDebounce(delay: number, id: DebounceId, callback: () => activeDebounces[idString] = { dispose: () => clearTimeout(timeout), }; -} \ No newline at end of file +} diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 44cdf929747..1cb1e3052fb 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -11,4 +11,4 @@ export function registerLanguageFeatures(): void { ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(selector) ); -} \ No newline at end of file +} From 7e517e8b795fb041e886e296bd05f873f625c64c Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:17:38 -0400 Subject: [PATCH 06/28] Add back completion provider --- .../AzureYamlCompletionItemProvider.ts | 54 +++++++++++++++++++ ext/vscode/src/language/languageFeatures.ts | 7 ++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 ext/vscode/src/language/AzureYamlCompletionItemProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts new file mode 100644 index 00000000000..5d77354ecff --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { + public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { + // if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter || + // context.triggerCharacter !== '/') { + // // Shouldn't have been triggered, return an empty set + // return []; + // } + + const pathPrefix = this.getPathPrefix(document, position); + const matchingPaths = await this.getMatchingPaths(document, pathPrefix, token); + + return matchingPaths.map((path) => { + const completionItem = new vscode.CompletionItem(path); + completionItem.insertText = path; + completionItem.kind = vscode.CompletionItemKind.Folder; + return completionItem; + }); + } + + private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { + const line = document.lineAt(position.line); + const lineRegex = /\s+project:\s*(?\S*)/i; + const match = lineRegex.exec(line.text); + + return match?.groups?.['project']; + } + + private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { + if (!pathPrefix || pathPrefix[0] !== '.') { + return []; + } + + const currentFolder = vscode.Uri.joinPath(document.uri, '..', pathPrefix); + const results: string[] = []; + + for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { + if (type !== vscode.FileType.Directory) { + continue; + } else if (file[0] === '.') { + // Ignore folders that start with '.' + continue; + } + + results.push(file); + } + + return results; + } +} diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 1cb1e3052fb..4319591b702 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -3,12 +3,17 @@ import * as vscode from 'vscode'; import ext from '../ext'; +import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; export function registerLanguageFeatures(): void { const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; + ext.context.subscriptions.push( + vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), + ); + ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(selector) ); -} +} \ No newline at end of file From d81d6ca872413ef45fe461034f0e42f8345bbece Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:18:10 -0400 Subject: [PATCH 07/28] Add back code action provider --- ext/vscode/src/language/AzureYamlCodeActionProvider.ts | 10 ++++++++++ ext/vscode/src/language/languageFeatures.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 ext/vscode/src/language/AzureYamlCodeActionProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts new file mode 100644 index 00000000000..211729919f6 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export class AzureYamlCodeActionProvider implements vscode.CodeActionProvider { + public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 4319591b702..98dc7c2f44d 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 { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; +import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; export function registerLanguageFeatures(): void { const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; @@ -13,7 +14,11 @@ export function registerLanguageFeatures(): void { vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), ); + ext.context.subscriptions.push( + vscode.languages.registerCodeActionsProvider(selector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorMove] }) + ); + ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(selector) ); -} \ No newline at end of file +} From da31daf0c49752c8d61c91688f9666bc14194e5b Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:37:13 -0400 Subject: [PATCH 08/28] Karol's feedback --- ext/vscode/src/language/AzureYamlDiagnosticProvider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index fd445d8731c..99b621e1a6a 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -63,9 +63,13 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { } } - // If not, then emit an error diagnostic about it + const rangeStart = document.positionAt(projectNode.range?.[0] ?? 0); + const rangeEnd = document.positionAt(projectNode.range?.[1] ?? 0); + const range = new vscode.Range(rangeStart, rangeEnd); + + // If not existent, then emit an error diagnostic about it results.push(new vscode.Diagnostic( - new vscode.Range(document.positionAt(projectNode.range?.[0] ?? 0), document.positionAt(projectNode.range?.[1] ?? 0)), + range, vscode.l10n.t('The project path must be an existing folder path relative to the azure.yaml file.'), vscode.DiagnosticSeverity.Error )); From 2eb80d222446ab01c450c2eb577b6eaa9e8ede37 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:28:46 -0400 Subject: [PATCH 09/28] Commit current progress --- .../language/AzureYamlCodeActionProvider.ts | 62 ++++++++++++++++++- .../AzureYamlCompletionItemProvider.ts | 3 +- .../language/AzureYamlDiagnosticProvider.ts | 44 ++++++++----- .../src/language/getContainingFolderUri.ts | 8 +++ 4 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 ext/vscode/src/language/getContainingFolderUri.ts diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts index 211729919f6..bf013e4b52a 100644 --- a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts +++ b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts @@ -1,10 +1,68 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { isAzureYamlProjectPathDiagnostic } from './AzureYamlDiagnosticProvider'; +import { getContainingFolderUri } from './getContainingFolderUri'; + +export class AzureYamlCodeActionProvider extends vscode.Disposable implements vscode.CodeActionProvider { + private readonly knownFolderRenames: { oldFolder: vscode.Uri, newFolder: vscode.Uri }[] = []; + + public constructor() { + const disposables: vscode.Disposable[] = []; + + disposables.push(vscode.workspace.onDidRenameFiles(e => this.onDidRenameFiles(e))); + + super(() => { + vscode.Disposable.from(...disposables).dispose(); + }); + } -export class AzureYamlCodeActionProvider implements vscode.CodeActionProvider { public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { - throw new Error('Method not implemented.'); + const diagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter(isAzureYamlProjectPathDiagnostic); + + if (!diagnostics || diagnostics.length === 0) { + // Nothing to do + return []; + } + + const results: vscode.CodeAction[] = []; + + for (const diagnostic of diagnostics) { + const azureYamlFolder = getContainingFolderUri(document.uri); + const missingFolder = vscode.Uri.joinPath(azureYamlFolder, diagnostic.sourceNode.value); + + const knownFolderRename = this.knownFolderRenames.find(r => r.oldFolder.fsPath === missingFolder.fsPath); + if (knownFolderRename) { + const newRelativeFolder = path.posix + .normalize( + path.relative(azureYamlFolder.fsPath, knownFolderRename.newFolder.fsPath) + ) + .replace(/\\/g, '/') // Turn backslashes into forward slashes + .replace(/^\.?\/?/, './'); // Ensure it starts with ./ + + + const action = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); + + const edit = new vscode.WorkspaceEdit(); + edit.replace(document.uri, diagnostic.range, newRelativeFolder); + action.edit = edit; + + results.push(action); + } + } + + return results; + } + + private async onDidRenameFiles(e: vscode.FileRenameEvent): Promise { + if (await AzExtFsExtra.isDirectory(e.files[0].newUri)) { + // If the new URI is a directory, then this is a folder rename event, and we should keep track + this.knownFolderRenames.push({ oldFolder: e.files[0].oldUri, newFolder: e.files[0].newUri }); + } } } diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts index 5d77354ecff..a4101cb7fb3 100644 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as vscode from 'vscode'; +import { getContainingFolderUri } from './getContainingFolderUri'; export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { @@ -35,7 +36,7 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro return []; } - const currentFolder = vscode.Uri.joinPath(document.uri, '..', pathPrefix); + const currentFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), pathPrefix); const results: string[] = []; for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 99b621e1a6a..a6126c27a15 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import * as yaml from 'yaml'; import { documentDebounce } from './documentDebounce'; +import { getContainingFolderUri } from './getContainingFolderUri'; // Time between when the user stops typing and when we send diagnostics const DiagnosticDelay = 1000; @@ -50,29 +52,26 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { if (!projectPath) { continue; } else { - const projectFolder = vscode.Uri.joinPath(document.uri, '..', projectPath); - - try { - const fstat = await vscode.workspace.fs.stat(projectFolder); - - if (fstat.type === vscode.FileType.Directory) { - continue; - } - } catch { - // Suppress the error--we'll emit our diagnostic below + const projectFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), projectPath); + + if (await AzExtFsExtra.pathExists(projectFolder) && await AzExtFsExtra.isDirectory(projectFolder)) { + continue; } } + // If not existent, then emit an error diagnostic about it const rangeStart = document.positionAt(projectNode.range?.[0] ?? 0); const rangeEnd = document.positionAt(projectNode.range?.[1] ?? 0); const range = new vscode.Range(rangeStart, rangeEnd); - // If not existent, then emit an error diagnostic about it - results.push(new vscode.Diagnostic( + const diagnostic = new AzureYamlProjectPathDiagnostic( range, vscode.l10n.t('The project path must be an existing folder path relative to the azure.yaml file.'), - vscode.DiagnosticSeverity.Error - )); + vscode.DiagnosticSeverity.Error, + projectNode + ); + + results.push(diagnostic); } } catch { // Best effort--the YAML extension will show parsing errors for us if it is present @@ -106,3 +105,20 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { })); } } + +export class AzureYamlProjectPathDiagnostic extends vscode.Diagnostic { + public readonly isAzureYamlProjectPathDiagnostic: boolean = true; + + public constructor( + range: vscode.Range, + message: string, + severity: vscode.DiagnosticSeverity, + public readonly sourceNode: yaml.Scalar + ) { + super(range, message, severity); + } +} + +export function isAzureYamlProjectPathDiagnostic(diagnostic: vscode.Diagnostic): diagnostic is AzureYamlProjectPathDiagnostic { + return (diagnostic as AzureYamlProjectPathDiagnostic).isAzureYamlProjectPathDiagnostic; +} \ No newline at end of file diff --git a/ext/vscode/src/language/getContainingFolderUri.ts b/ext/vscode/src/language/getContainingFolderUri.ts new file mode 100644 index 00000000000..db14fcd8b85 --- /dev/null +++ b/ext/vscode/src/language/getContainingFolderUri.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; + +export function getContainingFolderUri(targetUri: vscode.Uri): vscode.Uri { + return vscode.Uri.joinPath(targetUri, '..'); +} From f72cde41fda30756be160af8042fd9764529d577 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:32:10 -0400 Subject: [PATCH 10/28] Matt's feedback --- .../language/AzureYamlCodeActionProvider.ts | 68 ------------------- .../AzureYamlCompletionItemProvider.ts | 55 --------------- .../language/AzureYamlDiagnosticProvider.ts | 2 +- ext/vscode/src/language/languageFeatures.ts | 10 --- 4 files changed, 1 insertion(+), 134 deletions(-) delete mode 100644 ext/vscode/src/language/AzureYamlCodeActionProvider.ts delete mode 100644 ext/vscode/src/language/AzureYamlCompletionItemProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts deleted file mode 100644 index bf013e4b52a..00000000000 --- a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { isAzureYamlProjectPathDiagnostic } from './AzureYamlDiagnosticProvider'; -import { getContainingFolderUri } from './getContainingFolderUri'; - -export class AzureYamlCodeActionProvider extends vscode.Disposable implements vscode.CodeActionProvider { - private readonly knownFolderRenames: { oldFolder: vscode.Uri, newFolder: vscode.Uri }[] = []; - - public constructor() { - const disposables: vscode.Disposable[] = []; - - disposables.push(vscode.workspace.onDidRenameFiles(e => this.onDidRenameFiles(e))); - - super(() => { - vscode.Disposable.from(...disposables).dispose(); - }); - } - - public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { - const diagnostics = vscode.languages - .getDiagnostics(document.uri) - .filter(isAzureYamlProjectPathDiagnostic); - - if (!diagnostics || diagnostics.length === 0) { - // Nothing to do - return []; - } - - const results: vscode.CodeAction[] = []; - - for (const diagnostic of diagnostics) { - const azureYamlFolder = getContainingFolderUri(document.uri); - const missingFolder = vscode.Uri.joinPath(azureYamlFolder, diagnostic.sourceNode.value); - - const knownFolderRename = this.knownFolderRenames.find(r => r.oldFolder.fsPath === missingFolder.fsPath); - if (knownFolderRename) { - const newRelativeFolder = path.posix - .normalize( - path.relative(azureYamlFolder.fsPath, knownFolderRename.newFolder.fsPath) - ) - .replace(/\\/g, '/') // Turn backslashes into forward slashes - .replace(/^\.?\/?/, './'); // Ensure it starts with ./ - - - const action = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); - - const edit = new vscode.WorkspaceEdit(); - edit.replace(document.uri, diagnostic.range, newRelativeFolder); - action.edit = edit; - - results.push(action); - } - } - - return results; - } - - private async onDidRenameFiles(e: vscode.FileRenameEvent): Promise { - if (await AzExtFsExtra.isDirectory(e.files[0].newUri)) { - // If the new URI is a directory, then this is a folder rename event, and we should keep track - this.knownFolderRenames.push({ oldFolder: e.files[0].oldUri, newFolder: e.files[0].newUri }); - } - } -} diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts deleted file mode 100644 index a4101cb7fb3..00000000000 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { getContainingFolderUri } from './getContainingFolderUri'; - -export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { - public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { - // if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter || - // context.triggerCharacter !== '/') { - // // Shouldn't have been triggered, return an empty set - // return []; - // } - - const pathPrefix = this.getPathPrefix(document, position); - const matchingPaths = await this.getMatchingPaths(document, pathPrefix, token); - - return matchingPaths.map((path) => { - const completionItem = new vscode.CompletionItem(path); - completionItem.insertText = path; - completionItem.kind = vscode.CompletionItemKind.Folder; - return completionItem; - }); - } - - private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { - const line = document.lineAt(position.line); - const lineRegex = /\s+project:\s*(?\S*)/i; - const match = lineRegex.exec(line.text); - - return match?.groups?.['project']; - } - - private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { - if (!pathPrefix || pathPrefix[0] !== '.') { - return []; - } - - const currentFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), pathPrefix); - const results: string[] = []; - - for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { - if (type !== vscode.FileType.Directory) { - continue; - } else if (file[0] === '.') { - // Ignore folders that start with '.' - continue; - } - - results.push(file); - } - - return results; - } -} diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index a6126c27a15..302c2c9cdae 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -54,7 +54,7 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { } else { const projectFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), projectPath); - if (await AzExtFsExtra.pathExists(projectFolder) && await AzExtFsExtra.isDirectory(projectFolder)) { + if (await AzExtFsExtra.pathExists(projectFolder)) { continue; } } diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 98dc7c2f44d..1cb1e3052fb 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -3,21 +3,11 @@ import * as vscode from 'vscode'; import ext from '../ext'; -import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; -import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; export function registerLanguageFeatures(): void { const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; - ext.context.subscriptions.push( - vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), - ); - - ext.context.subscriptions.push( - vscode.languages.registerCodeActionsProvider(selector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorMove] }) - ); - ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(selector) ); From 2d92b8fc902b794681ee8d4a9e186f13ba0daeb9 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:34:43 -0400 Subject: [PATCH 11/28] Please stop deleting my files --- .../language/AzureYamlCodeActionProvider.ts | 68 +++++++++++++++++++ .../AzureYamlCompletionItemProvider.ts | 55 +++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 ext/vscode/src/language/AzureYamlCodeActionProvider.ts create mode 100644 ext/vscode/src/language/AzureYamlCompletionItemProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts new file mode 100644 index 00000000000..bf013e4b52a --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { isAzureYamlProjectPathDiagnostic } from './AzureYamlDiagnosticProvider'; +import { getContainingFolderUri } from './getContainingFolderUri'; + +export class AzureYamlCodeActionProvider extends vscode.Disposable implements vscode.CodeActionProvider { + private readonly knownFolderRenames: { oldFolder: vscode.Uri, newFolder: vscode.Uri }[] = []; + + public constructor() { + const disposables: vscode.Disposable[] = []; + + disposables.push(vscode.workspace.onDidRenameFiles(e => this.onDidRenameFiles(e))); + + super(() => { + vscode.Disposable.from(...disposables).dispose(); + }); + } + + public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { + const diagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter(isAzureYamlProjectPathDiagnostic); + + if (!diagnostics || diagnostics.length === 0) { + // Nothing to do + return []; + } + + const results: vscode.CodeAction[] = []; + + for (const diagnostic of diagnostics) { + const azureYamlFolder = getContainingFolderUri(document.uri); + const missingFolder = vscode.Uri.joinPath(azureYamlFolder, diagnostic.sourceNode.value); + + const knownFolderRename = this.knownFolderRenames.find(r => r.oldFolder.fsPath === missingFolder.fsPath); + if (knownFolderRename) { + const newRelativeFolder = path.posix + .normalize( + path.relative(azureYamlFolder.fsPath, knownFolderRename.newFolder.fsPath) + ) + .replace(/\\/g, '/') // Turn backslashes into forward slashes + .replace(/^\.?\/?/, './'); // Ensure it starts with ./ + + + const action = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); + + const edit = new vscode.WorkspaceEdit(); + edit.replace(document.uri, diagnostic.range, newRelativeFolder); + action.edit = edit; + + results.push(action); + } + } + + return results; + } + + private async onDidRenameFiles(e: vscode.FileRenameEvent): Promise { + if (await AzExtFsExtra.isDirectory(e.files[0].newUri)) { + // If the new URI is a directory, then this is a folder rename event, and we should keep track + this.knownFolderRenames.push({ oldFolder: e.files[0].oldUri, newFolder: e.files[0].newUri }); + } + } +} diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts new file mode 100644 index 00000000000..a4101cb7fb3 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { getContainingFolderUri } from './getContainingFolderUri'; + +export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { + public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { + // if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter || + // context.triggerCharacter !== '/') { + // // Shouldn't have been triggered, return an empty set + // return []; + // } + + const pathPrefix = this.getPathPrefix(document, position); + const matchingPaths = await this.getMatchingPaths(document, pathPrefix, token); + + return matchingPaths.map((path) => { + const completionItem = new vscode.CompletionItem(path); + completionItem.insertText = path; + completionItem.kind = vscode.CompletionItemKind.Folder; + return completionItem; + }); + } + + private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { + const line = document.lineAt(position.line); + const lineRegex = /\s+project:\s*(?\S*)/i; + const match = lineRegex.exec(line.text); + + return match?.groups?.['project']; + } + + private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { + if (!pathPrefix || pathPrefix[0] !== '.') { + return []; + } + + const currentFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), pathPrefix); + const results: string[] = []; + + for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { + if (type !== vscode.FileType.Directory) { + continue; + } else if (file[0] === '.') { + // Ignore folders that start with '.' + continue; + } + + results.push(file); + } + + return results; + } +} From 6da3ad9fd0742528611c2ef631ecb29303c818cb Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:34:55 -0400 Subject: [PATCH 12/28] But seriously --- ext/vscode/src/language/languageFeatures.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 1cb1e3052fb..98dc7c2f44d 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -3,11 +3,21 @@ import * as vscode from 'vscode'; import ext from '../ext'; +import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; +import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; export function registerLanguageFeatures(): void { const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; + ext.context.subscriptions.push( + vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), + ); + + ext.context.subscriptions.push( + vscode.languages.registerCodeActionsProvider(selector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorMove] }) + ); + ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(selector) ); From a7abbc32feed769f19321e56bc60ba3212ec53ac Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:48:51 -0400 Subject: [PATCH 13/28] Minor changes --- .../src/language/AzureYamlCodeActionProvider.ts | 11 ++++++----- .../src/language/AzureYamlDiagnosticProvider.ts | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts index bf013e4b52a..e3d6352676e 100644 --- a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts +++ b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts @@ -36,6 +36,7 @@ export class AzureYamlCodeActionProvider extends vscode.Disposable implements vs const azureYamlFolder = getContainingFolderUri(document.uri); const missingFolder = vscode.Uri.joinPath(azureYamlFolder, diagnostic.sourceNode.value); + // Add a code action to rename the folder if possible const knownFolderRename = this.knownFolderRenames.find(r => r.oldFolder.fsPath === missingFolder.fsPath); if (knownFolderRename) { const newRelativeFolder = path.posix @@ -46,13 +47,13 @@ export class AzureYamlCodeActionProvider extends vscode.Disposable implements vs .replace(/^\.?\/?/, './'); // Ensure it starts with ./ - const action = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); + const renameFolderAction = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); - const edit = new vscode.WorkspaceEdit(); - edit.replace(document.uri, diagnostic.range, newRelativeFolder); - action.edit = edit; + const renameFolderEdit = new vscode.WorkspaceEdit(); + renameFolderEdit.replace(document.uri, diagnostic.range, newRelativeFolder); + renameFolderAction.edit = renameFolderEdit; - results.push(action); + results.push(renameFolderAction); } } diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 302c2c9cdae..cd729bd2129 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -23,6 +23,8 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { disposables.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => this.updateDiagnosticsFor(e.document))); disposables.push(vscode.workspace.onDidRenameFiles(() => this.updateDiagnosticsForOpenTabs())); + disposables.push(vscode.workspace.onDidCreateFiles(() => this.updateDiagnosticsForOpenTabs())); + disposables.push(vscode.workspace.onDidDeleteFiles(() => this.updateDiagnosticsForOpenTabs())); disposables.push(vscode.window.onDidChangeVisibleTextEditors(() => this.updateDiagnosticsForOpenTabs())); super(() => { From e6771da42cf00383cabfff8c7e37cd78f4fb71e6 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 11:45:13 -0400 Subject: [PATCH 14/28] Commit current progress --- .../language/AzureYamlDiagnosticProvider.ts | 36 +++--------- .../getAzureYamlProjectInformation.ts | 56 +++++++++++++++++++ ext/vscode/src/language/languageFeatures.ts | 10 ++-- 3 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 ext/vscode/src/language/getAzureYamlProjectInformation.ts diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index cd729bd2129..9f5c1fc4eba 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -3,9 +3,8 @@ import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -import * as yaml from 'yaml'; import { documentDebounce } from './documentDebounce'; -import { getContainingFolderUri } from './getContainingFolderUri'; +import { getAzureYamlProjectInformation } from './getAzureYamlProjectInformation'; // Time between when the user stops typing and when we send diagnostics const DiagnosticDelay = 1000; @@ -38,39 +37,18 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { const results: vscode.Diagnostic[] = []; try { - // Parse the document - const yamlDocument = yaml.parseDocument(document.getText()) as yaml.Document; - if (!yamlDocument || yamlDocument.errors.length > 0) { - throw new Error(vscode.l10n.t('Unable to parse {0}', document.uri.toString())); - } - - const services = yamlDocument.get('services') as yaml.YAMLMap; - - // For each service, ensure that a directory exists matching the relative path specified for the service - for (const service of services?.items || []) { - const projectNode = service.value?.get('project', true) as yaml.Scalar; - const projectPath = projectNode?.value; + const projectInformation = await getAzureYamlProjectInformation(document); - if (!projectPath) { + for (const project of projectInformation) { + if (await AzExtFsExtra.pathExists(project.projectUri)) { continue; - } else { - const projectFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), projectPath); - - if (await AzExtFsExtra.pathExists(projectFolder)) { - continue; - } } - // If not existent, then emit an error diagnostic about it - const rangeStart = document.positionAt(projectNode.range?.[0] ?? 0); - const rangeEnd = document.positionAt(projectNode.range?.[1] ?? 0); - const range = new vscode.Range(rangeStart, rangeEnd); - const diagnostic = new AzureYamlProjectPathDiagnostic( - range, + project.projectValueNodeRange, vscode.l10n.t('The project path must be an existing folder path relative to the azure.yaml file.'), vscode.DiagnosticSeverity.Error, - projectNode + project.projectValue, ); results.push(diagnostic); @@ -115,7 +93,7 @@ export class AzureYamlProjectPathDiagnostic extends vscode.Diagnostic { range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, - public readonly sourceNode: yaml.Scalar + public readonly projectValue: string ) { super(range, message, severity); } diff --git a/ext/vscode/src/language/getAzureYamlProjectInformation.ts b/ext/vscode/src/language/getAzureYamlProjectInformation.ts new file mode 100644 index 00000000000..1de6d20c66d --- /dev/null +++ b/ext/vscode/src/language/getAzureYamlProjectInformation.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import * as yaml from 'yaml'; +import { AzureYamlSelector } from './languageFeatures'; +import { getContainingFolderUri } from './getContainingFolderUri'; + +interface AzureYamlProjectInformation { + azureYamlUri: vscode.Uri; + serviceName: string; + + projectValue: string; + projectUri: vscode.Uri; + + projectValueNodeRange: vscode.Range; +} + +export async function getAzureYamlProjectInformation(document: vscode.TextDocument): Promise { + if (!vscode.languages.match(AzureYamlSelector, document)) { + throw new Error('Document is not an Azure YAML file'); + } + + // Parse the document + const yamlDocument = yaml.parseDocument(document.getText()) as yaml.Document; + if (!yamlDocument || yamlDocument.errors.length > 0) { + throw new Error(vscode.l10n.t('Unable to parse {0}', document.uri.toString())); + } + + const results: AzureYamlProjectInformation[] = []; + + const services = yamlDocument.get('services') as yaml.YAMLMap, yaml.YAMLMap>; + + // For each service, ensure that a directory exists matching the relative path specified for the service + for (const service of services?.items || []) { + const projectNode = service.value?.get('project', true) as yaml.Scalar; + const projectPath = projectNode?.value; + + if (!projectNode || !projectPath || !service.key || !projectNode.range?.[0] || !projectNode.range?.[1]) { + continue; + } + + results.push({ + azureYamlUri: document.uri, + serviceName: service.key.value, + projectValue: projectPath, + projectUri: vscode.Uri.joinPath(getContainingFolderUri(document.uri), projectPath), + projectValueNodeRange: new vscode.Range( + document.positionAt(projectNode.range[0]), + document.positionAt(projectNode.range[1]) + ), + }); + } + + return results; +} \ No newline at end of file diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 98dc7c2f44d..d7e48db329f 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -7,18 +7,18 @@ import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvid import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; -export function registerLanguageFeatures(): void { - const selector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; +export const AzureYamlSelector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; +export function registerLanguageFeatures(): void { ext.context.subscriptions.push( - vscode.languages.registerCompletionItemProvider(selector, new AzureYamlCompletionItemProvider(), '/'), + vscode.languages.registerCompletionItemProvider(AzureYamlSelector, new AzureYamlCompletionItemProvider(), '/'), ); ext.context.subscriptions.push( - vscode.languages.registerCodeActionsProvider(selector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorMove] }) + vscode.languages.registerCodeActionsProvider(AzureYamlSelector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }) ); ext.context.subscriptions.push( - new AzureYamlDiagnosticProvider(selector) + new AzureYamlDiagnosticProvider(AzureYamlSelector) ); } From e89db6976c3c6e9cc718c4e43826c79114416f1e Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:18:31 -0400 Subject: [PATCH 15/28] Use a proactive handler --- .../language/AzureYamlCodeActionProvider.ts | 69 ------------------- .../AzureYamlProjectRenameProvider.ts | 52 ++++++++++++++ ext/vscode/src/language/languageFeatures.ts | 6 +- 3 files changed, 55 insertions(+), 72 deletions(-) delete mode 100644 ext/vscode/src/language/AzureYamlCodeActionProvider.ts create mode 100644 ext/vscode/src/language/AzureYamlProjectRenameProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts b/ext/vscode/src/language/AzureYamlCodeActionProvider.ts deleted file mode 100644 index e3d6352676e..00000000000 --- a/ext/vscode/src/language/AzureYamlCodeActionProvider.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { isAzureYamlProjectPathDiagnostic } from './AzureYamlDiagnosticProvider'; -import { getContainingFolderUri } from './getContainingFolderUri'; - -export class AzureYamlCodeActionProvider extends vscode.Disposable implements vscode.CodeActionProvider { - private readonly knownFolderRenames: { oldFolder: vscode.Uri, newFolder: vscode.Uri }[] = []; - - public constructor() { - const disposables: vscode.Disposable[] = []; - - disposables.push(vscode.workspace.onDidRenameFiles(e => this.onDidRenameFiles(e))); - - super(() => { - vscode.Disposable.from(...disposables).dispose(); - }); - } - - public async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { - const diagnostics = vscode.languages - .getDiagnostics(document.uri) - .filter(isAzureYamlProjectPathDiagnostic); - - if (!diagnostics || diagnostics.length === 0) { - // Nothing to do - return []; - } - - const results: vscode.CodeAction[] = []; - - for (const diagnostic of diagnostics) { - const azureYamlFolder = getContainingFolderUri(document.uri); - const missingFolder = vscode.Uri.joinPath(azureYamlFolder, diagnostic.sourceNode.value); - - // Add a code action to rename the folder if possible - const knownFolderRename = this.knownFolderRenames.find(r => r.oldFolder.fsPath === missingFolder.fsPath); - if (knownFolderRename) { - const newRelativeFolder = path.posix - .normalize( - path.relative(azureYamlFolder.fsPath, knownFolderRename.newFolder.fsPath) - ) - .replace(/\\/g, '/') // Turn backslashes into forward slashes - .replace(/^\.?\/?/, './'); // Ensure it starts with ./ - - - const renameFolderAction = new vscode.CodeAction(vscode.l10n.t('Change path to "{0}"', newRelativeFolder), vscode.CodeActionKind.QuickFix); - - const renameFolderEdit = new vscode.WorkspaceEdit(); - renameFolderEdit.replace(document.uri, diagnostic.range, newRelativeFolder); - renameFolderAction.edit = renameFolderEdit; - - results.push(renameFolderAction); - } - } - - return results; - } - - private async onDidRenameFiles(e: vscode.FileRenameEvent): Promise { - if (await AzExtFsExtra.isDirectory(e.files[0].newUri)) { - // If the new URI is a directory, then this is a folder rename event, and we should keep track - this.knownFolderRenames.push({ oldFolder: e.files[0].oldUri, newFolder: e.files[0].newUri }); - } - } -} diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts new file mode 100644 index 00000000000..23cd23424e7 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getAzureYamlProjectInformation } from './getAzureYamlProjectInformation'; + +export class AzureYamlProjectRenameProvider extends vscode.Disposable { + public constructor() { + const disposables: vscode.Disposable[] = []; + disposables.push(vscode.workspace.onWillRenameFiles(evt => this.handleWillRenameFile(evt))); + + super(() => { + vscode.Disposable.from(...disposables).dispose(); + }); + } + + public async provideWorkspaceEdits(oldUri: vscode.Uri, newUri: vscode.Uri): Promise { + if (!await AzExtFsExtra.isDirectory(oldUri)) { + return undefined; + } + + const azureYamlUris = await vscode.workspace.findFiles('**/azure.{yml,yaml}'); + 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 new vscode.WorkspaceEdit(); + } + + const newRelativePath = path.posix.normalize(path.relative(path.dirname(azureYamlUri.fsPath), newUri.fsPath)).replace(/\\/g, '/').replace(/^\.?\/?/, './'); + const projectUriEdit = new vscode.WorkspaceEdit(); + projectUriEdit.replace(azureYamlUri, projectToUpdate.projectValueNodeRange, newRelativePath); + return projectUriEdit; + } + + private handleWillRenameFile(evt: vscode.FileWillRenameEvent): void { + // 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 + const oldUri = evt.files[0].oldUri; + const newUri = evt.files[0].newUri; + + evt.waitUntil(this.provideWorkspaceEdits(oldUri, newUri)); + } +} \ No newline at end of file diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index d7e48db329f..910361a30e7 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import ext from '../ext'; import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; -import { AzureYamlCodeActionProvider } from './AzureYamlCodeActionProvider'; +import { AzureYamlProjectRenameProvider } from './AzureYamlProjectRenameProvider'; export const AzureYamlSelector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; @@ -15,10 +15,10 @@ export function registerLanguageFeatures(): void { ); ext.context.subscriptions.push( - vscode.languages.registerCodeActionsProvider(AzureYamlSelector, new AzureYamlCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }) + new AzureYamlDiagnosticProvider(AzureYamlSelector) ); ext.context.subscriptions.push( - new AzureYamlDiagnosticProvider(AzureYamlSelector) + new AzureYamlProjectRenameProvider() ); } From 7dbc2c396fa042560eced0fe349990012793089c Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:42:28 -0400 Subject: [PATCH 16/28] Minor changes --- .../AzureYamlCompletionItemProvider.ts | 4 ++-- .../language/AzureYamlDiagnosticProvider.ts | 22 ++----------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts index a4101cb7fb3..f34aff0044d 100644 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -13,7 +13,7 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro // } const pathPrefix = this.getPathPrefix(document, position); - const matchingPaths = await this.getMatchingPaths(document, pathPrefix, token); + const matchingPaths = await this.getMatchingWorkspacePaths(document, pathPrefix, token); return matchingPaths.map((path) => { const completionItem = new vscode.CompletionItem(path); @@ -31,7 +31,7 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro return match?.groups?.['project']; } - private async getMatchingPaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { + private async getMatchingWorkspacePaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { if (!pathPrefix || pathPrefix[0] !== '.') { return []; } diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 9f5c1fc4eba..9ef9fe7aa73 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -44,11 +44,10 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { continue; } - const diagnostic = new AzureYamlProjectPathDiagnostic( + const diagnostic = new vscode.Diagnostic( project.projectValueNodeRange, vscode.l10n.t('The project path must be an existing folder path relative to the azure.yaml file.'), - vscode.DiagnosticSeverity.Error, - project.projectValue, + vscode.DiagnosticSeverity.Error ); results.push(diagnostic); @@ -85,20 +84,3 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { })); } } - -export class AzureYamlProjectPathDiagnostic extends vscode.Diagnostic { - public readonly isAzureYamlProjectPathDiagnostic: boolean = true; - - public constructor( - range: vscode.Range, - message: string, - severity: vscode.DiagnosticSeverity, - public readonly projectValue: string - ) { - super(range, message, severity); - } -} - -export function isAzureYamlProjectPathDiagnostic(diagnostic: vscode.Diagnostic): diagnostic is AzureYamlProjectPathDiagnostic { - return (diagnostic as AzureYamlProjectPathDiagnostic).isAzureYamlProjectPathDiagnostic; -} \ No newline at end of file From 4a32798b9ac96750e9dc41cb1cfd24340d687266 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:16:39 -0400 Subject: [PATCH 17/28] Add a mind-blowing document drop provider --- ext/vscode/.vscode/cspell-dictionary.txt | 3 ++ .../AzureYamlDocumentDropEditProvider.ts | 31 +++++++++++++++++++ ext/vscode/src/language/languageFeatures.ts | 5 +++ 3 files changed, 39 insertions(+) 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 036203c717d..70f49abc7cd 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/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts new file mode 100644 index 00000000000..950aa5a62a3 --- /dev/null +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEditProvider { + public async provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const maybeFolder = dataTransfer.get('text/uri-list')?.value; + const folder = vscode.Uri.parse(maybeFolder); + + const basename = path.basename(folder.fsPath); + const newRelativePath = path.posix.normalize(path.relative(path.dirname(document.uri.fsPath), folder.fsPath)).replace(/\\/g, '/').replace(/^\.?\/?/, './'); + + if (await AzExtFsExtra.pathExists(folder) && await AzExtFsExtra.isDirectory(folder)) { + const snippet = new vscode.SnippetString('\t') + .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'); + return new vscode.DocumentDropEdit(snippet); + } + + return undefined; + } +} \ No newline at end of file diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 910361a30e7..955580872de 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -6,6 +6,7 @@ import ext from '../ext'; import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; 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}' }; @@ -21,4 +22,8 @@ export function registerLanguageFeatures(): void { ext.context.subscriptions.push( new AzureYamlProjectRenameProvider() ); + + ext.context.subscriptions.push( + vscode.languages.registerDocumentDropEditProvider(AzureYamlSelector, new AzureYamlDocumentDropEditProvider()) + ); } From accaf4b8004b4131498e83c5653aabc3aef0391a Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:22:22 -0400 Subject: [PATCH 18/28] General cleanup refactoring --- .../language/AzureYamlCompletionItemProvider.ts | 2 +- .../src/language/AzureYamlDiagnosticProvider.ts | 2 +- .../language/AzureYamlDocumentDropEditProvider.ts | 10 ++++++---- .../language/AzureYamlProjectRenameProvider.ts | 5 ++--- ...amlProjectInformation.ts => azureYamlUtils.ts} | 15 ++++++++++++++- ext/vscode/src/language/getContainingFolderUri.ts | 8 -------- 6 files changed, 24 insertions(+), 18 deletions(-) rename ext/vscode/src/language/{getAzureYamlProjectInformation.ts => azureYamlUtils.ts} (74%) delete mode 100644 ext/vscode/src/language/getContainingFolderUri.ts diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts index f34aff0044d..42b9966c981 100644 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as vscode from 'vscode'; -import { getContainingFolderUri } from './getContainingFolderUri'; +import { getContainingFolderUri } from './azureYamlUtils'; export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 9ef9fe7aa73..07ae7be7ebf 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -4,7 +4,7 @@ import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { documentDebounce } from './documentDebounce'; -import { getAzureYamlProjectInformation } from './getAzureYamlProjectInformation'; +import { getAzureYamlProjectInformation } from './azureYamlUtils'; // Time between when the user stops typing and when we send diagnostics const DiagnosticDelay = 1000; diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts index 950aa5a62a3..3f2786cce30 100644 --- a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -4,16 +4,18 @@ import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import * as path from 'path'; import * as vscode from 'vscode'; +import { getProjectRelativePath } from './azureYamlUtils'; export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEditProvider { public async provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { const maybeFolder = dataTransfer.get('text/uri-list')?.value; - const folder = vscode.Uri.parse(maybeFolder); + const maybeFolderUri = vscode.Uri.parse(maybeFolder); - const basename = path.basename(folder.fsPath); - const newRelativePath = path.posix.normalize(path.relative(path.dirname(document.uri.fsPath), folder.fsPath)).replace(/\\/g, '/').replace(/^\.?\/?/, './'); - if (await AzExtFsExtra.pathExists(folder) && await AzExtFsExtra.isDirectory(folder)) { + if (await AzExtFsExtra.pathExists(maybeFolderUri) && await AzExtFsExtra.isDirectory(maybeFolderUri)) { + const basename = path.basename(maybeFolderUri.fsPath); + const newRelativePath = getProjectRelativePath(document.uri, maybeFolderUri); + const snippet = new vscode.SnippetString('\t') .appendPlaceholder(basename).appendText(':\n') .appendText(`\t\tproject: ${newRelativePath}\n`) diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index 23cd23424e7..ed905fc46ea 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -2,9 +2,8 @@ // Licensed under the MIT License. import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; -import * as path from 'path'; import * as vscode from 'vscode'; -import { getAzureYamlProjectInformation } from './getAzureYamlProjectInformation'; +import { getAzureYamlProjectInformation, getProjectRelativePath } from './azureYamlUtils'; export class AzureYamlProjectRenameProvider extends vscode.Disposable { public constructor() { @@ -35,7 +34,7 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { return new vscode.WorkspaceEdit(); } - const newRelativePath = path.posix.normalize(path.relative(path.dirname(azureYamlUri.fsPath), newUri.fsPath)).replace(/\\/g, '/').replace(/^\.?\/?/, './'); + const newRelativePath = getProjectRelativePath(azureYamlUri, newUri); const projectUriEdit = new vscode.WorkspaceEdit(); projectUriEdit.replace(azureYamlUri, projectToUpdate.projectValueNodeRange, newRelativePath); return projectUriEdit; diff --git a/ext/vscode/src/language/getAzureYamlProjectInformation.ts b/ext/vscode/src/language/azureYamlUtils.ts similarity index 74% rename from ext/vscode/src/language/getAzureYamlProjectInformation.ts rename to ext/vscode/src/language/azureYamlUtils.ts index 1de6d20c66d..df00bb26c02 100644 --- a/ext/vscode/src/language/getAzureYamlProjectInformation.ts +++ b/ext/vscode/src/language/azureYamlUtils.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; import * as vscode from 'vscode'; import * as yaml from 'yaml'; import { AzureYamlSelector } from './languageFeatures'; -import { getContainingFolderUri } from './getContainingFolderUri'; interface AzureYamlProjectInformation { azureYamlUri: vscode.Uri; @@ -53,4 +53,17 @@ export async function getAzureYamlProjectInformation(document: vscode.TextDocume } return results; +} + +export function getContainingFolderUri(targetUri: vscode.Uri): vscode.Uri { + return vscode.Uri.joinPath(targetUri, '..'); +} + +export function getProjectRelativePath(azureYamlUri: vscode.Uri, projectUri: vscode.Uri): string { + const relativePath = path.relative(path.dirname(azureYamlUri.fsPath), projectUri.fsPath); + const normalizedPosixRelativePath = path.posix.normalize(relativePath) + .replace(/\\/g, '/') // Replace backslashes with forward slashes + .replace(/^\.?\/?/, './'); // Make sure it starts with `./` + + return normalizedPosixRelativePath; } \ No newline at end of file diff --git a/ext/vscode/src/language/getContainingFolderUri.ts b/ext/vscode/src/language/getContainingFolderUri.ts deleted file mode 100644 index db14fcd8b85..00000000000 --- a/ext/vscode/src/language/getContainingFolderUri.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; - -export function getContainingFolderUri(targetUri: vscode.Uri): vscode.Uri { - return vscode.Uri.joinPath(targetUri, '..'); -} From fdad2183bf3aa38ec9576343889c091b19855fe5 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:23:04 -0400 Subject: [PATCH 19/28] Fix minor issue --- ext/vscode/src/language/AzureYamlProjectRenameProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index ed905fc46ea..f4e96631021 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -31,7 +31,7 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { const projectToUpdate = projectInformation.find(pi => pi.projectUri.toString() === oldUri.toString()); if (!projectToUpdate) { - return new vscode.WorkspaceEdit(); + return undefined; } const newRelativePath = getProjectRelativePath(azureYamlUri, newUri); From 7bb9da10ee8aa1d3df5c77be7cdec2a36f40d3c8 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:39:43 -0400 Subject: [PATCH 20/28] Cleanup whitespace situation --- ext/vscode/.vscode/settings.json | 3 +++ ext/vscode/src/language/AzureYamlCompletionItemProvider.ts | 4 ++-- ext/vscode/src/language/AzureYamlDiagnosticProvider.ts | 5 ++--- ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts | 2 +- ext/vscode/src/language/AzureYamlProjectRenameProvider.ts | 2 +- ext/vscode/src/language/azureYamlUtils.ts | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ext/vscode/.vscode/settings.json b/ext/vscode/.vscode/settings.json index 83c4bb53158..e3f66e3d97c 100644 --- a/ext/vscode/.vscode/settings.json +++ b/ext/vscode/.vscode/settings.json @@ -12,4 +12,7 @@ "cSpell.import": ["./.vscode/cspell.yaml"], // Block committing directly to main branch "git.branchProtection": ["main"], + // Keep files nice + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, } diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts index 42b9966c981..933d5972c80 100644 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts @@ -14,7 +14,7 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro const pathPrefix = this.getPathPrefix(document, position); const matchingPaths = await this.getMatchingWorkspacePaths(document, pathPrefix, token); - + return matchingPaths.map((path) => { const completionItem = new vscode.CompletionItem(path); completionItem.insertText = path; @@ -38,7 +38,7 @@ export class AzureYamlCompletionItemProvider implements vscode.CompletionItemPro const currentFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), pathPrefix); const results: string[] = []; - + for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { if (type !== vscode.FileType.Directory) { continue; diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 07ae7be7ebf..dd3c147fdff 100644 --- a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts +++ b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts @@ -19,7 +19,7 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { const diagnosticCollection = vscode.languages.createDiagnosticCollection('azure.yaml'); disposables.push(diagnosticCollection); - + disposables.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => this.updateDiagnosticsFor(e.document))); disposables.push(vscode.workspace.onDidRenameFiles(() => this.updateDiagnosticsForOpenTabs())); disposables.push(vscode.workspace.onDidCreateFiles(() => this.updateDiagnosticsForOpenTabs())); @@ -70,12 +70,11 @@ export class AzureYamlDiagnosticProvider extends vscode.Disposable { this.diagnosticCollection.set(document.uri, await this.provideDiagnostics(document)); }; - if (delay) { + if (delay) { documentDebounce(DiagnosticDelay, { uri: document.uri, callId: 'updateDiagnosticsFor' }, method, this); } else { await method(); } - } private async updateDiagnosticsForOpenTabs(): Promise { diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts index 3f2786cce30..22988d9d33f 100644 --- a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -30,4 +30,4 @@ export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEdi return undefined; } -} \ No newline at end of file +} diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index f4e96631021..13ca4b992a0 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -48,4 +48,4 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { evt.waitUntil(this.provideWorkspaceEdits(oldUri, newUri)); } -} \ No newline at end of file +} diff --git a/ext/vscode/src/language/azureYamlUtils.ts b/ext/vscode/src/language/azureYamlUtils.ts index df00bb26c02..4b02b653f85 100644 --- a/ext/vscode/src/language/azureYamlUtils.ts +++ b/ext/vscode/src/language/azureYamlUtils.ts @@ -64,6 +64,6 @@ export function getProjectRelativePath(azureYamlUri: vscode.Uri, projectUri: vsc const normalizedPosixRelativePath = path.posix.normalize(relativePath) .replace(/\\/g, '/') // Replace backslashes with forward slashes .replace(/^\.?\/?/, './'); // Make sure it starts with `./` - + return normalizedPosixRelativePath; -} \ No newline at end of file +} From 0e83590b2c29f67a10fe6ab9db510084cb5993a9 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:12:28 -0400 Subject: [PATCH 21/28] Move comment --- ext/vscode/src/language/AzureYamlProjectRenameProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index 13ca4b992a0..dc99775fffa 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -16,6 +16,8 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { } public async provideWorkspaceEdits(oldUri: vscode.Uri, newUri: vscode.Uri): 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; } @@ -41,8 +43,6 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { } private handleWillRenameFile(evt: vscode.FileWillRenameEvent): void { - // 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 const oldUri = evt.files[0].oldUri; const newUri = evt.files[0].newUri; From 0e5f98bb7d492246e444c6d0ae6cdac8373ff2a3 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 20 Jun 2023 17:02:43 -0400 Subject: [PATCH 22/28] Remove completion provider --- .../AzureYamlCompletionItemProvider.ts | 55 ------------------- ext/vscode/src/language/languageFeatures.ts | 5 -- 2 files changed, 60 deletions(-) delete mode 100644 ext/vscode/src/language/AzureYamlCompletionItemProvider.ts diff --git a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts b/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts deleted file mode 100644 index 933d5972c80..00000000000 --- a/ext/vscode/src/language/AzureYamlCompletionItemProvider.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { getContainingFolderUri } from './azureYamlUtils'; - -export class AzureYamlCompletionItemProvider implements vscode.CompletionItemProvider { - public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { - // if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter || - // context.triggerCharacter !== '/') { - // // Shouldn't have been triggered, return an empty set - // return []; - // } - - const pathPrefix = this.getPathPrefix(document, position); - const matchingPaths = await this.getMatchingWorkspacePaths(document, pathPrefix, token); - - return matchingPaths.map((path) => { - const completionItem = new vscode.CompletionItem(path); - completionItem.insertText = path; - completionItem.kind = vscode.CompletionItemKind.Folder; - return completionItem; - }); - } - - private getPathPrefix(document: vscode.TextDocument, position: vscode.Position): string | undefined { - const line = document.lineAt(position.line); - const lineRegex = /\s+project:\s*(?\S*)/i; - const match = lineRegex.exec(line.text); - - return match?.groups?.['project']; - } - - private async getMatchingWorkspacePaths(document: vscode.TextDocument, pathPrefix: string | undefined, token: vscode.CancellationToken): Promise { - if (!pathPrefix || pathPrefix[0] !== '.') { - return []; - } - - const currentFolder = vscode.Uri.joinPath(getContainingFolderUri(document.uri), pathPrefix); - const results: string[] = []; - - for (const [file, type] of await vscode.workspace.fs.readDirectory(currentFolder)) { - if (type !== vscode.FileType.Directory) { - continue; - } else if (file[0] === '.') { - // Ignore folders that start with '.' - continue; - } - - results.push(file); - } - - return results; - } -} diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 955580872de..a0cf4298798 100644 --- a/ext/vscode/src/language/languageFeatures.ts +++ b/ext/vscode/src/language/languageFeatures.ts @@ -3,7 +3,6 @@ import * as vscode from 'vscode'; import ext from '../ext'; -import { AzureYamlCompletionItemProvider } from './AzureYamlCompletionItemProvider'; import { AzureYamlDiagnosticProvider } from './AzureYamlDiagnosticProvider'; import { AzureYamlProjectRenameProvider } from './AzureYamlProjectRenameProvider'; import { AzureYamlDocumentDropEditProvider } from './AzureYamlDocumentDropEditProvider'; @@ -11,10 +10,6 @@ import { AzureYamlDocumentDropEditProvider } from './AzureYamlDocumentDropEditPr export const AzureYamlSelector: vscode.DocumentSelector = { language: 'yaml', scheme: 'file', pattern: '**/azure.{yml,yaml}' }; export function registerLanguageFeatures(): void { - ext.context.subscriptions.push( - vscode.languages.registerCompletionItemProvider(AzureYamlSelector, new AzureYamlCompletionItemProvider(), '/'), - ); - ext.context.subscriptions.push( new AzureYamlDiagnosticProvider(AzureYamlSelector) ); From f59c528d42eab1002bf7a0a604300d15d6d48ea3 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 21 Jun 2023 08:47:34 -0400 Subject: [PATCH 23/28] Pass cancellation token and max at one result --- ext/vscode/src/language/AzureYamlProjectRenameProvider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts index dc99775fffa..04e0050f224 100644 --- a/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts +++ b/ext/vscode/src/language/AzureYamlProjectRenameProvider.ts @@ -15,14 +15,14 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { }); } - public async provideWorkspaceEdits(oldUri: vscode.Uri, newUri: vscode.Uri): Promise { + 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}'); + const azureYamlUris = await vscode.workspace.findFiles('**/azure.{yml,yaml}', undefined, 1, token); if (azureYamlUris.length === 0) { return undefined; } @@ -46,6 +46,6 @@ export class AzureYamlProjectRenameProvider extends vscode.Disposable { const oldUri = evt.files[0].oldUri; const newUri = evt.files[0].newUri; - evt.waitUntil(this.provideWorkspaceEdits(oldUri, newUri)); + evt.waitUntil(this.provideWorkspaceEdits(oldUri, newUri, evt.token)); } } From 5a4895376415de034e9cfb837ab932d5f86782bc Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 21 Jun 2023 08:50:59 -0400 Subject: [PATCH 24/28] Git is making me sad --- .../AzureYamlDocumentDropEditProvider.ts | 33 +++++++++++++++++++ ext/vscode/src/language/languageFeatures.ts | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts new file mode 100644 index 00000000000..22988d9d33f --- /dev/null +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getProjectRelativePath } from './azureYamlUtils'; + +export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEditProvider { + public async provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + 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 snippet = new vscode.SnippetString('\t') + .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'); + return new vscode.DocumentDropEdit(snippet); + } + + return undefined; + } +} diff --git a/ext/vscode/src/language/languageFeatures.ts b/ext/vscode/src/language/languageFeatures.ts index 247a9b0eee6..a0cf4298798 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()) + ); } From 42fec1e7db84739341027593f421025b2ab46d48 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 21 Jun 2023 08:51:39 -0400 Subject: [PATCH 25/28] Fix cspell dictionary --- ext/vscode/.vscode/cspell-dictionary.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ext/vscode/.vscode/cspell-dictionary.txt b/ext/vscode/.vscode/cspell-dictionary.txt index 036203c717d..70f49abc7cd 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 From 8b8046fd25845c7d9dc1ca5b424e9bb2eb8dc219 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 21 Jun 2023 09:30:11 -0400 Subject: [PATCH 26/28] Slightly improve tab behavior --- ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts index 22988d9d33f..3016fb0887f 100644 --- a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -16,7 +16,9 @@ export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEdi const basename = path.basename(maybeFolderUri.fsPath); const newRelativePath = getProjectRelativePath(document.uri, maybeFolderUri); - const snippet = new vscode.SnippetString('\t') + 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: ') From 5910ca5028a8a42d20d871c9fa9ba63bc2b97866 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Thu, 22 Jun 2023 08:52:09 -0400 Subject: [PATCH 27/28] Extra newline --- ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts index 3016fb0887f..759613dc004 100644 --- a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -11,7 +11,6 @@ export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEdi 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); From 54656744db9145c5ddd20665e8ab494b4ec9e66b Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 11 Jul 2023 09:57:04 -0400 Subject: [PATCH 28/28] Add telemetry --- .../language/AzureYamlDiagnosticProvider.ts | 43 +++++++------ .../AzureYamlDocumentDropEditProvider.ts | 46 ++++++++------ .../AzureYamlProjectRenameProvider.ts | 61 +++++++++++-------- ext/vscode/src/telemetry/telemetryId.ts | 21 +++++-- 4 files changed, 99 insertions(+), 72 deletions(-) diff --git a/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts b/ext/vscode/src/language/AzureYamlDiagnosticProvider.ts index 3d908ca6d84..3bd706372b4 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 index 759613dc004..19b4c1f30c4 100644 --- a/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts +++ b/ext/vscode/src/language/AzureYamlDocumentDropEditProvider.ts @@ -1,34 +1,40 @@ // 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 path from 'path'; import * as vscode from 'vscode'; import { getProjectRelativePath } from './azureYamlUtils'; +import { TelemetryId } from '../telemetry/telemetryId'; export class AzureYamlDocumentDropEditProvider implements vscode.DocumentDropEditProvider { - public async provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - const maybeFolder = dataTransfer.get('text/uri-list')?.value; - const maybeFolderUri = vscode.Uri.parse(maybeFolder); + 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); + 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 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'); - return new vscode.DocumentDropEdit(snippet); - } + 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'); - return undefined; + 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 04e0050f224..87f96c34b81 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/telemetry/telemetryId.ts b/ext/vscode/src/telemetry/telemetryId.ts index 1cf23e47451..03599b3b47d 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', }