{
  "$schema" : "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
  "version" : "2.1.0",
  "runs" : [ {
    "tool" : {
      "driver" : {
        "name" : "CodeQL",
        "organization" : "GitHub",
        "semanticVersion" : "2.8.1",
        "rules" : [ {
          "id" : "remote-query",
          "name" : "remote-query",
          "defaultConfiguration" : {
            "enabled" : true,
            "level" : "warning"
          },
          "properties" : {
            "id" : "remote-query",
            "kind" : "problem"
          }
        } ]
      }
    },
    "artifacts" : [ {
      "location" : {
        "uri" : "extensions/ql-vscode/src/astViewer.ts",
        "uriBaseId" : "%SRCROOT%",
        "index" : 0
      },
      "contents" : {
        "text" : "import {\n  window,\n  TreeDataProvider,\n  EventEmitter,\n  Event,\n  ProviderResult,\n  TreeItemCollapsibleState,\n  TreeItem,\n  TreeView,\n  TextEditorSelectionChangeEvent,\n  TextEditorSelectionChangeKind,\n  Location,\n  Range\n} from 'vscode';\nimport * as path from 'path';\n\nimport { DatabaseItem } from './databases';\nimport { UrlValue, BqrsId } from './pure/bqrs-cli-types';\nimport { showLocation } from './interface-utils';\nimport { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';\nimport { commandRunner } from './commandRunner';\nimport { DisposableObject } from './pure/disposable-object';\nimport { showAndLogErrorMessage } from './helpers';\n\nexport interface AstItem {\n  id: BqrsId;\n  label?: string;\n  location?: UrlValue;\n  fileLocation?: Location;\n  children: ChildAstItem[];\n  order: number;\n}\n\nexport interface ChildAstItem extends AstItem {\n  parent: ChildAstItem | AstItem;\n}\n\nclass AstViewerDataProvider extends DisposableObject implements TreeDataProvider<AstItem> {\n\n  public roots: AstItem[] = [];\n  public db: DatabaseItem | undefined;\n\n  private _onDidChangeTreeData =\n    this.push(new EventEmitter<AstItem | undefined>());\n  readonly onDidChangeTreeData: Event<AstItem | undefined> =\n    this._onDidChangeTreeData.event;\n\n  constructor() {\n    super();\n    this.push(\n      commandRunner('codeQLAstViewer.gotoCode',\n        async (item: AstItem) => {\n          await showLocation(item.fileLocation);\n        })\n    );\n  }\n\n  refresh(): void {\n    this._onDidChangeTreeData.fire(undefined);\n  }\n  getChildren(item?: AstItem): ProviderResult<AstItem[]> {\n    const children = item ? item.children : this.roots;\n    return children.sort((c1, c2) => (c1.order - c2.order));\n  }\n\n  getParent(item: ChildAstItem): ProviderResult<AstItem> {\n    return item.parent;\n  }\n\n  getTreeItem(item: AstItem): TreeItem {\n    const line = this.extractLineInfo(item?.location);\n\n    const state = item.children.length\n      ? TreeItemCollapsibleState.Collapsed\n      : TreeItemCollapsibleState.None;\n    const treeItem = new TreeItem(item.label || '', state);\n    treeItem.description = line ? `Line ${line}` : '';\n    treeItem.id = String(item.id);\n    treeItem.tooltip = `${treeItem.description} ${treeItem.label}`;\n    treeItem.command = {\n      command: 'codeQLAstViewer.gotoCode',\n      title: 'Go To Code',\n      tooltip: `Go To ${item.location}`,\n      arguments: [item]\n    };\n    return treeItem;\n  }\n\n  private extractLineInfo(loc?: UrlValue) {\n    if (!loc) {\n      return '';\n    } else if (isStringLoc(loc)) {\n      return loc;\n    } else if (isWholeFileLoc(loc)) {\n      return loc.uri;\n    } else if (isLineColumnLoc(loc)) {\n      return loc.startLine;\n    } else {\n      return '';\n    }\n  }\n}\n\nexport class AstViewer extends DisposableObject {\n  private treeView: TreeView<AstItem>;\n  private treeDataProvider: AstViewerDataProvider;\n  private currentFile: string | undefined;\n\n  constructor() {\n    super();\n\n    this.treeDataProvider = new AstViewerDataProvider();\n    this.treeView = window.createTreeView('codeQLAstViewer', {\n      treeDataProvider: this.treeDataProvider,\n      showCollapseAll: true\n    });\n\n    this.push(this.treeView);\n    this.push(this.treeDataProvider);\n    this.push(\n      commandRunner('codeQLAstViewer.clear', async () => {\n        this.clear();\n      })\n    );\n    this.push(window.onDidChangeTextEditorSelection(this.updateTreeSelection, this));\n  }\n\n  updateRoots(roots: AstItem[], db: DatabaseItem, fileName: string) {\n    this.treeDataProvider.roots = roots;\n    this.treeDataProvider.db = db;\n    this.treeDataProvider.refresh();\n    this.treeView.message = `AST for ${path.basename(fileName)}`;\n    this.currentFile = fileName;\n    // Handle error on reveal. This could happen if\n    // the tree view is disposed during the reveal.\n    this.treeView.reveal(roots[0], { focus: false })?.then(\n      () => { /**/ },\n      err => showAndLogErrorMessage(err)\n    );\n  }\n\n  private updateTreeSelection(e: TextEditorSelectionChangeEvent) {\n    function isInside(selectedRange: Range, astRange?: Range): boolean {\n      return !!astRange?.contains(selectedRange);\n    }\n\n    // Recursively iterate all children until we find the node with the smallest\n    // range that contains the selection.\n    // Some nodes do not have a location, but their children might, so must\n    // recurse though location-less AST nodes to see if children are correct.\n    function findBest(selectedRange: Range, items?: AstItem[]): AstItem | undefined {\n      if (!items || !items.length) {\n        return;\n      }\n      for (const item of items) {\n        let candidate: AstItem | undefined = undefined;\n        if (isInside(selectedRange, item.fileLocation?.range)) {\n          candidate = item;\n        }\n        // always iterate through children since the location of an AST node in code QL does not\n        // always cover the complete text of the node.\n        candidate = findBest(selectedRange, item.children) || candidate;\n        if (candidate) {\n          return candidate;\n        }\n      }\n      return;\n    }\n\n    // Avoid recursive tree-source code updates.\n    if (e.kind === TextEditorSelectionChangeKind.Command) {\n      return;\n    }\n\n    if (\n      this.treeView.visible &&\n      e.textEditor.document.uri.fsPath === this.currentFile &&\n      e.selections.length === 1\n    ) {\n      const selection = e.selections[0];\n      const range = selection.anchor.isBefore(selection.active)\n        ? new Range(selection.anchor, selection.active)\n        : new Range(selection.active, selection.anchor);\n\n      const targetItem = findBest(range, this.treeDataProvider.roots);\n      if (targetItem) {\n        // Handle error on reveal. This could happen if\n        // the tree view is disposed during the reveal.\n        this.treeView.reveal(targetItem)?.then(\n          () => { /**/ },\n          err => showAndLogErrorMessage(err)\n        );\n      }\n    }\n  }\n\n  private clear() {\n    this.treeDataProvider.roots = [];\n    this.treeDataProvider.db = undefined;\n    this.treeDataProvider.refresh();\n    this.treeView.message = undefined;\n    this.currentFile = undefined;\n  }\n}\n"
      }
    }, {
      "location" : {
        "uri" : "extensions/ql-vscode/src/databases.ts",
        "uriBaseId" : "%SRCROOT%",
        "index" : 1
      },
      "contents" : {
        "text" : "import * as fs from 'fs-extra';\nimport * as glob from 'glob-promise';\nimport * as path from 'path';\nimport * as vscode from 'vscode';\nimport * as cli from './cli';\nimport { ExtensionContext } from 'vscode';\nimport {\n  showAndLogErrorMessage,\n  showAndLogWarningMessage,\n  showAndLogInformationMessage,\n  isLikelyDatabaseRoot\n} from './helpers';\nimport {\n  ProgressCallback,\n  withProgress\n} from './commandRunner';\nimport { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';\nimport { DisposableObject } from './pure/disposable-object';\nimport { Logger, logger } from './logging';\nimport { registerDatabases, Dataset, deregisterDatabases } from './pure/messages';\nimport { QueryServerClient } from './queryserver-client';\n\n/**\n * databases.ts\n * ------------\n * Managing state of what the current database is, and what other\n * databases have been recently selected.\n *\n * The source of truth of the current state resides inside the\n * `DatabaseManager` class below.\n */\n\n/**\n * The name of the key in the workspaceState dictionary in which we\n * persist the current database across sessions.\n */\nconst CURRENT_DB = 'currentDatabase';\n\n/**\n * The name of the key in the workspaceState dictionary in which we\n * persist the list of databases across sessions.\n */\nconst DB_LIST = 'databaseList';\n\nexport interface DatabaseOptions {\n  displayName?: string;\n  ignoreSourceArchive?: boolean;\n  dateAdded?: number | undefined;\n  language?: string;\n}\n\nexport interface FullDatabaseOptions extends DatabaseOptions {\n  ignoreSourceArchive: boolean;\n  dateAdded: number | undefined;\n  language: string | undefined;\n}\n\ninterface PersistedDatabaseItem {\n  uri: string;\n  options?: DatabaseOptions;\n}\n\n/**\n * The layout of the database.\n */\nexport enum DatabaseKind {\n  /** A CodeQL database */\n  Database,\n  /** A raw QL dataset */\n  RawDataset\n}\n\nexport interface DatabaseContents {\n  /** The layout of the database */\n  kind: DatabaseKind;\n  /**\n   * The name of the database.\n   */\n  name: string;\n  /** The URI of the QL dataset within the database. */\n  datasetUri: vscode.Uri;\n  /** The URI of the source archive within the database, if one exists. */\n  sourceArchiveUri?: vscode.Uri;\n  /** The URI of the CodeQL database scheme within the database, if exactly one exists. */\n  dbSchemeUri?: vscode.Uri;\n}\n\n/**\n * An error thrown when we cannot find a valid database in a putative\n * database directory.\n */\nclass InvalidDatabaseError extends Error {\n}\n\n\nasync function findDataset(parentDirectory: string): Promise<vscode.Uri> {\n  /*\n   * Look directly in the root\n   */\n  let dbRelativePaths = await glob('db-*/', {\n    cwd: parentDirectory\n  });\n\n  if (dbRelativePaths.length === 0) {\n    /*\n     * Check If they are in the old location\n     */\n    dbRelativePaths = await glob('working/db-*/', {\n      cwd: parentDirectory\n    });\n  }\n  if (dbRelativePaths.length === 0) {\n    throw new InvalidDatabaseError(`'${parentDirectory}' does not contain a dataset directory.`);\n  }\n\n  const dbAbsolutePath = path.join(parentDirectory, dbRelativePaths[0]);\n  if (dbRelativePaths.length > 1) {\n    void showAndLogWarningMessage(`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`);\n  }\n\n  return vscode.Uri.file(dbAbsolutePath);\n}\n\n// exported for testing\nexport async function findSourceArchive(\n  databasePath: string, silent = false\n): Promise<vscode.Uri | undefined> {\n  const relativePaths = ['src', 'output/src_archive'];\n\n  for (const relativePath of relativePaths) {\n    const basePath = path.join(databasePath, relativePath);\n    const zipPath = basePath + '.zip';\n\n    // Prefer using a zip archive over a directory.\n    if (await fs.pathExists(zipPath)) {\n      return encodeArchiveBasePath(zipPath);\n    } else if (await fs.pathExists(basePath)) {\n      return vscode.Uri.file(basePath);\n    }\n  }\n  if (!silent) {\n    void showAndLogInformationMessage(\n      `Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`\n    );\n  }\n  return undefined;\n}\n\nasync function resolveDatabase(\n  databasePath: string\n): Promise<DatabaseContents> {\n\n  const name = path.basename(databasePath);\n\n  // Look for dataset and source archive.\n  const datasetUri = await findDataset(databasePath);\n  const sourceArchiveUri = await findSourceArchive(databasePath);\n\n  return {\n    kind: DatabaseKind.Database,\n    name,\n    datasetUri,\n    sourceArchiveUri\n  };\n}\n\n/** Gets the relative paths of all `.dbscheme` files in the given directory. */\nasync function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {\n  return await glob('*.dbscheme', { cwd: dbDirectory });\n}\n\nasync function resolveDatabaseContents(uri: vscode.Uri): Promise<DatabaseContents> {\n  if (uri.scheme !== 'file') {\n    throw new Error(`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`);\n  }\n  const databasePath = uri.fsPath;\n  if (!await fs.pathExists(databasePath)) {\n    throw new InvalidDatabaseError(`Database '${databasePath}' does not exist.`);\n  }\n\n  const contents = await resolveDatabase(databasePath);\n\n  if (contents === undefined) {\n    throw new InvalidDatabaseError(`'${databasePath}' is not a valid database.`);\n  }\n\n  // Look for a single dbscheme file within the database.\n  // This should be found in the dataset directory, regardless of the form of database.\n  const dbPath = contents.datasetUri.fsPath;\n  const dbSchemeFiles = await getDbSchemeFiles(dbPath);\n  if (dbSchemeFiles.length === 0) {\n    throw new InvalidDatabaseError(`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`);\n  }\n  else if (dbSchemeFiles.length > 1) {\n    throw new InvalidDatabaseError(`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`);\n  } else {\n    contents.dbSchemeUri = vscode.Uri.file(path.resolve(dbPath, dbSchemeFiles[0]));\n  }\n  return contents;\n}\n\n/** An item in the list of available databases */\nexport interface DatabaseItem {\n  /** The URI of the database */\n  readonly databaseUri: vscode.Uri;\n  /** The name of the database to be displayed in the UI */\n  name: string;\n\n  /** The primary language of the database or empty string if unknown */\n  readonly language: string;\n  /** The URI of the database's source archive, or `undefined` if no source archive is to be used. */\n  readonly sourceArchive: vscode.Uri | undefined;\n  /**\n   * The contents of the database.\n   * Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.\n   */\n  readonly contents: DatabaseContents | undefined;\n\n  /**\n   * The date this database was added as a unix timestamp. Or undefined if we don't know.\n   */\n  readonly dateAdded: number | undefined;\n\n  /** If the database is invalid, describes why. */\n  readonly error: Error | undefined;\n  /**\n   * Resolves the contents of the database.\n   *\n   * @remarks\n   * The contents include the database directory, source archive, and metadata about the database.\n   * If the database is invalid, `this.error` is updated with the error object that describes why\n   * the database is invalid. This error is also thrown.\n   */\n  refresh(): Promise<void>;\n  /**\n   * Resolves a filename to its URI in the source archive.\n   *\n   * @param file Filename within the source archive. May be `undefined` to return a dummy file path.\n   */\n  resolveSourceFile(file: string | undefined): vscode.Uri;\n\n  /**\n   * Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.\n   */\n  hasMetadataFile(): Promise<boolean>;\n\n  /**\n   * Returns `sourceLocationPrefix` of exported database.\n   */\n  getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;\n\n  /**\n   * Returns dataset folder of exported database.\n   */\n  getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;\n\n  /**\n   * Returns the root uri of the virtual filesystem for this database's source archive,\n   * as displayed in the filesystem explorer.\n   */\n  getSourceArchiveExplorerUri(): vscode.Uri;\n\n  /**\n   * Holds if `uri` belongs to this database's source archive.\n   */\n  belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean;\n\n  /**\n   * Whether the database may be affected by test execution for the given path.\n   */\n  isAffectedByTest(testPath: string): Promise<boolean>;\n\n  /**\n   * Gets the state of this database, to be persisted in the workspace state.\n   */\n  getPersistedState(): PersistedDatabaseItem;\n\n  /**\n   * Verifies that this database item has a zipped source folder. Returns an error message if it does not.\n   */\n  verifyZippedSources(): string | undefined;\n}\n\nexport enum DatabaseEventKind {\n  Add = 'Add',\n  Remove = 'Remove',\n\n  // Fired when databases are refreshed from persisted state\n  Refresh = 'Refresh',\n\n  // Fired when the current database changes\n  Change = 'Change',\n\n  Rename = 'Rename'\n}\n\nexport interface DatabaseChangedEvent {\n  kind: DatabaseEventKind;\n  item: DatabaseItem | undefined;\n}\n\n// Exported for testing\nexport class DatabaseItemImpl implements DatabaseItem {\n  private _error: Error | undefined = undefined;\n  private _contents: DatabaseContents | undefined;\n  /** A cache of database info */\n  private _dbinfo: cli.DbInfo | undefined;\n\n  public constructor(\n    public readonly databaseUri: vscode.Uri,\n    contents: DatabaseContents | undefined,\n    private options: FullDatabaseOptions,\n    private readonly onChanged: (event: DatabaseChangedEvent) => void\n  ) {\n    this._contents = contents;\n  }\n\n  public get name(): string {\n    if (this.options.displayName) {\n      return this.options.displayName;\n    }\n    else if (this._contents) {\n      return this._contents.name;\n    }\n    else {\n      return path.basename(this.databaseUri.fsPath);\n    }\n  }\n\n  public set name(newName: string) {\n    this.options.displayName = newName;\n  }\n\n  public get sourceArchive(): vscode.Uri | undefined {\n    if (this.options.ignoreSourceArchive || (this._contents === undefined)) {\n      return undefined;\n    } else {\n      return this._contents.sourceArchiveUri;\n    }\n  }\n\n  public get contents(): DatabaseContents | undefined {\n    return this._contents;\n  }\n\n  public get dateAdded(): number | undefined {\n    return this.options.dateAdded;\n  }\n\n  public get error(): Error | undefined {\n    return this._error;\n  }\n\n  public async refresh(): Promise<void> {\n    try {\n      try {\n        this._contents = await resolveDatabaseContents(this.databaseUri);\n        this._error = undefined;\n      }\n      catch (e) {\n        this._contents = undefined;\n        this._error = e;\n        throw e;\n      }\n    }\n    finally {\n      this.onChanged({\n        kind: DatabaseEventKind.Refresh,\n        item: this\n      });\n    }\n  }\n\n  public resolveSourceFile(uriStr: string | undefined): vscode.Uri {\n    const sourceArchive = this.sourceArchive;\n    const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined;\n    if (uri && uri.scheme !== 'file') {\n      throw new Error(`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`);\n    }\n    if (!sourceArchive) {\n      if (uri) {\n        return uri;\n      } else {\n        return this.databaseUri;\n      }\n    }\n\n    if (uri) {\n      const relativeFilePath = decodeURI(uri.path).replace(':', '_').replace(/^\\/*/, '');\n      if (sourceArchive.scheme === zipArchiveScheme) {\n        const zipRef = decodeSourceArchiveUri(sourceArchive);\n        const pathWithinSourceArchive = zipRef.pathWithinSourceArchive === '/'\n          ? relativeFilePath\n          : zipRef.pathWithinSourceArchive + '/' + relativeFilePath;\n        return encodeSourceArchiveUri({\n          pathWithinSourceArchive,\n          sourceArchiveZipPath: zipRef.sourceArchiveZipPath,\n        });\n\n      } else {\n        let newPath = sourceArchive.path;\n        if (!newPath.endsWith('/')) {\n          // Ensure a trailing slash.\n          newPath += '/';\n        }\n        newPath += relativeFilePath;\n\n        return sourceArchive.with({ path: newPath });\n      }\n\n    } else {\n      return sourceArchive;\n    }\n  }\n\n  /**\n   * Gets the state of this database, to be persisted in the workspace state.\n   */\n  public getPersistedState(): PersistedDatabaseItem {\n    return {\n      uri: this.databaseUri.toString(true),\n      options: this.options\n    };\n  }\n\n  /**\n   * Holds if the database item refers to an exported snapshot\n   */\n  public async hasMetadataFile(): Promise<boolean> {\n    return await isLikelyDatabaseRoot(this.databaseUri.fsPath);\n  }\n\n  /**\n   * Returns information about a database.\n   */\n  private async getDbInfo(server: cli.CodeQLCliServer): Promise<cli.DbInfo> {\n    if (this._dbinfo === undefined) {\n      this._dbinfo = await server.resolveDatabase(this.databaseUri.fsPath);\n    }\n    return this._dbinfo;\n  }\n\n  /**\n   * Returns `sourceLocationPrefix` of database. Requires that the database\n   * has a `.dbinfo` file, which is the source of the prefix.\n   */\n  public async getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string> {\n    const dbInfo = await this.getDbInfo(server);\n    return dbInfo.sourceLocationPrefix;\n  }\n\n  /**\n   * Returns path to dataset folder of database.\n   */\n  public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {\n    const dbInfo = await this.getDbInfo(server);\n    return dbInfo.datasetFolder;\n  }\n\n  public get language() {\n    return this.options.language || '';\n  }\n\n  /**\n   * Returns the root uri of the virtual filesystem for this database's source archive.\n   */\n  public getSourceArchiveExplorerUri(): vscode.Uri {\n    const sourceArchive = this.sourceArchive;\n    if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith('.zip')) {\n      throw new Error(this.verifyZippedSources());\n    }\n    return encodeArchiveBasePath(sourceArchive.fsPath);\n  }\n\n  public verifyZippedSources(): string | undefined {\n    const sourceArchive = this.sourceArchive;\n    if (sourceArchive === undefined) {\n      return `${this.name} has no source archive.`;\n    }\n\n    if (!sourceArchive.fsPath.endsWith('.zip')) {\n      return `${this.name} has a source folder that is unzipped.`;\n    }\n    return;\n  }\n\n  /**\n   * Holds if `uri` belongs to this database's source archive.\n   */\n  public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean {\n    if (this.sourceArchive === undefined)\n      return false;\n    return uri.scheme === zipArchiveScheme &&\n      decodeSourceArchiveUri(uri).sourceArchiveZipPath === this.sourceArchive.fsPath;\n  }\n\n  public async isAffectedByTest(testPath: string): Promise<boolean> {\n    const databasePath = this.databaseUri.fsPath;\n    if (!databasePath.endsWith('.testproj')) {\n      return false;\n    }\n    try {\n      const stats = await fs.stat(testPath);\n      if (stats.isDirectory()) {\n        return !path.relative(testPath, databasePath).startsWith('..');\n      } else {\n        // database for /one/two/three/test.ql is at /one/two/three/three.testproj\n        const testdir = path.dirname(testPath);\n        const testdirbase = path.basename(testdir);\n        return databasePath == path.join(testdir, testdirbase + '.testproj');\n      }\n    } catch {\n      // No information available for test path - assume database is unaffected.\n      return false;\n    }\n  }\n}\n\n/**\n * A promise that resolves to an event's result value when the event\n * `event` fires. If waiting for the event takes too long (by default\n * >1000ms) log a warning, and resolve to undefined.\n */\nfunction eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | undefined> {\n  return new Promise((res, _rej) => {\n    const timeout = setTimeout(() => {\n      void logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);\n      res(undefined);\n      dispose();\n    }, timeoutMs);\n    const disposable = event(e => {\n      res(e);\n      dispose();\n    });\n    function dispose() {\n      clearTimeout(timeout);\n      disposable.dispose();\n    }\n  });\n}\n\nexport class DatabaseManager extends DisposableObject {\n  private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseChangedEvent>());\n\n  readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;\n\n  private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseChangedEvent>());\n  readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;\n\n  private readonly _databaseItems: DatabaseItem[] = [];\n  private _currentDatabaseItem: DatabaseItem | undefined = undefined;\n\n  constructor(\n    private readonly ctx: ExtensionContext,\n    private readonly qs: QueryServerClient,\n    private readonly cli: cli.CodeQLCliServer,\n    public logger: Logger\n  ) {\n    super();\n\n    qs.onDidStartQueryServer(this.reregisterDatabases.bind(this));\n\n    // Let this run async.\n    void this.loadPersistedState();\n  }\n\n  public async openDatabase(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    uri: vscode.Uri,\n  ): Promise<DatabaseItem> {\n    const contents = await resolveDatabaseContents(uri);\n    // Ignore the source archive for QLTest databases by default.\n    const isQLTestDatabase = path.extname(uri.fsPath) === '.testproj';\n    const fullOptions: FullDatabaseOptions = {\n      ignoreSourceArchive: isQLTestDatabase,\n      // displayName is only set if a user explicitly renames a database\n      displayName: undefined,\n      dateAdded: Date.now(),\n      language: await this.getPrimaryLanguage(uri.fsPath)\n    };\n    const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (event) => {\n      this._onDidChangeDatabaseItem.fire(event);\n    });\n\n    await this.addDatabaseItem(progress, token, databaseItem);\n    await this.addDatabaseSourceArchiveFolder(databaseItem);\n\n    return databaseItem;\n  }\n\n  private async reregisterDatabases(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken\n  ) {\n    let completed = 0;\n    await Promise.all(this._databaseItems.map(async (databaseItem) => {\n      await this.registerDatabase(progress, token, databaseItem);\n      completed++;\n      progress({\n        maxStep: this._databaseItems.length,\n        step: completed,\n        message: 'Re-registering databases'\n      });\n    }));\n  }\n\n  public async addDatabaseSourceArchiveFolder(item: DatabaseItem) {\n    // The folder may already be in workspace state from a previous\n    // session. If not, add it.\n    const index = this.getDatabaseWorkspaceFolderIndex(item);\n    if (index === -1) {\n      // Add that filesystem as a folder to the current workspace.\n      //\n      // It's important that we add workspace folders to the end,\n      // rather than beginning of the list, because the first\n      // workspace folder is special; if it gets updated, the entire\n      // extension host is restarted. (cf.\n      // https://github.com/microsoft/vscode/blob/e0d2ed907d1b22808c56127678fb436d604586a7/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts#L209-L214)\n      //\n      // This is undesirable, as we might be adding and removing many\n      // workspace folders as the user adds and removes databases.\n      const end = (vscode.workspace.workspaceFolders || []).length;\n\n      const msg = item.verifyZippedSources();\n      if (msg) {\n        void logger.log(`Could not add source folder because ${msg}`);\n        return;\n      }\n\n      const uri = item.getSourceArchiveExplorerUri();\n      void logger.log(`Adding workspace folder for ${item.name} source archive at index ${end}`);\n      if ((vscode.workspace.workspaceFolders || []).length < 2) {\n        // Adding this workspace folder makes the workspace\n        // multi-root, which may surprise the user. Let them know\n        // we're doing this.\n        void vscode.window.showInformationMessage(`Adding workspace folder for source archive of database ${item.name}.`);\n      }\n      vscode.workspace.updateWorkspaceFolders(end, 0, {\n        name: `[${item.name} source archive]`,\n        uri,\n      });\n      // vscode api documentation says we must to wait for this event\n      // between multiple `updateWorkspaceFolders` calls.\n      await eventFired(vscode.workspace.onDidChangeWorkspaceFolders);\n    }\n  }\n\n  private async createDatabaseItemFromPersistedState(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    state: PersistedDatabaseItem\n  ): Promise<DatabaseItem> {\n\n    let displayName: string | undefined = undefined;\n    let ignoreSourceArchive = false;\n    let dateAdded = undefined;\n    let language = undefined;\n    if (state.options) {\n      if (typeof state.options.displayName === 'string') {\n        displayName = state.options.displayName;\n      }\n      if (typeof state.options.ignoreSourceArchive === 'boolean') {\n        ignoreSourceArchive = state.options.ignoreSourceArchive;\n      }\n      if (typeof state.options.dateAdded === 'number') {\n        dateAdded = state.options.dateAdded;\n      }\n      language = state.options.language;\n    }\n\n    const dbBaseUri = vscode.Uri.parse(state.uri, true);\n    if (language === undefined) {\n      // we haven't been successful yet at getting the language. try again\n      language = await this.getPrimaryLanguage(dbBaseUri.fsPath);\n    }\n\n    const fullOptions: FullDatabaseOptions = {\n      ignoreSourceArchive,\n      displayName,\n      dateAdded,\n      language\n    };\n    const item = new DatabaseItemImpl(dbBaseUri, undefined, fullOptions,\n      (event) => {\n        this._onDidChangeDatabaseItem.fire(event);\n      });\n\n    await this.addDatabaseItem(progress, token, item);\n    return item;\n  }\n\n  private async loadPersistedState(): Promise<void> {\n    return withProgress({\n      location: vscode.ProgressLocation.Notification\n    },\n      async (progress, token) => {\n        const currentDatabaseUri = this.ctx.workspaceState.get<string>(CURRENT_DB);\n        const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(DB_LIST, []);\n        let step = 0;\n        progress({\n          maxStep: databases.length,\n          message: 'Loading persisted databases',\n          step\n        });\n        try {\n          for (const database of databases) {\n            progress({\n              maxStep: databases.length,\n              message: `Loading ${database.options?.displayName || 'databases'}`,\n              step: ++step\n            });\n\n            const databaseItem = await this.createDatabaseItemFromPersistedState(progress, token, database);\n            try {\n              await databaseItem.refresh();\n              await this.registerDatabase(progress, token, databaseItem);\n              if (currentDatabaseUri === database.uri) {\n                await this.setCurrentDatabaseItem(databaseItem, true);\n              }\n            }\n            catch (e) {\n              // When loading from persisted state, leave invalid databases in the list. They will be\n              // marked as invalid, and cannot be set as the current database.\n            }\n          }\n        } catch (e) {\n          // database list had an unexpected type - nothing to be done?\n          void showAndLogErrorMessage(`Database list loading failed: ${e.message}`);\n        }\n      });\n  }\n\n  public get databaseItems(): readonly DatabaseItem[] {\n    return this._databaseItems;\n  }\n\n  public get currentDatabaseItem(): DatabaseItem | undefined {\n    return this._currentDatabaseItem;\n  }\n\n  public async setCurrentDatabaseItem(\n    item: DatabaseItem | undefined,\n    skipRefresh = false\n  ): Promise<void> {\n\n    if (!skipRefresh && (item !== undefined)) {\n      await item.refresh();  // Will throw on invalid database.\n    }\n    if (this._currentDatabaseItem !== item) {\n      this._currentDatabaseItem = item;\n      this.updatePersistedCurrentDatabaseItem();\n\n      await vscode.commands.executeCommand('setContext', 'codeQL.currentDatabaseItem', item?.name);\n\n      this._onDidChangeCurrentDatabaseItem.fire({\n        item,\n        kind: DatabaseEventKind.Change\n      });\n    }\n  }\n\n  /**\n   * Returns the index of the workspace folder that corresponds to the source archive of `item`\n   * if there is one, and -1 otherwise.\n   */\n  private getDatabaseWorkspaceFolderIndex(item: DatabaseItem): number {\n    return (vscode.workspace.workspaceFolders || [])\n      .findIndex(folder => item.belongsToSourceArchiveExplorerUri(folder.uri));\n  }\n\n  public findDatabaseItem(uri: vscode.Uri): DatabaseItem | undefined {\n    const uriString = uri.toString(true);\n    return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);\n  }\n\n  public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {\n    const uriString = uri.toString(true);\n    return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);\n  }\n\n  private async addDatabaseItem(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    item: DatabaseItem\n  ) {\n    this._databaseItems.push(item);\n    await this.updatePersistedDatabaseList();\n\n    // Add this database item to the allow-list\n    // Database items reconstituted from persisted state\n    // will not have their contents yet.\n    if (item.contents?.datasetUri) {\n      await this.registerDatabase(progress, token, item);\n    }\n    // note that we use undefined as the item in order to reset the entire tree\n    this._onDidChangeDatabaseItem.fire({\n      item: undefined,\n      kind: DatabaseEventKind.Add\n    });\n  }\n\n  public async renameDatabaseItem(item: DatabaseItem, newName: string) {\n    item.name = newName;\n    await this.updatePersistedDatabaseList();\n    this._onDidChangeDatabaseItem.fire({\n      // pass undefined so that the entire tree is rebuilt in order to re-sort\n      item: undefined,\n      kind: DatabaseEventKind.Rename\n    });\n  }\n\n  public async removeDatabaseItem(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    item: DatabaseItem\n  ) {\n    if (this._currentDatabaseItem == item) {\n      this._currentDatabaseItem = undefined;\n    }\n    const index = this.databaseItems.findIndex(searchItem => searchItem === item);\n    if (index >= 0) {\n      this._databaseItems.splice(index, 1);\n    }\n    await this.updatePersistedDatabaseList();\n\n    // Delete folder from workspace, if it is still there\n    const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(\n      folder => item.belongsToSourceArchiveExplorerUri(folder.uri)\n    );\n    if (folderIndex >= 0) {\n      void logger.log(`Removing workspace folder at index ${folderIndex}`);\n      vscode.workspace.updateWorkspaceFolders(folderIndex, 1);\n    }\n\n    // Remove this database item from the allow-list\n    await this.deregisterDatabase(progress, token, item);\n\n    // Delete folder from file system only if it is controlled by the extension\n    if (this.isExtensionControlledLocation(item.databaseUri)) {\n      void logger.log('Deleting database from filesystem.');\n      fs.remove(item.databaseUri.fsPath).then(\n        () => void logger.log(`Deleted '${item.databaseUri.fsPath}'`),\n        e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));\n    }\n\n    // note that we use undefined as the item in order to reset the entire tree\n    this._onDidChangeDatabaseItem.fire({\n      item: undefined,\n      kind: DatabaseEventKind.Remove\n    });\n  }\n\n  private async deregisterDatabase(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    dbItem: DatabaseItem,\n  ) {\n    if (dbItem.contents && (await this.cli.cliConstraints.supportsDatabaseRegistration())) {\n      const databases: Dataset[] = [{\n        dbDir: dbItem.contents.datasetUri.fsPath,\n        workingSet: 'default'\n      }];\n      await this.qs.sendRequest(deregisterDatabases, { databases }, token, progress);\n    }\n  }\n\n  private async registerDatabase(\n    progress: ProgressCallback,\n    token: vscode.CancellationToken,\n    dbItem: DatabaseItem,\n  ) {\n    if (dbItem.contents && (await this.cli.cliConstraints.supportsDatabaseRegistration())) {\n      const databases: Dataset[] = [{\n        dbDir: dbItem.contents.datasetUri.fsPath,\n        workingSet: 'default'\n      }];\n      await this.qs.sendRequest(registerDatabases, { databases }, token, progress);\n    }\n  }\n\n  private updatePersistedCurrentDatabaseItem(): void {\n    void this.ctx.workspaceState.update(CURRENT_DB, this._currentDatabaseItem ?\n      this._currentDatabaseItem.databaseUri.toString(true) : undefined);\n  }\n\n  private async updatePersistedDatabaseList(): Promise<void> {\n    await this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));\n  }\n\n  private isExtensionControlledLocation(uri: vscode.Uri) {\n    const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;\n    // the uri.fsPath function on windows returns a lowercase drive letter,\n    // but storagePath will have an uppercase drive letter. Be sure to compare\n    // URIs to URIs only\n    if (storagePath) {\n      return uri.fsPath.startsWith(vscode.Uri.file(storagePath).fsPath);\n    }\n    return false;\n  }\n\n  private async getPrimaryLanguage(dbPath: string) {\n    if (!(await this.cli.cliConstraints.supportsLanguageName())) {\n      // return undefined so that we recalculate on restart until the cli is at a version that\n      // supports this feature. This recalculation is cheap since we avoid calling into the cli\n      // unless we know it can return the langauges property.\n      return undefined;\n    }\n    const dbInfo = await this.cli.resolveDatabase(dbPath);\n    return dbInfo.languages?.[0] || '';\n  }\n}\n\n/**\n * Get the set of directories containing upgrades, given a list of\n * scripts returned by the cli's upgrade resolution.\n */\nexport function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {\n  const parentDirs = scripts.map(dir => path.dirname(dir));\n  const uniqueParentDirs = new Set(parentDirs);\n  return Array.from(uniqueParentDirs).map(filePath => vscode.Uri.file(filePath));\n}\n"
      }
    }, {
      "location" : {
        "uri" : "extensions/ql-vscode/src/discovery.ts",
        "uriBaseId" : "%SRCROOT%",
        "index" : 2
      },
      "contents" : {
        "text" : "import { DisposableObject } from './pure/disposable-object';\nimport { logger } from './logging';\n\n/**\n * Base class for \"discovery\" operations, which scan the file system to find specific kinds of\n * files. This class automatically prevents more than one discovery operation from running at the\n * same time.\n */\nexport abstract class Discovery<T> extends DisposableObject {\n  private retry = false;\n  private discoveryInProgress = false;\n\n  constructor(private readonly name: string) {\n    super();\n  }\n\n  /**\n   * Force the discovery process to run. Normally invoked by the derived class when a relevant file\n   * system change is detected.\n   */\n  public refresh(): void {\n    // We avoid having multiple discovery operations in progress at the same time. Otherwise, if we\n    // got a storm of refresh requests due to, say, the copying or deletion of a large directory\n    // tree, we could potentially spawn a separate simultaneous discovery operation for each\n    // individual file change notification.\n    // Our approach is to spawn a discovery operation immediately upon receiving the first refresh\n    // request. If we receive any additional refresh requests before the first one is complete, we\n    // record this fact by setting `this.retry = true`. When the original discovery operation\n    // completes, we discard its results and spawn another one to account for that additional\n    // changes that have happened since.\n    // The means that for the common case of a single file being modified, we'll complete the\n    // discovery and update as soon as possible. If multiple files are being modified, we'll\n    // probably wind up doing discovery at least twice.\n    // We could choose to delay the initial discovery request by a second or two to wait for any\n    // other change notifications that might be coming along. However, this would create more\n    // latency in the common case, in order to save a bit of latency in the uncommon case.\n\n    if (this.discoveryInProgress) {\n      // There's already a discovery operation in progress. Tell it to restart when it's done.\n      this.retry = true;\n    }\n    else {\n      // No discovery in progress, so start one now.\n      this.discoveryInProgress = true;\n      this.launchDiscovery();\n    }\n  }\n\n  /**\n   * Starts the asynchronous discovery operation by invoking the `discover` function. When the\n   * discovery operation completes, the `update` function will be invoked with the results of the\n   * discovery.\n   */\n  private launchDiscovery(): void {\n    const discoveryPromise = this.discover();\n    discoveryPromise.then(results => {\n      if (!this.retry) {\n        // Update any listeners with the results of the discovery.\n        this.discoveryInProgress = false;\n        this.update(results);\n      }\n    })\n\n      .catch(err => {\n        void logger.log(`${this.name} failed. Reason: ${err.message}`);\n      })\n\n      .finally(() => {\n        if (this.retry) {\n          // Another refresh request came in while we were still running a previous discovery\n          // operation. Since the discovery results we just computed are now stale, we'll launch\n          // another discovery operation instead of updating.\n          // Note that by doing this inside of `finally`, we will relaunch discovery even if the\n          // initial discovery operation failed.\n          this.retry = false;\n          this.launchDiscovery();\n        }\n      });\n  }\n\n  /**\n   * Overridden by the derived class to spawn the actual discovery operation, returning the results.\n   */\n  protected abstract discover(): Promise<T>;\n\n  /**\n   * Overridden by the derived class to atomically update the `Discovery` object with the results of\n   * the discovery operation, and to notify any listeners that the discovery results may have\n   * changed.\n   * @param results The discovery results returned by the `discover` function.\n   */\n  protected abstract update(results: T): void;\n}\n"
      }
    }, {
      "location" : {
        "uri" : "extensions/ql-vscode/src/query-history.ts",
        "uriBaseId" : "%SRCROOT%",
        "index" : 3
      },
      "contents" : {
        "text" : "import * as path from 'path';\nimport {\n  commands,\n  Disposable,\n  env,\n  Event,\n  EventEmitter,\n  ExtensionContext,\n  ProviderResult,\n  Range,\n  ThemeIcon,\n  TreeItem,\n  TreeView,\n  Uri,\n  ViewColumn,\n  window,\n  workspace,\n} from 'vscode';\nimport { QueryHistoryConfig } from './config';\nimport {\n  showAndLogErrorMessage,\n  showAndLogInformationMessage,\n  showAndLogWarningMessage,\n  showBinaryChoiceDialog\n} from './helpers';\nimport { logger } from './logging';\nimport { URLSearchParams } from 'url';\nimport { QueryServerClient } from './queryserver-client';\nimport { DisposableObject } from './pure/disposable-object';\nimport { commandRunner } from './commandRunner';\nimport { assertNever, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from './pure/helpers-pure';\nimport { CompletedLocalQueryInfo, LocalQueryInfo as LocalQueryInfo, QueryHistoryInfo } from './query-results';\nimport { DatabaseManager } from './databases';\nimport { registerQueryHistoryScubber } from './query-history-scrubber';\nimport { QueryStatus } from './query-status';\nimport { slurpQueryHistory, splatQueryHistory } from './query-serialization';\n\n/**\n * query-history.ts\n * ------------\n * Managing state of previous queries that we've executed.\n *\n * The source of truth of the current state resides inside the\n * `TreeDataProvider` subclass below.\n */\n\nexport const SHOW_QUERY_TEXT_MSG = `\\\n////////////////////////////////////////////////////////////////////////////////////\n// This is the text of the entire query file when it was executed for this query  //\n// run. The text or dependent libraries may have changed since then.              //\n//                                                                                //\n// This buffer is readonly. To re-execute this query, you must open the original  //\n// query file.                                                                    //\n////////////////////////////////////////////////////////////////////////////////////\n\n`;\n\nconst SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\\\n////////////////////////////////////////////////////////////////////////////////////\n// This is the Quick Eval selection of the query file when it was executed for    //\n// this query run. The text or dependent libraries may have changed since then.   //\n//                                                                                //\n// This buffer is readonly. To re-execute this query, you must open the original  //\n// query file.                                                                    //\n////////////////////////////////////////////////////////////////////////////////////\n\n`;\n\n/**\n * Path to icon to display next to a failed query history item.\n */\nconst FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';\n\n/**\n * Path to icon to display next to a successful local run.\n */\nconst LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON = 'media/drive.svg';\n\n/**\n * Path to icon to display next to a successful remote run.\n */\nconst REMOTE_SUCCESS_QUERY_HISTORY_ITEM_ICON = 'media/globe.svg';\n\nexport enum SortOrder {\n  NameAsc = 'NameAsc',\n  NameDesc = 'NameDesc',\n  DateAsc = 'DateAsc',\n  DateDesc = 'DateDesc',\n  CountAsc = 'CountAsc',\n  CountDesc = 'CountDesc',\n}\n\n/**\n * Number of milliseconds two clicks have to arrive apart to be\n * considered a double-click.\n */\nconst DOUBLE_CLICK_TIME = 500;\n\nconst WORKSPACE_QUERY_HISTORY_FILE = 'workspace-query-history.json';\n\n/**\n * Tree data provider for the query history view.\n */\nexport class HistoryTreeDataProvider extends DisposableObject {\n  private _sortOrder = SortOrder.DateAsc;\n\n  private _onDidChangeTreeData = super.push(new EventEmitter<QueryHistoryInfo | undefined>());\n\n  readonly onDidChangeTreeData: Event<QueryHistoryInfo | undefined> = this\n    ._onDidChangeTreeData.event;\n\n  private history: QueryHistoryInfo[] = [];\n\n  private failedIconPath: string;\n\n  private localSuccessIconPath: string;\n\n  private remoteSuccessIconPath: string;\n\n  private current: QueryHistoryInfo | undefined;\n\n  constructor(extensionPath: string) {\n    super();\n    this.failedIconPath = path.join(\n      extensionPath,\n      FAILED_QUERY_HISTORY_ITEM_ICON\n    );\n    this.localSuccessIconPath = path.join(\n      extensionPath,\n      LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON\n    );\n    this.remoteSuccessIconPath = path.join(\n      extensionPath,\n      REMOTE_SUCCESS_QUERY_HISTORY_ITEM_ICON\n    );\n  }\n\n  async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {\n    const treeItem = new TreeItem(element.label);\n\n    treeItem.command = {\n      title: 'Query History Item',\n      command: 'codeQLQueryHistory.itemClicked',\n      arguments: [element],\n      tooltip: element.failureReason || element.label\n    };\n\n    // Populate the icon and the context value. We use the context value to\n    // control which commands are visible in the context menu.\n    let hasResults;\n    switch (element.status) {\n      case QueryStatus.InProgress:\n        treeItem.iconPath = new ThemeIcon('sync~spin');\n        treeItem.contextValue = 'inProgressResultsItem';\n        break;\n      case QueryStatus.Completed:\n        if (element.t === 'local') {\n          hasResults = await element.completedQuery?.query.hasInterpretedResults();\n          treeItem.iconPath = this.localSuccessIconPath;\n          treeItem.contextValue = hasResults\n            ? 'interpretedResultsItem'\n            : 'rawResultsItem';\n        } else {\n          treeItem.iconPath = this.remoteSuccessIconPath;\n          treeItem.contextValue = 'remoteResultsItem';\n        }\n        break;\n      case QueryStatus.Failed:\n        treeItem.iconPath = this.failedIconPath;\n        treeItem.contextValue = 'cancelledResultsItem';\n        break;\n      default:\n        assertNever(element.status);\n    }\n\n    return treeItem;\n  }\n\n  getChildren(\n    element?: QueryHistoryInfo\n  ): ProviderResult<QueryHistoryInfo[]> {\n    return element ? [] : this.history.sort((h1, h2) => {\n\n      // TODO remote queries are not implemented yet.\n      if (h1.t !== 'local' && h2.t !== 'local') {\n        return 0;\n      }\n      if (h1.t !== 'local') {\n        return -1;\n      }\n      if (h2.t !== 'local') {\n        return 1;\n      }\n\n      const resultCount1 = h1.completedQuery?.resultCount ?? -1;\n      const resultCount2 = h2.completedQuery?.resultCount ?? -1;\n\n      switch (this.sortOrder) {\n        case SortOrder.NameAsc:\n          return h1.label.localeCompare(h2.label, env.language);\n        case SortOrder.NameDesc:\n          return h2.label.localeCompare(h1.label, env.language);\n        case SortOrder.DateAsc:\n          return h1.initialInfo.start.getTime() - h2.initialInfo.start.getTime();\n        case SortOrder.DateDesc:\n          return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime();\n        case SortOrder.CountAsc:\n          // If the result counts are equal, sort by name.\n          return resultCount1 - resultCount2 === 0\n            ? h1.label.localeCompare(h2.label, env.language)\n            : resultCount1 - resultCount2;\n        case SortOrder.CountDesc:\n          // If the result counts are equal, sort by name.\n          return resultCount2 - resultCount1 === 0\n            ? h2.label.localeCompare(h1.label, env.language)\n            : resultCount2 - resultCount1;\n        default:\n          assertNever(this.sortOrder);\n      }\n    });\n  }\n\n  getParent(_element: QueryHistoryInfo): ProviderResult<QueryHistoryInfo> {\n    return null;\n  }\n\n  getCurrent(): QueryHistoryInfo | undefined {\n    return this.current;\n  }\n\n  pushQuery(item: QueryHistoryInfo): void {\n    this.history.push(item);\n    this.setCurrentItem(item);\n    this.refresh();\n  }\n\n  setCurrentItem(item?: QueryHistoryInfo) {\n    this.current = item;\n  }\n\n  remove(item: QueryHistoryInfo) {\n    const isCurrent = this.current === item;\n    if (isCurrent) {\n      this.setCurrentItem();\n    }\n    const index = this.history.findIndex((i) => i === item);\n    if (index >= 0) {\n      this.history.splice(index, 1);\n      if (isCurrent && this.history.length > 0) {\n        // Try to keep a current item, near the deleted item if there\n        // are any available.\n        this.setCurrentItem(this.history[Math.min(index, this.history.length - 1)]);\n      }\n      this.refresh();\n    }\n  }\n\n  get allHistory(): QueryHistoryInfo[] {\n    return this.history;\n  }\n\n  set allHistory(history: QueryHistoryInfo[]) {\n    this.history = history;\n    this.current = history[0];\n    this.refresh();\n  }\n\n  refresh() {\n    this._onDidChangeTreeData.fire(undefined);\n  }\n\n  public get sortOrder() {\n    return this._sortOrder;\n  }\n\n  public set sortOrder(newSortOrder: SortOrder) {\n    this._sortOrder = newSortOrder;\n    this._onDidChangeTreeData.fire(undefined);\n  }\n}\n\nexport class QueryHistoryManager extends DisposableObject {\n\n  treeDataProvider: HistoryTreeDataProvider;\n  treeView: TreeView<QueryHistoryInfo>;\n  lastItemClick: { time: Date; item: QueryHistoryInfo } | undefined;\n  compareWithItem: LocalQueryInfo | undefined;\n  queryHistoryScrubber: Disposable | undefined;\n  private queryMetadataStorageLocation;\n\n  private readonly _onDidAddQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());\n  readonly onDidAddQueryItem: Event<QueryHistoryInfo> = this\n    ._onDidAddQueryItem.event;\n\n  private readonly _onDidRemoveQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());\n  readonly onDidRemoveQueryItem: Event<QueryHistoryInfo> = this\n    ._onDidRemoveQueryItem.event;\n\n  private readonly _onWillOpenQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());\n  readonly onWillOpenQueryItem: Event<QueryHistoryInfo> = this\n    ._onWillOpenQueryItem.event;\n\n  constructor(\n    private qs: QueryServerClient,\n    private dbm: DatabaseManager,\n    private queryStorageDir: string,\n    ctx: ExtensionContext,\n    private queryHistoryConfigListener: QueryHistoryConfig,\n    private doCompareCallback: (\n      from: CompletedLocalQueryInfo,\n      to: CompletedLocalQueryInfo\n    ) => Promise<void>\n  ) {\n    super();\n\n    // Note that we use workspace storage to hold the metadata for the query history.\n    // This is because the query history is specific to each workspace.\n    // For situations where `ctx.storageUri` is undefined (i.e., there is no workspace),\n    // we default to global storage.\n    this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);\n\n    this.treeDataProvider = this.push(new HistoryTreeDataProvider(\n      ctx.extensionPath\n    ));\n    this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {\n      treeDataProvider: this.treeDataProvider,\n      canSelectMany: true,\n    }));\n\n    // Lazily update the tree view selection due to limitations of TreeView API (see\n    // `updateTreeViewSelectionIfVisible` doc for details)\n    this.push(\n      this.treeView.onDidChangeVisibility(async (_ev) =>\n        this.updateTreeViewSelectionIfVisible()\n      )\n    );\n    this.push(\n      this.treeView.onDidChangeSelection(async (ev) => {\n        if (ev.selection.length === 0) {\n          // Don't allow the selection to become empty\n          this.updateTreeViewSelectionIfVisible();\n        } else {\n          this.treeDataProvider.setCurrentItem(ev.selection[0]);\n        }\n        if (ev.selection.some(item => item.t !== 'local')) {\n          // Don't allow comparison of non-local items\n          this.updateCompareWith([]);\n        } else {\n          this.updateCompareWith([...ev.selection] as LocalQueryInfo[]);\n        }\n      })\n    );\n\n    void logger.log('Registering query history panel commands.');\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.openQuery',\n        this.handleOpenQuery.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.removeHistoryItem',\n        this.handleRemoveHistoryItem.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.sortByName',\n        this.handleSortByName.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.sortByDate',\n        this.handleSortByDate.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.sortByCount',\n        this.handleSortByCount.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.setLabel',\n        this.handleSetLabel.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.compareWith',\n        this.handleCompareWith.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.showQueryLog',\n        this.handleShowQueryLog.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.cancel',\n        this.handleCancel.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.showQueryText',\n        this.handleShowQueryText.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.viewCsvResults',\n        this.handleViewCsvResults.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.viewCsvAlerts',\n        this.handleViewCsvAlerts.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.viewSarifAlerts',\n        this.handleViewSarifAlerts.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.viewDil',\n        this.handleViewDil.bind(this)\n      )\n    );\n    this.push(\n      commandRunner(\n        'codeQLQueryHistory.itemClicked',\n        async (item: LocalQueryInfo) => {\n          return this.handleItemClicked(item, [item]);\n        }\n      )\n    );\n\n    // There are two configuration items that affect the query history:\n    // 1. The ttl for query history items.\n    // 2. The default label for query history items.\n    // When either of these change, must refresh the tree view.\n    this.push(\n      queryHistoryConfigListener.onDidChangeConfiguration(() => {\n        this.treeDataProvider.refresh();\n        this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);\n      })\n    );\n\n    // displays query text in a read-only document\n    this.push(workspace.registerTextDocumentContentProvider('codeql', {\n      provideTextDocumentContent(\n        uri: Uri\n      ): ProviderResult<string> {\n        const params = new URLSearchParams(uri.query);\n\n        return (\n          (JSON.parse(params.get('isQuickEval') || '')\n            ? SHOW_QUERY_TEXT_QUICK_EVAL_MSG\n            : SHOW_QUERY_TEXT_MSG) + params.get('queryText')\n        );\n      },\n    }));\n\n    this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);\n  }\n\n  /**\n   * Register and create the history scrubber.\n   */\n  private registerQueryHistoryScrubber(queryHistoryConfigListener: QueryHistoryConfig, ctx: ExtensionContext) {\n    this.queryHistoryScrubber?.dispose();\n    // Every hour check if we need to re-run the query history scrubber.\n    this.queryHistoryScrubber = this.push(\n      registerQueryHistoryScubber(\n        ONE_HOUR_IN_MS,\n        TWO_HOURS_IN_MS,\n        queryHistoryConfigListener.ttlInMillis,\n        this.queryStorageDir,\n        ctx\n      )\n    );\n  }\n\n  async readQueryHistory(): Promise<void> {\n    void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);\n    const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);\n    this.treeDataProvider.allHistory = history;\n    this.treeDataProvider.allHistory.forEach((item) => {\n      this._onDidAddQueryItem.fire(item);\n    });\n  }\n\n  async writeQueryHistory(): Promise<void> {\n    await splatQueryHistory(this.treeDataProvider.allHistory, this.queryMetadataStorageLocation);\n  }\n\n  async handleOpenQuery(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ): Promise<void> {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {\n      return;\n    }\n\n    const queryPath = finalSingleItem.t === 'local'\n      ? finalSingleItem.initialInfo.queryPath\n      : finalSingleItem.remoteQuery.queryFilePath;\n\n    const textDocument = await workspace.openTextDocument(\n      Uri.file(queryPath)\n    );\n    const editor = await window.showTextDocument(\n      textDocument,\n      ViewColumn.One\n    );\n\n    if (finalSingleItem.t === 'local') {\n      const queryText = finalSingleItem.initialInfo.queryText;\n      if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {\n        await editor.edit((edit) =>\n          edit.replace(\n            textDocument.validateRange(\n              new Range(0, 0, textDocument.lineCount, 0)\n            ),\n            queryText\n          )\n        );\n      }\n    }\n  }\n\n  async handleRemoveHistoryItem(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[] = []\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n    const toDelete = (finalMultiSelect || [finalSingleItem]);\n    await Promise.all(toDelete.map(async (item) => {\n      if (item.t === 'local') {\n        // Removing in progress local queries is not supported. They must be cancelled first.\n        if (item.status !== QueryStatus.InProgress) {\n          this.treeDataProvider.remove(item);\n          item.completedQuery?.dispose();\n\n          // User has explicitly asked for this query to be removed.\n          // We need to delete it from disk as well.\n          await item.completedQuery?.query.deleteQuery();\n        }\n      } else {\n        // Remote queries can be removed locally, but not remotely.\n        // The user must cancel the query on GitHub Actions explicitly.\n        this.treeDataProvider.remove(item);\n        void logger.log(`Deleted ${item.label}.`);\n        if (item.status === QueryStatus.InProgress) {\n          void logger.log('The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.');\n        }\n\n        this._onDidRemoveQueryItem.fire(item);\n      }\n\n    }));\n    await this.writeQueryHistory();\n    const current = this.treeDataProvider.getCurrent();\n    if (current !== undefined) {\n      await this.treeView.reveal(current, { select: true });\n      await this._onWillOpenQueryItem.fire(current);\n    }\n  }\n\n  async handleSortByName() {\n    if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {\n      this.treeDataProvider.sortOrder = SortOrder.NameDesc;\n    } else {\n      this.treeDataProvider.sortOrder = SortOrder.NameAsc;\n    }\n  }\n\n  async handleSortByDate() {\n    if (this.treeDataProvider.sortOrder === SortOrder.DateAsc) {\n      this.treeDataProvider.sortOrder = SortOrder.DateDesc;\n    } else {\n      this.treeDataProvider.sortOrder = SortOrder.DateAsc;\n    }\n  }\n\n  async handleSortByCount() {\n    if (this.treeDataProvider.sortOrder === SortOrder.CountAsc) {\n      this.treeDataProvider.sortOrder = SortOrder.CountDesc;\n    } else {\n      this.treeDataProvider.sortOrder = SortOrder.CountAsc;\n    }\n  }\n\n  async handleSetLabel(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ): Promise<void> {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    // TODO will support remote queries\n    if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {\n      return;\n    }\n\n    const response = await window.showInputBox({\n      prompt: 'Label:',\n      placeHolder: '(use default)',\n      value: finalSingleItem.label,\n    });\n    // undefined response means the user cancelled the dialog; don't change anything\n    if (response !== undefined) {\n      // Interpret empty string response as 'go back to using default'\n      finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;\n      this.treeDataProvider.refresh();\n    }\n  }\n\n  async handleCompareWith(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    try {\n      // local queries only\n      if (finalSingleItem?.t !== 'local') {\n        throw new Error('Please select a local query.');\n      }\n\n      if (!finalSingleItem.completedQuery?.didRunSuccessfully) {\n        throw new Error('Please select a query that has completed successfully.');\n      }\n\n      const from = this.compareWithItem || singleItem;\n      const to = await this.findOtherQueryToCompare(from, finalMultiSelect);\n\n      if (from.completed && to?.completed) {\n        await this.doCompareCallback(from as CompletedLocalQueryInfo, to as CompletedLocalQueryInfo);\n      }\n    } catch (e) {\n      void showAndLogErrorMessage(e.message);\n    }\n  }\n\n  async handleItemClicked(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {\n      return;\n    }\n\n    this.treeDataProvider.setCurrentItem(finalSingleItem);\n\n    const now = new Date();\n    const prevItemClick = this.lastItemClick;\n    this.lastItemClick = { time: now, item: finalSingleItem };\n\n    if (\n      prevItemClick !== undefined &&\n      now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&\n      finalSingleItem == prevItemClick.item\n    ) {\n      // show original query file on double click\n      await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);\n    } else {\n      // show results on single click\n      await this._onWillOpenQueryItem.fire(finalSingleItem);\n    }\n  }\n\n  async handleShowQueryLog(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    // Local queries only\n    if (!this.assertSingleQuery(multiSelect) || singleItem?.t !== 'local') {\n      return;\n    }\n\n    if (!singleItem.completedQuery) {\n      return;\n    }\n\n    if (singleItem.completedQuery.logFileLocation) {\n      await this.tryOpenExternalFile(singleItem.completedQuery.logFileLocation);\n    } else {\n      void showAndLogWarningMessage('No log file available');\n    }\n  }\n\n  async handleCancel(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    // Local queries only\n    // In the future, we may support cancelling remote queries, but this is not a short term plan.\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    (finalMultiSelect || [finalSingleItem]).forEach((item) => {\n      if (item.status === QueryStatus.InProgress && item.t === 'local') {\n        item.cancel();\n      }\n    });\n  }\n\n  async handleShowQueryText(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {\n      return;\n    }\n\n    const params = new URLSearchParams({\n      isQuickEval: String(!!(finalSingleItem.t === 'local' && finalSingleItem.initialInfo.quickEvalPosition)),\n      queryText: encodeURIComponent(await this.getQueryText(finalSingleItem)),\n    });\n    const queryId = finalSingleItem.t === 'local'\n      ? finalSingleItem.initialInfo.id\n      : finalSingleItem.queryId;\n\n    const uri = Uri.parse(\n      `codeql:${queryId}?${params.toString()}`, true\n    );\n    const doc = await workspace.openTextDocument(uri);\n    await window.showTextDocument(doc, { preview: false });\n  }\n\n  async handleViewSarifAlerts(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    // Local queries only\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {\n      return;\n    }\n\n    const query = finalSingleItem.completedQuery.query;\n    const hasInterpretedResults = query.canHaveInterpretedResults();\n    if (hasInterpretedResults) {\n      await this.tryOpenExternalFile(\n        query.resultsPaths.interpretedResultsPath\n      );\n    } else {\n      const label = finalSingleItem.label;\n      void showAndLogInformationMessage(\n        `Query ${label} has no interpreted results.`\n      );\n    }\n  }\n\n  async handleViewCsvResults(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    // Local queries only\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {\n      return;\n    }\n    const query = finalSingleItem.completedQuery.query;\n    if (await query.hasCsv()) {\n      void this.tryOpenExternalFile(query.csvPath);\n      return;\n    }\n    await query.exportCsvResults(this.qs, query.csvPath, () => {\n      void this.tryOpenExternalFile(\n        query.csvPath\n      );\n    });\n  }\n\n  async handleViewCsvAlerts(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    // Local queries only\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {\n      return;\n    }\n\n    await this.tryOpenExternalFile(\n      await finalSingleItem.completedQuery.query.ensureCsvAlerts(this.qs, this.dbm)\n    );\n  }\n\n  async handleViewDil(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[],\n  ) {\n    const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);\n\n    // Local queries only\n    if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {\n      return;\n    }\n\n    await this.tryOpenExternalFile(\n      await finalSingleItem.completedQuery.query.ensureDilPath(this.qs)\n    );\n  }\n\n  async getQueryText(item: QueryHistoryInfo): Promise<string> {\n    return item.t === 'local'\n      ? item.initialInfo.queryText\n      : item.remoteQuery.queryText;\n  }\n\n  addQuery(item: QueryHistoryInfo) {\n    this.treeDataProvider.pushQuery(item);\n    this.updateTreeViewSelectionIfVisible();\n    this._onDidAddQueryItem.fire(item);\n  }\n\n  /**\n   * Update the tree view selection if the tree view is visible.\n   *\n   * If the tree view is not visible, we must wait until it becomes visible before updating the\n   * selection. This is because the only mechanism for updating the selection of the tree view\n   * has the side-effect of revealing the tree view. This changes the active sidebar to CodeQL,\n   * interrupting user workflows such as writing a commit message on the source control sidebar.\n   */\n  private updateTreeViewSelectionIfVisible() {\n    if (this.treeView.visible) {\n      const current = this.treeDataProvider.getCurrent();\n      if (current != undefined) {\n        // We must fire the onDidChangeTreeData event to ensure the current element can be selected\n        // using `reveal` if the tree view was not visible when the current element was added.\n        this.treeDataProvider.refresh();\n        void this.treeView.reveal(current, { select: true });\n      }\n    }\n  }\n\n  private async tryOpenExternalFile(fileLocation: string) {\n    const uri = Uri.file(fileLocation);\n    try {\n      await window.showTextDocument(uri, { preview: false });\n    } catch (e) {\n      if (\n        e.message.includes(\n          'Files above 50MB cannot be synchronized with extensions'\n        ) ||\n        e.message.includes('too large to open')\n      ) {\n        const res = await showBinaryChoiceDialog(\n          `VS Code does not allow extensions to open files >50MB. This file\nexceeds that limit. Do you want to open it outside of VS Code?\n\nYou can also try manually opening it inside VS Code by selecting\nthe file in the file explorer and dragging it into the workspace.`\n        );\n        if (res) {\n          try {\n            await commands.executeCommand('revealFileInOS', uri);\n          } catch (e) {\n            void showAndLogErrorMessage(e.message);\n          }\n        }\n      } else {\n        void showAndLogErrorMessage(`Could not open file ${fileLocation}`);\n        void logger.log(e.message);\n        void logger.log(e.stack);\n      }\n    }\n  }\n\n  private async findOtherQueryToCompare(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ): Promise<CompletedLocalQueryInfo | undefined> {\n\n    // Remote queries cannot be compared\n    if (singleItem.t !== 'local' || multiSelect.some(s => s.t !== 'local') || !singleItem.completedQuery) {\n      return undefined;\n    }\n    const dbName = singleItem.initialInfo.databaseInfo.name;\n\n    // if exactly 2 queries are selected, use those\n    if (multiSelect?.length === 2) {\n      // return the query that is not the first selected one\n      const otherQuery =\n        (singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0]) as LocalQueryInfo;\n      if (!otherQuery.completedQuery) {\n        throw new Error('Please select a completed query.');\n      }\n      if (!otherQuery.completedQuery.didRunSuccessfully) {\n        throw new Error('Please select a successful query.');\n      }\n      if (otherQuery.initialInfo.databaseInfo.name !== dbName) {\n        throw new Error('Query databases must be the same.');\n      }\n      return otherQuery as CompletedLocalQueryInfo;\n    }\n\n    if (multiSelect?.length > 2) {\n      throw new Error('Please select no more than 2 queries.');\n    }\n\n    // otherwise, let the user choose\n    const comparableQueryLabels = this.treeDataProvider.allHistory\n      .filter(\n        (otherQuery) =>\n          otherQuery !== singleItem &&\n          otherQuery.t === 'local' &&\n          otherQuery.completedQuery &&\n          otherQuery.completedQuery.didRunSuccessfully &&\n          otherQuery.initialInfo.databaseInfo.name === dbName\n      )\n      .map((item) => ({\n        label: item.label,\n        description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,\n        detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,\n        query: item as CompletedLocalQueryInfo,\n      }));\n    if (comparableQueryLabels.length < 1) {\n      throw new Error('No other queries available to compare with.');\n    }\n    const choice = await window.showQuickPick(comparableQueryLabels);\n    return choice?.query;\n  }\n\n  private assertSingleQuery(multiSelect: QueryHistoryInfo[] = [], message = 'Please select a single query.') {\n    if (multiSelect.length > 1) {\n      void showAndLogErrorMessage(\n        message\n      );\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * Updates the compare with source query. This ensures that all compare command invocations\n   * when exactly 2 queries are selected always have the proper _from_ query. Always use\n   * compareWithItem as the _from_ query.\n   *\n   * The heuristic is this:\n   *\n   * 1. If selection is empty or has length > 2 delete compareWithItem.\n   * 2. If selection is length 1, then set that item to compareWithItem.\n   * 3. If selection is length 2, then make sure compareWithItem is one of the selected items\n   *    if not, then delete compareWithItem. If it is then, do nothing.\n   *\n   * This ensures that compareWithItem is always the first item selected if there are only\n   * two selected items.\n   *\n   * @param newSelection the new selection after the most recent selection change\n   */\n  private updateCompareWith(newSelection: LocalQueryInfo[]) {\n    if (newSelection.length === 1) {\n      this.compareWithItem = newSelection[0];\n    } else if (\n      newSelection.length !== 2 ||\n      !this.compareWithItem ||\n      !newSelection.includes(this.compareWithItem)\n    ) {\n      this.compareWithItem = undefined;\n    }\n  }\n\n  /**\n   * If no items are selected, attempt to grab the selection from the treeview.\n   * However, often the treeview itself does not have any selection. In this case,\n   * grab the selection from the `treeDataProvider` current item.\n   *\n   * We need to use this method because when clicking on commands from the view title\n   * bar, the selections are not passed in.\n   *\n   * @param singleItem the single item selected, or undefined if no item is selected\n   * @param multiSelect a multi-select or undefined if no items are selected\n   */\n  private determineSelection(\n    singleItem: QueryHistoryInfo,\n    multiSelect: QueryHistoryInfo[]\n  ): {\n    finalSingleItem: QueryHistoryInfo;\n    finalMultiSelect: QueryHistoryInfo[]\n  } {\n    if (!singleItem && !multiSelect?.[0]) {\n      const selection = this.treeView.selection;\n      const current = this.treeDataProvider.getCurrent();\n      if (selection?.length) {\n        return {\n          finalSingleItem: selection[0],\n          finalMultiSelect: [...selection]\n        };\n      } else if (current) {\n        return {\n          finalSingleItem: current,\n          finalMultiSelect: [current]\n        };\n      }\n    }\n\n    // ensure we only return undefined if we have neither a single or multi-selecion\n    if (singleItem && !multiSelect?.[0]) {\n      multiSelect = [singleItem];\n    } else if (!singleItem && multiSelect?.[0]) {\n      singleItem = multiSelect[0];\n    }\n    return {\n      finalSingleItem: singleItem,\n      finalMultiSelect: multiSelect\n    };\n  }\n\n  async refreshTreeView(): Promise<void> {\n    this.treeDataProvider.refresh();\n    await this.writeQueryHistory();\n  }\n}\n"
      }
    } ],
    "results" : [ {
      "ruleId" : "remote-query",
      "ruleIndex" : 0,
      "rule" : {
        "id" : "remote-query",
        "index" : 0
      },
      "message" : {
        "text" : "Refresh method"
      },
      "locations" : [ {
        "physicalLocation" : {
          "artifactLocation" : {
            "uri" : "extensions/ql-vscode/src/astViewer.ts",
            "uriBaseId" : "%SRCROOT%",
            "index" : 0
          },
          "region" : {
            "startLine" : 58,
            "startColumn" : 3,
            "endLine" : 60,
            "endColumn" : 4
          }
        }
      } ],
      "partialFingerprints" : {
        "primaryLocationLineHash" : "6e8184a0a4dee1d0:1",
        "primaryLocationStartColumnFingerprint" : "0"
      }
    }, {
      "ruleId" : "remote-query",
      "ruleIndex" : 0,
      "rule" : {
        "id" : "remote-query",
        "index" : 0
      },
      "message" : {
        "text" : "Refresh method"
      },
      "locations" : [ {
        "physicalLocation" : {
          "artifactLocation" : {
            "uri" : "extensions/ql-vscode/src/databases.ts",
            "uriBaseId" : "%SRCROOT%",
            "index" : 1
          },
          "region" : {
            "startLine" : 234,
            "startColumn" : 3,
            "endColumn" : 28
          }
        }
      } ],
      "partialFingerprints" : {
        "primaryLocationLineHash" : "582c06e6df85ad64:1",
        "primaryLocationStartColumnFingerprint" : "0"
      }
    }, {
      "ruleId" : "remote-query",
      "ruleIndex" : 0,
      "rule" : {
        "id" : "remote-query",
        "index" : 0
      },
      "message" : {
        "text" : "Refresh method"
      },
      "locations" : [ {
        "physicalLocation" : {
          "artifactLocation" : {
            "uri" : "extensions/ql-vscode/src/databases.ts",
            "uriBaseId" : "%SRCROOT%",
            "index" : 1
          },
          "region" : {
            "startLine" : 354,
            "startColumn" : 3,
            "endLine" : 372,
            "endColumn" : 4
          }
        }
      } ],
      "partialFingerprints" : {
        "primaryLocationLineHash" : "6900ddb6b42d9006:1",
        "primaryLocationStartColumnFingerprint" : "0"
      }
    }, {
      "ruleId" : "remote-query",
      "ruleIndex" : 0,
      "rule" : {
        "id" : "remote-query",
        "index" : 0
      },
      "message" : {
        "text" : "Refresh method"
      },
      "locations" : [ {
        "physicalLocation" : {
          "artifactLocation" : {
            "uri" : "extensions/ql-vscode/src/discovery.ts",
            "uriBaseId" : "%SRCROOT%",
            "index" : 2
          },
          "region" : {
            "startLine" : 21,
            "startColumn" : 3,
            "endLine" : 47,
            "endColumn" : 4
          }
        }
      } ],
      "partialFingerprints" : {
        "primaryLocationLineHash" : "52ccaf6c251fbca8:1",
        "primaryLocationStartColumnFingerprint" : "0"
      }
    }, {
      "ruleId" : "remote-query",
      "ruleIndex" : 0,
      "rule" : {
        "id" : "remote-query",
        "index" : 0
      },
      "message" : {
        "text" : "Refresh method"
      },
      "locations" : [ {
        "physicalLocation" : {
          "artifactLocation" : {
            "uri" : "extensions/ql-vscode/src/query-history.ts",
            "uriBaseId" : "%SRCROOT%",
            "index" : 3
          },
          "region" : {
            "startLine" : 268,
            "startColumn" : 3,
            "endLine" : 270,
            "endColumn" : 4
          }
        }
      } ],
      "partialFingerprints" : {
        "primaryLocationLineHash" : "f498e96868eb1ae7:1",
        "primaryLocationStartColumnFingerprint" : "0"
      }
    } ],
    "columnKind" : "utf16CodeUnits",
    "properties" : {
      "semmle.formatSpecifier" : "sarif-latest"
    }
  } ]
}