More auto-detection improvements (#486)
* Introduce constants * Properly handle text editor events * Use waitForSubscriptions to avoid race condition * Calculate workspaceFolder ourselves on lang change * Only load schema for active text editor * Only request schema on session change if logged in * Failure to load Git extension shouldn't be fatal * Simplify onDidOpenTextDocument handling * Add a bunch of logs
This commit is contained in:
Родитель
282970bd18
Коммит
d4d82fcf06
|
@ -13,8 +13,21 @@ import { schemaContributor, CUSTOM_SCHEMA_REQUEST, CUSTOM_CONTENT_REQUEST } from
|
|||
import { telemetryHelper } from './helpers/telemetryHelper';
|
||||
import { getAzureAccountExtensionApi } from './extensionApis';
|
||||
|
||||
/**
|
||||
* The unique string that identifies the Azure Pipelines languge.
|
||||
*/
|
||||
const LANGUAGE_IDENTIFIER = 'azure-pipelines';
|
||||
|
||||
/**
|
||||
* The document selector to use when deciding whether to activate Azure Pipelines-specific features.
|
||||
*/
|
||||
const DOCUMENT_SELECTOR = [
|
||||
{ language: LANGUAGE_IDENTIFIER, scheme: 'file' },
|
||||
{ language: LANGUAGE_IDENTIFIER, scheme: 'untitled' }
|
||||
]
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const configurePipelineEnabled = vscode.workspace.getConfiguration('azure-pipelines').get<boolean>('configure', true);
|
||||
const configurePipelineEnabled = vscode.workspace.getConfiguration(LANGUAGE_IDENTIFIER).get<boolean>('configure', true);
|
||||
telemetryHelper.initialize('azurePipelines.activate', {
|
||||
isActivationEvent: 'true',
|
||||
configurePipelineEnabled: `${configurePipelineEnabled}`,
|
||||
|
@ -34,7 +47,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
async function activateYmlContributor(context: vscode.ExtensionContext) {
|
||||
const serverOptions: languageclient.ServerOptions = getServerOptions(context);
|
||||
const clientOptions: languageclient.LanguageClientOptions = getClientOptions();
|
||||
const client = new languageclient.LanguageClient('azure-pipelines', 'Azure Pipelines Language', serverOptions, clientOptions);
|
||||
const client = new languageclient.LanguageClient(LANGUAGE_IDENTIFIER, 'Azure Pipelines Language', serverOptions, clientOptions);
|
||||
|
||||
const disposable = client.start();
|
||||
context.subscriptions.push(disposable);
|
||||
|
@ -56,7 +69,7 @@ async function activateYmlContributor(context: vscode.ExtensionContext) {
|
|||
});
|
||||
|
||||
// TODO: Can we get rid of this since it's set in package.json?
|
||||
vscode.languages.setLanguageConfiguration('azure-pipelines', { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ });
|
||||
vscode.languages.setLanguageConfiguration(LANGUAGE_IDENTIFIER, { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ });
|
||||
|
||||
// Let the server know of any schema changes.
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async event => {
|
||||
|
@ -66,27 +79,33 @@ async function activateYmlContributor(context: vscode.ExtensionContext) {
|
|||
}));
|
||||
|
||||
// Load the schema if we were activated because an Azure Pipelines file.
|
||||
if (vscode.window.activeTextEditor?.document.languageId === 'azure-pipelines') {
|
||||
if (vscode.window.activeTextEditor?.document.languageId === LANGUAGE_IDENTIFIER) {
|
||||
await loadSchema(context, client);
|
||||
}
|
||||
|
||||
// And subscribe to future open events, as well.
|
||||
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
await loadSchema(context, client);
|
||||
}));
|
||||
|
||||
// Or if the active editor's language changes.
|
||||
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(async textDocument => {
|
||||
// NOTE: We need to explicitly compute the workspace folder here rather than
|
||||
// relying on the logic in loadSchema, because somehow preview editors
|
||||
// don't count as "active".
|
||||
if (textDocument?.languageId !== 'azure-pipelines') {
|
||||
// Ensure this event is due to a language change.
|
||||
// Since onDidOpenTextDocument is fired *before* activeTextEditor changes,
|
||||
// if the URIs are the same we know that the new text document must be
|
||||
// due to a language change.
|
||||
if (textDocument.uri !== vscode.window.activeTextEditor?.document.uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(textDocument.uri);
|
||||
await loadSchema(context, client, workspaceFolder);
|
||||
await loadSchema(context, client);
|
||||
}));
|
||||
|
||||
// Re-request the schema on Azure login since auto-detection is dependent on login.
|
||||
// Re-request the schema when sessions change since auto-detection is dependent on
|
||||
// being able to query ADO organizations using session credentials.
|
||||
const azureAccountApi = await getAzureAccountExtensionApi();
|
||||
context.subscriptions.push(azureAccountApi.onStatusChanged(async status => {
|
||||
if (status === 'LoggedIn') {
|
||||
context.subscriptions.push(azureAccountApi.onSessionsChanged(async () => {
|
||||
if (azureAccountApi.status === 'LoggedIn') {
|
||||
await loadSchema(context, client);
|
||||
}
|
||||
}));
|
||||
|
@ -105,7 +124,7 @@ async function loadSchema(
|
|||
workspaceFolder?: vscode.WorkspaceFolder): Promise<void> {
|
||||
if (workspaceFolder === undefined) {
|
||||
const textDocument = vscode.window.activeTextEditor?.document;
|
||||
if (textDocument?.languageId !== 'azure-pipelines') {
|
||||
if (textDocument?.languageId !== LANGUAGE_IDENTIFIER) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -133,10 +152,7 @@ function getServerOptions(context: vscode.ExtensionContext): languageclient.Serv
|
|||
function getClientOptions(): languageclient.LanguageClientOptions {
|
||||
return {
|
||||
// Register the server for Azure Pipelines documents
|
||||
documentSelector: [
|
||||
{ language: 'azure-pipelines', scheme: 'file' },
|
||||
{ language: 'azure-pipelines', scheme: 'untitled' }
|
||||
],
|
||||
documentSelector: DOCUMENT_SELECTOR,
|
||||
synchronize: {
|
||||
// TODO: Switch to handling the workspace/configuration request
|
||||
configurationSection: ['yaml', 'http.proxy', 'http.proxyStrictSSL'],
|
||||
|
|
|
@ -33,14 +33,20 @@ export async function locateSchemaFile(
|
|||
// TODO: Support auto-detection for Azure Pipelines files outside of the workspace.
|
||||
if (workspaceFolder !== undefined) {
|
||||
try {
|
||||
logger.log(`Detecting schema for workspace folder ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
schemaUri = await autoDetectSchema(context, workspaceFolder);
|
||||
if (schemaUri) {
|
||||
logger.log(
|
||||
`Detected schema for workspace folder ${workspaceFolder.name}: ${schemaUri.path}`,
|
||||
'SchemaDetection');
|
||||
return schemaUri.path;
|
||||
}
|
||||
} catch (error) {
|
||||
// Well, we tried our best. Fall back to the predetermined schema paths.
|
||||
// TODO: Re-throw error once we're more confident in the schema detection.
|
||||
logger.log(`Error auto-detecting schema: ${error}`, 'SchemaAutoDetectError');
|
||||
logger.log(
|
||||
`Error auto-detecting schema for workspace folder ${workspaceFolder.name}: ${error}`,
|
||||
'SchemaDetection');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +66,10 @@ export async function locateSchemaFile(
|
|||
schemaUri = vscode.Uri.file(path.join(context.extensionPath, 'service-schema.json'));
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`Using hardcoded schema for workspace folder ${workspaceFolder.name}: ${schemaUri.path}`,
|
||||
'SchemaDetection');
|
||||
|
||||
// TODO: We should update getSchemaAssociations so we don't need to constantly
|
||||
// notify the server of a "new" schema when in reality we're simply updating
|
||||
// associations -- which is exactly what getSchemaAssociations is there for!
|
||||
|
@ -83,7 +93,16 @@ async function autoDetectSchema(
|
|||
context: vscode.ExtensionContext,
|
||||
workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.Uri | undefined> {
|
||||
const azureAccountApi = await getAzureAccountExtensionApi();
|
||||
if (!(await azureAccountApi.waitForLogin())) {
|
||||
|
||||
// We could care less about the subscriptions; all we need are the sessions.
|
||||
// However, there's no waitForSessions API, and waitForLogin returns before
|
||||
// the underlying account information is guaranteed to finish loading.
|
||||
// The next-best option is then waitForSubscriptions which, by definition,
|
||||
// can't return until the sessions are also available.
|
||||
// This only returns false if there is no login.
|
||||
if (!(await azureAccountApi.waitForSubscriptions())) {
|
||||
logger.log(`Waiting for login`, 'SchemaDetection');
|
||||
|
||||
// Don't await this message so that we can return the fallback schema instead of blocking.
|
||||
// We'll detect the login in extension.ts and then re-request the schema.
|
||||
vscode.window.showInformationMessage(Messages.signInForEnhancedIntelliSense, Messages.signInLabel)
|
||||
|
@ -103,23 +122,32 @@ async function autoDetectSchema(
|
|||
|
||||
// Get the remote URL if we're in a Git repo.
|
||||
let remoteUrl: string | undefined;
|
||||
const gitExtension = await getGitExtensionApi();
|
||||
|
||||
// Use openRepository because it's possible the Git extension hasn't
|
||||
// finished opening all the repositories yet, and thus getRepository
|
||||
// may return null if an Azure Pipelines file is open on startup.
|
||||
const repo = await gitExtension.openRepository(workspaceFolder.uri);
|
||||
if (repo !== null) {
|
||||
await repo.status();
|
||||
if (repo.state.HEAD?.upstream !== undefined) {
|
||||
const remoteName = repo.state.HEAD.upstream.remote;
|
||||
remoteUrl = repo.state.remotes.find(remote => remote.name === remoteName)?.fetchUrl;
|
||||
try {
|
||||
const gitExtension = await getGitExtensionApi();
|
||||
|
||||
// Use openRepository because it's possible the Git extension hasn't
|
||||
// finished opening all the repositories yet, and thus getRepository
|
||||
// may return null if an Azure Pipelines file is open on startup.
|
||||
const repo = await gitExtension.openRepository(workspaceFolder.uri);
|
||||
if (repo !== null) {
|
||||
await repo.status();
|
||||
if (repo.state.HEAD?.upstream !== undefined) {
|
||||
const remoteName = repo.state.HEAD.upstream.remote;
|
||||
remoteUrl = repo.state.remotes.find(remote => remote.name === remoteName)?.fetchUrl;
|
||||
logger.log(`Found remote URL for ${workspaceFolder.name}: ${remoteUrl}`, 'SchemaDetection');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log and that's it - perhaps they're not in a Git repo, and so don't have the Git extension enabled.
|
||||
logger.log(`${workspaceFolder.name} has no remote URLs: ${error}`, 'SchemaDetection');
|
||||
}
|
||||
|
||||
let organizationName: string;
|
||||
let session: AzureSession | undefined;
|
||||
if (remoteUrl !== undefined && AzureDevOpsHelper.isAzureReposUrl(remoteUrl)) {
|
||||
logger.log(`${workspaceFolder.name} is an Azure repo`, 'SchemaDetection');
|
||||
|
||||
// If we're in an Azure repo, we can silently determine the organization name and session.
|
||||
organizationName = AzureDevOpsHelper.getRepositoryDetailsFromRemoteUrl(remoteUrl).organizationName;
|
||||
for (const azureSession of azureAccountApi.sessions) {
|
||||
|
@ -131,6 +159,8 @@ async function autoDetectSchema(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
logger.log(`${workspaceFolder.name} has no remote URL or is not an Azure repo`, 'SchemaDetection');
|
||||
|
||||
const azurePipelinesDetails = context.workspaceState.get<{
|
||||
[folder: string]: { organization: string; tenant: string; }
|
||||
}>('azurePipelinesDetails');
|
||||
|
@ -139,7 +169,13 @@ async function autoDetectSchema(
|
|||
const details = azurePipelinesDetails[workspaceFolder.name];
|
||||
organizationName = details.organization;
|
||||
session = azureAccountApi.sessions.find(session => session.tenantId === details.tenant);
|
||||
|
||||
logger.log(
|
||||
`Using cached information for ${workspaceFolder.name}: ${organizationName}, ${session.tenantId}`,
|
||||
'SchemaDetection');
|
||||
} else {
|
||||
logger.log(`Prompting for organization for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
|
||||
// Otherwise, we need to manually prompt.
|
||||
// We do this by asking them to select an organization via an information message,
|
||||
// then displaying the quick pick of all the organizations they have access to.
|
||||
|
@ -157,7 +193,6 @@ async function autoDetectSchema(
|
|||
>(async resolve => {
|
||||
const organizationAndSessions: QuickPickItemWithData<AzureSession>[] = [];
|
||||
|
||||
// FIXME: azureAccountApi.sessions changes under us. Why?
|
||||
for (const azureSession of azureAccountApi.sessions) {
|
||||
const organizationsClient = new OrganizationsClient(azureSession.credentials2);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
|
@ -200,6 +235,7 @@ async function autoDetectSchema(
|
|||
|
||||
// Not logged into an account that has access.
|
||||
if (session === undefined) {
|
||||
logger.log(`No organization found for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
vscode.window.showErrorMessage(format(Messages.unableToAccessOrganization, organizationName));
|
||||
return undefined;
|
||||
}
|
||||
|
@ -216,9 +252,12 @@ async function autoDetectSchema(
|
|||
// hit the network to request an updated schema for an organization once per session.
|
||||
const schemaUri = Utils.joinPath(context.globalStorageUri, `${organizationName}-schema.json`);
|
||||
if (seenOrganizations.has(organizationName)) {
|
||||
logger.log(`Returning cached schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
return schemaUri;
|
||||
}
|
||||
|
||||
logger.log(`Retrieving schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
|
||||
const token = await session.credentials2.getToken();
|
||||
const authHandler = azdev.getBearerHandler(token.accessToken);
|
||||
const azureDevOpsClient = new azdev.WebApi(`https://dev.azure.com/${organizationName}`, authHandler);
|
||||
|
|
Загрузка…
Ссылка в новой задаче