diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9c29fac..b21afeea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,37 +95,9 @@ jobs: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:integration name: Run integration tests - system: - needs: prepare - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 - id: cache-modules - with: - path: node_modules - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - restore-keys: ${{ runner.os }}-modules- - - uses: actions/cache@v3 - id: cache-vscode-test - with: - path: .vscode-test/vscode-* - key: ${{ runner.os }}-vscode-test - - uses: actions/cache@v3 - id: cache-out - with: - path: out - key: ${{ github.sha }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: yarn - - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:system - name: Run system tests - release: runs-on: ubuntu-latest - needs: [unit, integration, system] + needs: [unit, integration] if: github.ref == 'refs/heads/master' steps: - uses: actions/checkout@v3 diff --git a/package.json b/package.json index fbfc740c..2c8125cd 100644 --- a/package.json +++ b/package.json @@ -108,16 +108,13 @@ }, { "command": "appmap.openInstallGuide", - "title": "AppMap: Open Install Guide" + "title": "AppMap: Open Data Recording Guide", + "icon": "$(book)" }, { "command": "appmap.view.focusAppMap", "title": "AppMap: Open AppMaps View" }, - { - "command": "appmap.view.focusInstructions", - "title": "AppMap: Open Instructions" - }, { "command": "appmap.startRemoteRecording", "title": "AppMap: Start a Remote Recording", @@ -319,11 +316,6 @@ ] }, "viewsWelcome": [ - { - "view": "appmap.views.instructions", - "contents": "AppMap is initializing...", - "when": "!appmap.initialized" - }, { "view": "appmap.views.navie", "contents": "AppMap is initializing...", @@ -380,12 +372,6 @@ "visibility": "visible", "when": "!appMap.showSignIn" }, - { - "id": "appmap.views.instructions", - "name": "AppMap Recording Instructions", - "visibility": "collapsed", - "when": "!appMap.showSignIn" - }, { "id": "appmap.views.appmaps", "name": "AppMap Data", @@ -442,6 +428,11 @@ } ], "view/title": [ + { + "command": "appmap.openInstallGuide", + "when": "view == appmap.views.appmaps", + "group": "navigation@3" + }, { "command": "appmap.applyFilter", "when": "view == appmap.views.appmaps && appmap.hasData", @@ -508,7 +499,6 @@ "test:precache": "node -e 'require(\"@vscode/test-electron\").downloadAndUnzipVSCode(process.env.VSCODE_INSIDERS_VERSION)'", "test:integration": "ts-node ./test/integrationTest.ts", "test:web-client": "mocha web/test/*.test.mjs", - "test:system": "ts-node test/systemTest.ts", "test:unit": "mocha test/unit/**/*.test.[tj]s", "test:unit:some": "mocha", "test": "yarn test:unit && yarn test:web-client && yarn test:integration && yarn test:system", @@ -588,7 +578,7 @@ "dependencies": { "@appland/appmap": "^3.129.0", "@appland/client": "^1.14.1", - "@appland/components": "^4.30.0", + "@appland/components": "^4.31.0", "@appland/diagrams": "^1.8.0", "@appland/models": "^2.10.2", "@appland/rpc": "^1.7.0", diff --git a/src/analyzers/deps.ts b/src/analyzers/deps.ts deleted file mode 100644 index 1c2e6aeb..00000000 --- a/src/analyzers/deps.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Uri, workspace, WorkspaceFolder } from 'vscode'; -import utfDecoder from '../utfDecoder'; - -const fs = workspace.fs; - -// utility functions for analyzers - -export type DependencyFinder = ((name: string) => boolean) & { filename?: string }; - -function wordScanner(file: Uint8Array): DependencyFinder { - const text = utfDecoder(file); - return (name) => { - return new RegExp(`(\\W|^)${name}(\\W|$)`, 'mi').test(text); - }; -} - -export function fileWordScanner(filename: string) { - return (folder: WorkspaceFolder): PromiseLike => - fs.readFile(Uri.joinPath(folder.uri, filename)).then((file) => { - const scanner = wordScanner(file); - scanner.filename = filename; - return scanner; - }); -} diff --git a/src/analyzers/index.ts b/src/analyzers/index.ts deleted file mode 100644 index a3ec5a96..00000000 --- a/src/analyzers/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { WorkspaceFolder } from 'vscode'; -import LanguageResolver from '../services/languageResolver'; -import { systemNodeVersion, nvmNodeVersion } from '../services/command'; -import assert from 'assert'; - -import JavaAnalyzer from './java'; -import JavascriptAnalyzer from './javascript'; -import PythonAnalyzer from './python'; -import RubyAnalyzer from './ruby'; -import UnknownAnalyzer from './unknown'; - -const analyzers = { - java: JavaAnalyzer, - javascript: JavascriptAnalyzer, - python: PythonAnalyzer, - ruby: RubyAnalyzer, - unknown: UnknownAnalyzer, -}; - -export type Score = 'unsupported' | 'early-access' | 'ga'; -export const SCORE_VALUES: Record = { unsupported: 0, 'early-access': 1, ga: 2 }; -export const OVERALL_SCORE_VALUES: Record = { - unsupported: 1, - 'early-access': 2, - ga: 3, -}; - -export function scoreValue(...scores: Score[]): number { - return scores.reduce((s, x) => s + SCORE_VALUES[x], 0); -} - -export function overallScore(result: Features): number { - const languageScore = result.lang.score; - - if (languageScore === 'unsupported') return OVERALL_SCORE_VALUES['unsupported']; - - // standard scoring - return scoreValue(...Object.entries(result).map(([, t]) => t.score)); -} - -export type Feature = { - title?: string; - score: Score; - text: string; -}; - -export type Features = { - lang: Feature & { depFile?: string; plugin?: string; pluginType?: string }; - web?: Feature; - test?: Feature; -}; - -export type AppMapSummary = { - path: string; - name?: string; - requests?: number; - sqlQueries?: number; - functions?: number; -}; - -export type ProjectAnalysis = { - features: Features; - score: number; - name: string; - path?: string; - nodeVersion?: NodeVersion; -}; - -export type WithAppMaps = { - appMaps: Readonly>; - numHttpRequests: number; - numAppMaps: number; -}; - -export type NodeVersion = { - major: number; - minor: number; - patch: number; -}; - -export async function analyze( - folder: WorkspaceFolder -): Promise<(ProjectAnalysis & Partial)[]> { - const languages = await LanguageResolver.getLanguages(folder); - const results: (ProjectAnalysis & Partial)[] = []; - for (let i = 0; i < languages.length; i++) { - const language = languages[i]; - const analyzer = analyzers[language]; - if (analyzer) { - const result = await analyzer(folder); - assert(result); - - result.nodeVersion = await getNodeVersion(folder); - result.path = folder.uri.fsPath; - results[i] = result; - } - } - return results.filter(Boolean); -} - -async function getNodeVersion(folder: WorkspaceFolder): Promise { - const nvmVersion = await nvmNodeVersion(folder); - if (nvmVersion) { - return parseNodeVersion(nvmVersion); - } - - const systemVersion = await systemNodeVersion(); - if (systemVersion instanceof Error) { - return undefined; - } - - return parseNodeVersion(systemVersion); -} - -function parseNodeVersion(versionString: string): NodeVersion | undefined { - const digits = versionString - .replace(/[^0-9.]/g, '') - .split('.') - .map(Number); - - while (digits.length < 3) digits.push(0); - - return { - major: digits[0], - minor: digits[1], - patch: digits[2], - }; -} diff --git a/src/analyzers/java.ts b/src/analyzers/java.ts deleted file mode 100644 index 6c052de4..00000000 --- a/src/analyzers/java.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { WorkspaceFolder } from 'vscode'; -import { Features, ProjectAnalysis, overallScore } from '.'; -import { fileWordScanner } from './deps'; - -export default async function analyze(folder: WorkspaceFolder): Promise { - const features: Features = { - lang: { - title: 'Java', - score: 'ga', - text: "This project uses Java. It's one of the languages supported by AppMap.", - }, - }; - - try { - const dependency = await Promise.any( - ['pom.xml', 'build.gradle'].map((f) => fileWordScanner(f)(folder)) - ); - features.lang.depFile = dependency.filename; - if (dependency.filename == 'pom.xml') { - features.lang.plugin = 'com.appland:appmap-maven-plugin'; - features.lang.pluginType = 'plugin'; - } else { - features.lang.plugin = 'com.appland.appmap'; - features.lang.pluginType = 'plugin'; - } - if (dependency('spring')) { - features.web = { - title: 'Spring', - score: 'ga', - text: 'This project uses Spring. AppMap can record the HTTP requests served by your app.', - }; - } - - for (const framework of ['JUnit', 'TestNG']) { - if (dependency(framework)) { - features.test = { - title: framework.toLowerCase(), - score: 'ga', - text: `This project uses ${framework}. AppMap can record your tests.`, - }; - break; - } - } - } catch (_) { - features.lang = { - title: 'Java', - score: 'early-access', - text: `This project uses Java. It's one of the languages supported by AppMap, but no supported dependency file was found.`, - }; - } - - return { - name: folder.name, - features: features, - score: overallScore(features), - }; -} diff --git a/src/analyzers/javascript.ts b/src/analyzers/javascript.ts deleted file mode 100644 index 3e41ddda..00000000 --- a/src/analyzers/javascript.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Uri, workspace, WorkspaceFolder } from 'vscode'; -import { Features, ProjectAnalysis, Feature, overallScore } from '.'; -import utfDecoder from '../utfDecoder'; -import semverIntersects from 'semver/ranges/intersects'; -import assert from 'assert'; -import { parseSyml } from '@yarnpkg/parsers'; - -const fs = workspace.fs; - -export default async function analyze(folder: WorkspaceFolder): Promise { - const features: Features = { - lang: { - title: 'JavaScript', - score: 'early-access', - text: `This project uses JavaScript. AppMap provides early access support for JavaScript, primarily for Node.js.`, - depFile: 'package.json', - plugin: '@appland/appmap-agent-js', - pluginType: 'package', - }, - }; - - let dependencies: Dependencies | undefined; - try { - dependencies = { - ...(await readPkg(folder.uri)), - ...(await readPkgLock(folder.uri)), - ...(await readYarnLock(folder.uri)), - }; - } catch (e) { - console.warn(e); - } - - if (dependencies) { - if (dependencies?.express) { - features.web = { - title: 'Express.js', - score: 'early-access', - text: 'This project uses Express. AppMap will automatically recognize web requests, SQL queries, and key framework functions during recording.', - }; - } - - const testFeature = - detectTest(dependencies, 'Mocha', '>= 8') || detectTest(dependencies, 'Jest', '>= 25'); - if (testFeature) features.test = testFeature; - } else { - features.lang = { - title: 'JavaScript', - score: 'early-access', - text: `This project uses JavaScript. You can add AppMap to this project by creating a package.json file.`, - }; - } - - return { - name: folder.name, - features: features, - score: overallScore(features), - }; -} - -type Dependencies = Record; - -async function readPkg(uri: Uri): Promise { - const pkg = await readJsonObject(Uri.joinPath(uri, 'package.json')); - - let result: Dependencies = {}; - - if ('dependencies' in pkg) { - assert(typeof pkg.dependencies === 'object'); - result = { ...pkg.dependencies }; - } - - if ('devDependencies' in pkg) { - assert(typeof pkg.devDependencies === 'object'); - result = { ...result, ...pkg.devDependencies }; - } - - return result; -} - -async function readPkgLock(uri: Uri): Promise { - try { - const pkgLock = await readJsonObject(Uri.joinPath(uri, 'package-lock.json')); - assert( - 'dependencies' in pkgLock && pkgLock.dependencies && typeof pkgLock.dependencies === 'object' - ); - const { dependencies } = pkgLock; - const versions = Object.entries(dependencies).map(([pkg, { version }]) => [pkg, version]); - return Object.fromEntries(versions); - } catch { - // failure reading this file is not fatal - return {}; - } -} - -async function readJsonObject(uri: Uri): Promise { - const file = await fs.readFile(uri); - const json = utfDecoder(file); - const pkg: unknown = JSON.parse(json); - assert(pkg && typeof pkg === 'object'); - return pkg; -} - -async function readYarnLock(uri: Uri): Promise { - try { - const file = await fs.readFile(Uri.joinPath(uri, 'yarn.lock')); - const packages = parseSyml(utfDecoder(file)); - assert(packages && typeof packages === 'object'); - const versions = Object.entries(packages).map(([pkg, { version }]) => [ - parseYarnPkgSpec(pkg).name, - version, - ]); - return Object.fromEntries(versions); - } catch { - // failure reading this file is not fatal - return {}; - } -} - -function detectTest( - dep: Dependencies | undefined, - testPackage: string, - constraint: string -): Feature | undefined { - const version = dep && dep[testPackage.toLowerCase()]; - if (!version) return undefined; - if (version === 'latest' || semverIntersects(constraint, version)) - return { - title: testPackage, - score: 'early-access', - text: `This project uses ${testPackage}. You can record AppMaps of your tests.`, - }; - else - return { - title: testPackage, - score: 'unsupported', - text: `This project uses an unsupported version of ${testPackage}. You need version ${constraint} to record AppMaps of your tests.`, - }; -} - -type YarnPkgSpec = { - name: string; - srcVer?: string; -}; - -export function parseYarnPkgSpec(spec: string): YarnPkgSpec { - const [name, srcVer] = spec.split(/(?!^)@/, 2); - return { name, srcVer }; -} diff --git a/src/analyzers/python.ts b/src/analyzers/python.ts deleted file mode 100644 index c5dc1037..00000000 --- a/src/analyzers/python.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode'; -import { ProjectAnalysis, Features, overallScore } from '.'; -import { fileWordScanner, DependencyFinder } from './deps'; -import utfDecoder from '../utfDecoder'; -const fs = workspace.fs; - -async function pipDependencies(folder: WorkspaceFolder): Promise { - const requirements = utfDecoder(await fs.readFile(Uri.joinPath(folder.uri, 'requirements.txt'))); - const finder = (name) => { - return new RegExp(`^${name}(\\W|$)`, 'mi').test(requirements); - }; - finder.filename = 'requirements.txt'; - return finder; -} - -const pipfileDependencies = fileWordScanner('Pipfile'); -const poetryDependencies = fileWordScanner('poetry.lock'); - -async function grepFiles(pattern: string, folder: WorkspaceFolder) { - async function grepFile(file: PromiseLike) { - const text = utfDecoder(await file); - if (text.search(pattern) < 0) return Promise.reject(); - return Promise.resolve(); - } - - const files = await workspace.findFiles(new RelativePattern(folder, '**/*.py')); - - return Promise.any(files.map(fs.readFile).map(grepFile)).then( - () => true, - () => false - ); -} - -export default async function analyze(folder: WorkspaceFolder): Promise { - const features: Features = { - lang: { - title: 'Python', - score: 'ga', - text: `This project uses Python. It's one of the languages supported by AppMap.`, - }, - }; - - try { - const dependency = await Promise.any([ - pipDependencies(folder), - pipfileDependencies(folder), - poetryDependencies(folder), - ]); - features.lang.depFile = dependency.filename; - if (dependency('django')) { - features.web = { - title: 'Django', - score: 'ga', - text: 'This project uses Django. AppMap can record the HTTP requests served by your app.', - }; - } else if (dependency('flask')) { - features.web = { - title: 'Flask', - score: 'ga', - text: 'This project uses Flask. AppMap can record the HTTP requests served by your app.', - }; - } - - if (dependency('pytest')) { - features.test = { - title: 'pytest', - score: 'ga', - text: 'This project uses pytest. AppMap can record your tests.', - }; - } else if (await grepFiles('unittest', folder)) { - features.test = { - score: 'ga', - title: 'unittest', - text: 'This project uses unittest. AppMap can record your tests.', - }; - } - } catch (e) { - console.warn(e); - } - - return { - name: folder.name, - features: features, - score: overallScore(features), - }; -} diff --git a/src/analyzers/ruby.ts b/src/analyzers/ruby.ts deleted file mode 100644 index f3f352b6..00000000 --- a/src/analyzers/ruby.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { WorkspaceFolder } from 'vscode'; -import { Features, ProjectAnalysis, overallScore } from '.'; -import { fileWordScanner } from './deps'; - -const scanGemfile = fileWordScanner('Gemfile'); - -export default async function analyze(folder: WorkspaceFolder): Promise { - const features: Features = { - lang: { - title: 'Ruby', - score: 'ga', - text: "This project uses Ruby. It's one of the languages supported by AppMap.", - }, - }; - - try { - const dependency = await scanGemfile(folder); - features.lang.depFile = dependency.filename; - if (dependency('rails')) { - features.web = { - title: 'Rails', - score: 'ga', - text: 'This project uses Rails. AppMap can record the HTTP requests served by your app.', - }; - } - - for (const framework of ['Minitest', 'RSpec']) { - if (dependency(framework.toLowerCase())) { - features.test = { - title: framework, - score: 'ga', - text: `This project uses ${framework}. AppMap can record your tests.`, - }; - break; - } - } - } catch (e) { - console.warn(e); - } - - return { - name: folder.name, - features: features, - score: overallScore(features), - }; -} diff --git a/src/analyzers/unknown.ts b/src/analyzers/unknown.ts deleted file mode 100644 index 96e61882..00000000 --- a/src/analyzers/unknown.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { WorkspaceFolder } from 'vscode'; -import { ProjectAnalysis } from '.'; - -export default function analyze(folder: WorkspaceFolder): ProjectAnalysis { - return { - features: { - lang: { - score: 'unsupported', - text: `AppMap works with Ruby, Java, Python and JavaScript. None of those languages were detected in this project.`, - }, - }, - name: folder.name, - score: 0, - }; -} diff --git a/src/appMapService.ts b/src/appMapService.ts index 429c4f82..3df4be2d 100644 --- a/src/appMapService.ts +++ b/src/appMapService.ts @@ -11,7 +11,6 @@ import { AppmapUptodateService } from './services/appmapUptodateService'; import Command from './services/command'; import { NodeProcessService } from './services/nodeProcessService'; import ProjectStateService from './services/projectStateService'; -import { RunConfigService } from './services/runConfigService'; import SignInManager from './services/signInManager'; import { SourceFileWatcher } from './services/sourceFileWatcher'; import { WorkspaceServices } from './services/workspaceServices'; @@ -55,7 +54,6 @@ export default interface AppMapService { appmapServerAuthenticationProvider: AppMapServerAuthenticationProvider; recommender: AppMapRecommenderService; configManager: AppmapConfigManager; - runConfigService: RunConfigService; commandRegistry: typeof CommandRegistry; dependenciesInstalled: Promise; } diff --git a/src/commands/installAgent.ts b/src/commands/installAgent.ts index a43b6c04..ce090b71 100644 --- a/src/commands/installAgent.ts +++ b/src/commands/installAgent.ts @@ -86,7 +86,6 @@ export default function installAgent(context: vscode.ExtensionContext): void { } catch (err) { const exception = err as Error; Telemetry.sendEvent(INSTALL_BUTTON_ERROR, { - rootDirectory: path, exception, defaultTerminals, }); diff --git a/src/commands/updateConfigs.ts b/src/commands/updateConfigs.ts deleted file mode 100644 index bbfecbbf..00000000 --- a/src/commands/updateConfigs.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as vscode from 'vscode'; - -import assert from 'assert'; - -import { RunConfigService, RunConfigServiceInstance } from '../services/runConfigService'; -import { WorkspaceServices } from '../services/workspaceServices'; -import chooseWorkspace from '../lib/chooseWorkspace'; - -const JAVA_EXTENSIONS_PACK_ID = 'vscjava.vscode-java-pack'; - -export default async function updateAppMapConfigs( - context: vscode.ExtensionContext, - runConfigService: RunConfigService, - workspaceServices: WorkspaceServices -): Promise { - async function getRunConfigInstance(): Promise { - const workspace = await chooseWorkspace(); - assert(workspace); - - const runConfigServiceInstance = workspaceServices.getServiceInstance( - runConfigService, - workspace - ); - assert(runConfigServiceInstance); - - return runConfigServiceInstance; - } - - const testCommand = vscode.commands.registerCommand('appmap.updateAppMapTestConfig', async () => { - try { - const runConfigServiceInstance = await getRunConfigInstance(); - const updated = await runConfigServiceInstance.updateTestConfig(); - if (updated) { - vscode.window.showInformationMessage('AppMap test configuration added.'); - } else { - const choice = await vscode.window.showErrorMessage( - 'Failed to add AppMap test configuration. Please install the Extension Pack for Java.', - 'Install', - 'Cancel' - ); - - if (choice === 'Install') { - vscode.commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [ - JAVA_EXTENSIONS_PACK_ID, - ]); - } - } - } catch (e) { - const err = e as Error; - vscode.window.showErrorMessage(`Error: ${err.message}`); - } - }); - - context.subscriptions.push(testCommand); - - const launchCommand = vscode.commands.registerCommand( - 'appmap.updateAppMapLaunchConfig', - async () => { - try { - const runConfigServiceInstance = await getRunConfigInstance(); - runConfigServiceInstance.updateLaunchConfig(); - } catch (e) { - const err = e as Error; - vscode.window.showErrorMessage(`Error: ${err.message}`); - } - } - ); - - context.subscriptions.push(launchCommand); -} diff --git a/src/extension.ts b/src/extension.ts index 78d755bd..fa77a474 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -47,8 +47,6 @@ import SignInViewProvider from './webviews/signInWebview'; import SignInManager from './services/signInManager'; import { AppmapConfigManager } from './services/appmapConfigManager'; import { findByName } from './commands/findByName'; -import { RunConfigService } from './services/runConfigService'; -import updateAppMapConfigs from './commands/updateConfigs'; import downloadLatestJavaJar from './commands/downloadLatestJavaJar'; import IndexJanitor from './lib/indexJanitor'; import { unregister as unregisterTerminal } from './commands/installer/terminals'; @@ -226,10 +224,6 @@ export async function activate(context: vscode.ExtensionContext): Promise 0); } static async collectAppMapDescriptor(uri: vscode.Uri): Promise { diff --git a/src/services/languageResolver.ts b/src/services/languageResolver.ts deleted file mode 100644 index 7266d45f..00000000 --- a/src/services/languageResolver.ts +++ /dev/null @@ -1,293 +0,0 @@ -import * as vscode from 'vscode'; -import { extname } from 'path'; -import backgroundJob from '../lib/backgroundJob'; -import GitProperties from '../telemetry/properties/versionControlGit'; -import { findFiles } from '../lib/findFiles'; - -export const SUPPORTED_LANGUAGES = ['ruby', 'python', 'java', 'javascript'] as const; - -export const UNKNOWN_LANGUAGE = 'unknown' as const; - -export type RecognizedLanguage = (typeof SUPPORTED_LANGUAGES)[number] | typeof UNKNOWN_LANGUAGE; - -type LanguageDefinition = { - id: string; - name: string; - extensions: string[]; -}; - -const LANGUAGES: LanguageDefinition[] = [ - { - id: 'clojure', - name: 'Clojure', - extensions: ['.clj', '.cljc', '.cljs', '.cljx', '.clojure', '.edn'], - }, - { - id: 'coffeescript', - name: 'CoffeeScript', - extensions: ['.coffee', '.cson', '.iced'], - }, - { - id: 'c', - name: 'C', - extensions: ['.c'], - }, - { - id: 'c++', - name: 'C++', - extensions: ['.c++', '.cc', '.cpp', '.cxx', '.h', '.h++', '.hh', '.hpp', '.hxx'], - }, - - { - id: 'csharp', - name: 'C#', - extensions: ['.cake', '.cs', '.csx'], - }, - { - id: 'd', - name: 'D', - extensions: ['.d', '.di'], - }, - { - id: 'dart', - name: 'Dart', - extensions: ['.dart'], - }, - { - id: 'erlang', - name: 'Erlang', - extensions: ['.erl', '.hrl', '.xrl', '.yrl', '.'], - }, - { - id: 'fsharp', - name: 'F#', - extensions: ['.fs', '.fsi', '.fsscript', '.fsx'], - }, - { - id: 'go', - name: 'Go', - extensions: ['.go'], - }, - { - id: 'groovy', - name: 'Groovy', - extensions: ['.gradle', '.groovy', '.gvy'], - }, - { - id: 'haskell', - name: 'Haskell', - extensions: ['.hs'], - }, - { - id: 'haxe', - name: 'Haxe', - extensions: ['.hx'], - }, - { - id: 'java', - name: 'Java', - extensions: ['.jav', '.java'], - }, - { - id: 'javascript', - name: 'JavaScript', - extensions: ['.cjs', '.es6', '.js', '.jsx', '.mjs', '.ts'], - }, - { - id: 'kotlin', - name: 'Kotlin', - extensions: ['.kt', '.kts'], - }, - { - id: 'lisp', - name: 'Lisp', - extensions: ['..asd', '..cl', '..el', '..fnl', '..lisp', '..lsp', '..ros'], - }, - { - id: 'lua', - name: 'Lua', - extensions: ['.lua'], - }, - { - id: 'objective-c', - name: 'Objective-C', - extensions: ['.m'], - }, - { - id: 'objective-cpp', - name: 'Objective-C++', - extensions: ['.mm'], - }, - { - id: 'ocaml', - name: 'OCaml', - extensions: ['.ml', '.mli'], - }, - { - id: 'perl', - name: 'Perl', - extensions: ['.pl', '.pm', '.pod', '.psgi', '.t'], - }, - { - id: 'php', - name: 'PHP', - extensions: ['.ctp', '.php', '.php4', '.php5', '.phtml'], - }, - { - id: 'python', - name: 'Python', - extensions: ['.cpy', '.gyp', '.gpyi', '.ipy', '.py', '.pyi', '.pyw', '.rpy'], - }, - { - id: 'reason', - name: 'Reason', - extensions: ['.re', '.rei'], - }, - { - id: 'rust', - name: 'Rust', - extensions: ['.rs'], - }, - { - id: 'ruby', - name: 'Ruby', - extensions: ['.erb', '.gemspec', '.podspec', '.rake', '.rb', '.rbi', '.rbx', '.rjs', '.ru'], - }, - { - id: 'scala', - name: 'Scala', - extensions: ['.sbt', '.sc', '.scala'], - }, - { - id: 'swift', - name: 'Swift', - extensions: ['.swift'], - }, - { - id: 'visualbasic', - name: 'Visual Basic', - extensions: ['.bas', '.brs', '.vb', '.vbs'], - }, -]; - -/** - * Reverse mapping of file extensions to language id - */ -const LANGUAGE_EXTENSIONS = LANGUAGES.reduce>((memo, lang) => { - const extensions = lang.extensions || []; - - extensions.forEach((ext) => { - memo[ext] = lang.id; - }); - - return memo; -}, {}); - -type LanguageStats = Record; - -const LANGUAGE_CACHE: Record = {}; -const LANGUAGE_CACHE_EXPIRY = 60 * 1000; -const LANGUAGE_DISTRIBUTION_CACHE: Record = {}; - -function folderPath(folder: vscode.WorkspaceFolder | string): string { - return typeof folder === 'string' ? folder : folder.uri.fsPath; -} - -/** - * Provides language identification APIs. - */ -export default class LanguageResolver { - static clearCache(): void { - Object.keys(LANGUAGE_DISTRIBUTION_CACHE).forEach( - (key) => delete LANGUAGE_DISTRIBUTION_CACHE[key] - ); - Object.keys(LANGUAGE_CACHE).forEach((key) => delete LANGUAGE_CACHE[key]); - } - - static async getLanguageDistribution( - folder: vscode.WorkspaceFolder | string - ): Promise { - const cachedValue = LANGUAGE_DISTRIBUTION_CACHE[folderPath(folder)]; - if (cachedValue) { - return cachedValue; - } - - const countByFileExtension: Record = {}; - const gitProperties = new GitProperties(); - await gitProperties.initialize(folderPath(folder)); - - const searchPattern = new vscode.RelativePattern(folderPath(folder), '**'); - - // VSCode will already respect the user's ignore list. We can supplement that with the .gitignore files. - await findFiles(searchPattern).then((files) => { - files - .filter((file) => !gitProperties.isIgnored(file.fsPath)) - .forEach((file) => { - const fileExtension = extname(file.fsPath); - if (fileExtension !== '' && LANGUAGE_EXTENSIONS[fileExtension]) { - countByFileExtension[fileExtension] = (countByFileExtension[fileExtension] || 0) + 1; - } - }); - }); - - const countByLanguage = Object.entries(countByFileExtension).reduce((memo, [ext, count]) => { - const language = LANGUAGE_EXTENSIONS[ext]; - if (language) memo[language] = (memo[language] || 0) + count; - return memo; - }, {} as Record); - - // Convert each count to a ratio - const totalFiles = Object.values(countByLanguage).reduce((a, b) => a + b, 0); - if (totalFiles > 0) { - Object.keys(countByLanguage).forEach((key) => { - countByLanguage[key] /= totalFiles; - }); - } - - LANGUAGE_DISTRIBUTION_CACHE[folderPath(folder)] = countByLanguage; - - return countByLanguage; - } - - /** - * Retrieve the language ids for a given directory. Each language returned will be a language - * that is registered in LANGUAGE_AGENTS). If the language is not supported, returns 'unknown'. - * The languages are sorted by frequency of occurrence. - */ - private static async identifyLanguages( - folder: vscode.WorkspaceFolder | string - ): Promise { - let languageStats = LANGUAGE_CACHE[folderPath(folder)]; - if (!languageStats) { - languageStats = await backgroundJob( - `appmap.languageDistribution.${folderPath(folder)}`, - LanguageResolver.getLanguageDistribution.bind(null, folder), - 250 - ); - - console.log( - `[language-resolver] Detected languages ${JSON.stringify( - languageStats - )} in project ${folderPath(folder)}` - ); - - LANGUAGE_CACHE[folderPath(folder)] = languageStats; - setTimeout(() => delete LANGUAGE_CACHE[folderPath(folder)], LANGUAGE_CACHE_EXPIRY); - } - - const sortedEntries = Object.entries(languageStats).sort((a, b) => b[1] - a[1]); - return sortedEntries.map(([language]) => language) as RecognizedLanguage[]; - } - - /** - * Recursively inspects a folder for known source code files and returns the best-guess SUPPORTED - * language for the full directory tree. Ignores files and directories that are Git-ignored. - * - * @returns unknown if the most used language is not supported - */ - public static async getLanguages( - folder: vscode.WorkspaceFolder | string - ): Promise { - return await this.identifyLanguages(folder); - } -} diff --git a/src/services/projectStateService.ts b/src/services/projectStateService.ts index 5cfa321b..4de6d068 100644 --- a/src/services/projectStateService.ts +++ b/src/services/projectStateService.ts @@ -5,7 +5,6 @@ import ExtensionState, { Keys } from '../configuration/extensionState'; import { FileChangeEmitter } from './fileChangeEmitter'; import FindingsIndex from './findingsIndex'; import { ResolvedFinding } from './resolvedFinding'; -import { analyze, NodeVersion, scoreValue } from '../analyzers'; import ProjectMetadata from '../workspace/projectMetadata'; import AppMapCollection from './appmapCollection'; import ChangeEventDebouncer from './changeEventDebouncer'; @@ -16,7 +15,6 @@ import AppMapLoader from './appmapLoader'; import { PROJECT_OPEN, Telemetry } from '../telemetry'; import { workspaceServices } from './workspaceServices'; import { AppmapConfigManager } from './appmapConfigManager'; -import { RunConfigService, RunConfigStatus } from './runConfigService'; export class ProjectStateServiceInstance implements WorkspaceServiceInstance { protected disposables: vscode.Disposable[] = []; @@ -30,8 +28,6 @@ export class ProjectStateServiceInstance implements WorkspaceServiceInstance { public onStateChange = this._onStateChange.event; - private SUPPORTED_NODE_VERSIONS = [14, 16, 18]; - constructor( public readonly folder: vscode.WorkspaceFolder, protected readonly extensionState: ExtensionState, @@ -69,10 +65,7 @@ export class ProjectStateServiceInstance implements WorkspaceServiceInstance { this.updateMetadata(); } }), - AnalysisManager.onAnalysisToggled(() => this.setFindingsIndex(AnalysisManager.findingsIndex)), - RunConfigService.onStatusChange((service) => { - if (service.folder === this.folder) this.setRunConfigStatus(service.status); - }) + AnalysisManager.onAnalysisToggled(() => this.setFindingsIndex(AnalysisManager.findingsIndex)) ); this.syncConfigurationState(); @@ -80,7 +73,7 @@ export class ProjectStateServiceInstance implements WorkspaceServiceInstance { } public async initialize(): Promise { - await Promise.all([this.analyzeProject(), this.onUpdateAppMaps()]); + await this.onUpdateAppMaps(); Telemetry.sendEvent(PROJECT_OPEN, { rootDirectory: this.folder.uri.fsPath, project: this.metadata, @@ -118,11 +111,6 @@ export class ProjectStateServiceInstance implements WorkspaceServiceInstance { return this._metadata as Readonly; } - // Returns true if the project is installable and the agent has yet to be configured. - get installable(): boolean { - return this.metadata.score !== undefined && this.metadata.score >= 2 && !this.isAgentConfigured; - } - get complete(): boolean { return Boolean( this.isAgentConfigured && this.hasRecordedAppMaps && this._metadata?.openedNavie @@ -172,84 +160,21 @@ export class ProjectStateServiceInstance implements WorkspaceServiceInstance { } } - private async analyzeProject(): Promise { - const analyses = await analyze(this.folder); - - this._metadata.hasNode = !!analyses.some((a) => this.hasNode(a.nodeVersion)); - - let preferred = analyses.find((a) => a.features.web); - if (!preferred) preferred = analyses[0]; - - this._metadata.languages = analyses - .map((a) => a.features.lang) - .filter(Boolean) - .map((lang) => ({ - name: lang.title, - score: scoreValue(lang.score), - text: lang.text, - })); - if (preferred) { - this._metadata.language = { - name: preferred.features.lang.title, - score: scoreValue(preferred.features.lang.score), - text: preferred.features.lang.text, - }; - this._metadata.score = preferred.score; - if (preferred.features.test) { - this._metadata.testFramework = { - name: preferred.features.test.title, - score: scoreValue(preferred.features.test.score), - text: preferred.features.test.text, - }; - } - - if (preferred.features.web) { - this._metadata.webFramework = { - name: preferred.features.web.title, - score: scoreValue(preferred.features.web.score), - text: preferred.features.web.text, - }; - } - } - - this._onStateChange.fire(this._metadata); - } - private countRoutes(appMaps: AppMapLoader[]): number { return appMaps.reduce((sum, { descriptor }) => sum + (descriptor.numRequests || 0), 0); } private updateMetadata(): void { - if (this._metadata.language?.name === 'Java') { - this._metadata.agentInstalled = - this.metadata.debugConfigurationStatus === RunConfigStatus.Success; - } else { - this._metadata.agentInstalled = this.isAgentConfigured; - } - + this._metadata.agentInstalled = this.isAgentConfigured; this._metadata.appMapsRecorded = this.hasRecordedAppMaps || false; this._metadata.openedNavie = this.hasOpenedNavie || false; this._metadata.appMapOpened = this.hasOpenedAppMap || false; - this._onStateChange.fire(this._metadata); } dispose(): void { this.disposables.forEach((d) => d.dispose()); } - - private hasNode(nodeVersion: NodeVersion | undefined): boolean { - return !!( - nodeVersion && - nodeVersion.major !== 0 && - this.SUPPORTED_NODE_VERSIONS.includes(nodeVersion.major) - ); - } - - private setRunConfigStatus(status: RunConfigStatus): void { - this._metadata.debugConfigurationStatus = status; - this.updateMetadata(); - } } export default class ProjectStateService implements WorkspaceService { diff --git a/src/services/runConfigService.ts b/src/services/runConfigService.ts deleted file mode 100644 index 5d540c48..00000000 --- a/src/services/runConfigService.ts +++ /dev/null @@ -1,264 +0,0 @@ -import * as vscode from 'vscode'; - -import assert from 'assert'; -import path from 'path'; - -import { WorkspaceService, WorkspaceServiceInstance } from './workspaceService'; -import ProjectStateService, { ProjectStateServiceInstance } from './projectStateService'; -import { WorkspaceServices } from './workspaceServices'; -import ExtensionState from '../configuration/extensionState'; -import { DEBUG_EXCEPTION, Telemetry } from '../telemetry'; -import ErrorCode from '../telemetry/definitions/errorCodes'; - -type JavaTestConfig = { - name?: string; - vmArgs?: Array; -}; - -export enum RunConfigStatus { - Pending, - Success, - Error, -} - -type VmArgs = { - vmArgs?: string | Array; -}; - -const vmArgContainsAppMap = (vmArg: string | Array, regex: RegExp): boolean => - typeof vmArg === 'string' ? regex.test(vmArg) : vmArg.some((arg) => regex.test(arg)); - -const configsContain = (regex: RegExp, configs?: Array): boolean => - Boolean(configs?.some(({ vmArgs }) => vmArgs && vmArgContainsAppMap(vmArgs, regex))); - -export class RunConfigServiceInstance implements WorkspaceServiceInstance { - private static JAVA_TEST_RUNNER_EXTENSION_ID = 'vscjava.vscode-java-test'; - private static APPMAP_JAR_REGEX = /appmap-?(.*?)?.jar$/; - private outputDirVmarg = '-Dappmap.output.directory=${command:appmap.getAppmapDir}'; - private testConfigName = 'Test with AppMap'; - private _status: RunConfigStatus; - protected disposables: vscode.Disposable[] = []; - - public get appmapLaunchConfig(): vscode.DebugConfiguration { - return { - type: 'java', - name: 'Run with AppMap', - request: 'launch', - mainClass: '', - vmArgs: `-javaagent:${this.javaJarPath}`, - }; - } - - public get appmapTestConfig(): JavaTestConfig { - return { - name: this.testConfigName, - vmArgs: [`-javaagent:${this.javaJarPath}`, this.outputDirVmarg], - }; - } - - public get hasPreviouslyUpdatedLaunchConfig(): boolean { - return this.extensionState.getUpdatedLaunchConfig(this.folder); - } - - public get hasPreviouslyUpdatedTestConfig(): boolean { - return this.extensionState.getUpdatedTestConfig(this.folder); - } - - private get javaJarPath(): string { - return path.join('${userHome}', '.appmap', 'lib', 'java', 'appmap.jar'); - } - - constructor( - public folder: vscode.WorkspaceFolder, - public projectStateServiceInstance: ProjectStateServiceInstance, - private extensionState: ExtensionState, - private readonly _onStatusChange: vscode.EventEmitter - ) { - this.disposables.push( - vscode.extensions.onDidChange(this.updateConfigs.bind(this)), - vscode.workspace.onDidChangeConfiguration((e) => { - if ( - e.affectsConfiguration('java.test.config') || - e.affectsConfiguration('launch.configurations') - ) { - this.updateStatus(); - } - }) - ); - - this._status = RunConfigStatus.Pending; - this.updateConfigs(); - } - - private async updateStatus(): Promise { - this.status = (await this.hasConfigs()) ? RunConfigStatus.Success : RunConfigStatus.Error; - } - - public async updateConfigs(): Promise { - if (!this.isJavaProject()) return; - if (!this.hasPreviouslyUpdatedLaunchConfig) await this.updateLaunchConfig(); - if (this.hasPreviouslyUpdatedTestConfig) { - await this.updateExistingTestConfig(); - } else { - await this.updateTestConfig(); - } - - this.updateStatus(); - } - - public async updateLaunchConfig(): Promise { - try { - await this.updateConfig('launch', 'configurations', this.appmapLaunchConfig); - this.extensionState.setUpdatedLaunchConfig(this.folder, true); - } catch (e) { - this.sendConfigUpdateError(e); - } - } - - public async updateTestConfig(): Promise { - if (this.hasJavaTestExtension()) { - try { - await this.updateConfig('java.test', 'config', this.appmapTestConfig); - this.extensionState.setUpdatedTestConfig(this.folder, true); - return true; - } catch (e) { - this.sendConfigUpdateError(e); - return false; - } - } - return false; - } - - private async updateExistingTestConfig(): Promise { - if (this.hasJavaTestExtension()) { - try { - const parentConfig = vscode.workspace.getConfiguration('java.test'); - const configs = parentConfig.get>('config'); - if (!configs || !Array.isArray(configs)) return; - - configs.forEach((config) => { - if ( - config.name === this.testConfigName && - config.vmArgs && - !config.vmArgs.some((vmArg) => vmArg.includes('appmap.output.directory')) - ) - config.vmArgs.push(this.outputDirVmarg); - }); - - await parentConfig.update('config', configs); - } catch (e) { - this.sendConfigUpdateError(e); - } - } - } - - private async updateConfig( - parentSection: string, - section: string, - appmapConfig: vscode.DebugConfiguration | JavaTestConfig - ): Promise { - const parentConfig = vscode.workspace.getConfiguration(parentSection); - let configs = parentConfig.get>(section); - if (!Array.isArray(configs)) configs = []; - - configs.push(appmapConfig); - await parentConfig.update(section, configs); - } - - private isJavaProject(): boolean { - const { language } = this.projectStateServiceInstance.metadata; - return !!(language && language.name && language.name === 'Java'); - } - - public hasJavaTestExtension(): boolean { - const extensions = vscode.extensions.all; - return extensions.some( - (exension) => exension.id === RunConfigServiceInstance.JAVA_TEST_RUNNER_EXTENSION_ID - ); - } - - private sendConfigUpdateError(e: unknown): void { - this.status = RunConfigStatus.Error; - const err = e as Error; - Telemetry.sendEvent(DEBUG_EXCEPTION, { - exception: err, - errorCode: ErrorCode.ConfigUpdateError, - }); - } - - private set status(status: RunConfigStatus) { - if (this.status === status) return; - - this._status = status; - this._onStatusChange.fire(this); - } - - public get status(): RunConfigStatus { - return this._status; - } - - public hasLaunchConfig(): boolean { - const launchConfigs = vscode.workspace - .getConfiguration('launch') - .get('configurations', []); - return ( - Array.isArray(launchConfigs) && - configsContain(RunConfigServiceInstance.APPMAP_JAR_REGEX, launchConfigs) - ); - } - - public hasTestConfig(): boolean { - const testConfigs = vscode.workspace.getConfiguration('java.test').get('config', []); - return ( - Array.isArray(testConfigs) && - configsContain(RunConfigServiceInstance.APPMAP_JAR_REGEX, testConfigs) - ); - } - - public async addMissingConfigs(): Promise { - if (!this.isJavaProject()) return; - if (!this.hasLaunchConfig()) await this.updateLaunchConfig(); - if (!this.hasTestConfig()) await this.updateTestConfig(); - - await this.updateStatus(); - } - - public hasConfigs(): boolean | undefined { - return this.hasLaunchConfig() && this.hasTestConfig(); - } - - public async dispose(): Promise { - this.disposables.forEach((disposable) => disposable.dispose()); - } -} - -export class RunConfigService implements WorkspaceService { - private static _onStatusChange = new vscode.EventEmitter(); - public static readonly onStatusChange = RunConfigService._onStatusChange.event; - public static readonly serviceId = 'RunConfigService'; - - constructor( - private projectStateService: ProjectStateService, - private workspaceServices: WorkspaceServices, - private extensionState: ExtensionState - ) {} - - async create(folder: vscode.WorkspaceFolder): Promise { - const projectStateServiceInstance = this.workspaceServices.getServiceInstance( - this.projectStateService, - folder - ); - assert(projectStateServiceInstance); - - return new RunConfigServiceInstance( - folder, - projectStateServiceInstance, - this.extensionState, - RunConfigService._onStatusChange - ); - } - - static dispose(): void { - this._onStatusChange.dispose(); - } -} diff --git a/src/telemetry/definitions/events.ts b/src/telemetry/definitions/events.ts index 58310666..b417ea56 100644 --- a/src/telemetry/definitions/events.ts +++ b/src/telemetry/definitions/events.ts @@ -16,11 +16,6 @@ export const PROJECT_OPEN = new Event({ properties: [ Properties.AGENT_CONFIG_PRESENT, Properties.SCANNER_CONFIG_PRESENT, - Properties.PROJECT_LANGUAGE, - Properties.PROJECT_LANGUAGE_DISTRIBUTION, - Properties.WEB_FRAMEWORK, - Properties.TEST_FRAMEWORK, - Properties.IS_INSTALLABLE, Properties.HAS_DEVCONTAINER, Properties.DEPENDENCIES, Properties.PROJECT_PATH, @@ -33,9 +28,5 @@ export const PROJECT_OPEN = new Event({ export const INSTALL_BUTTON_ERROR = new Event({ name: 'install-button:error', - properties: [ - Properties.PROJECT_LANGUAGE, - Properties.DEBUG_EXCEPTION, - Properties.DEFAULT_TERMINALS, - ], + properties: [Properties.DEBUG_EXCEPTION, Properties.DEFAULT_TERMINALS], }); diff --git a/src/telemetry/definitions/properties.ts b/src/telemetry/definitions/properties.ts index 5ee3c058..09ee9d7d 100644 --- a/src/telemetry/definitions/properties.ts +++ b/src/telemetry/definitions/properties.ts @@ -1,16 +1,14 @@ import { PathLike, promises as fs } from 'fs'; import * as path from 'path'; import TelemetryDataProvider from '../telemetryDataProvider'; -import LanguageResolver, { UNKNOWN_LANGUAGE } from '../../services/languageResolver'; import { fileExists } from '../../util'; import ErrorCode from './errorCodes'; -import ProjectMetadata, { isLanguageSupported } from '../../workspace/projectMetadata'; +import ProjectMetadata from '../../workspace/projectMetadata'; import { TerminalConfig } from '../../commands/installAgent'; import * as vscode from 'vscode'; import { findRepository } from '../../lib/git'; import { workspaceServices } from '../../services/workspaceServices'; import { AppmapConfigManager } from '../../services/appmapConfigManager'; -import ProjectStateService from '../../services/projectStateService'; import { proxySettings } from '../../lib/proxySettings'; export const DEBUG_EXCEPTION = new TelemetryDataProvider({ @@ -73,73 +71,6 @@ export const PROXY_SETTINGS = new TelemetryDataProvider({ }, }); -export const PROJECT_LANGUAGE = new TelemetryDataProvider({ - id: 'appmap.project.language', - cache: false, - value(data: { - rootDirectory?: string; - metadata?: Record; - project?: ProjectMetadata; - }) { - // If metadata is available, use the language property. - // TODO: what is this record string,unknown? - if (data.metadata) { - const language = data.metadata.language as Record | undefined; - if (language?.name) { - return language.name; - } - } - - if (data.project?.language?.name) { - return data.project.language.name.toLowerCase(); - } - - // If no root directory is specified, we cannot resolve a langauge, so exit early. - if (!data.rootDirectory) { - return UNKNOWN_LANGUAGE; - } - - const workspaceFolder = vscode.workspace.getWorkspaceFolder( - vscode.Uri.file(data.rootDirectory) - ); - if (!workspaceFolder) { - return UNKNOWN_LANGUAGE; - } - const projectStateService = workspaceServices().getServiceInstanceFromClass( - ProjectStateService, - workspaceFolder - ); - if (!projectStateService) { - return UNKNOWN_LANGUAGE; - } - - return projectStateService.metadata?.language?.name?.toLowerCase() || UNKNOWN_LANGUAGE; - }, -}); - -export const WEB_FRAMEWORK = new TelemetryDataProvider({ - id: 'appmap.project.web_framework', - async value({ project }: { project: ProjectMetadata }) { - return project?.webFramework?.name; - }, -}); - -export const TEST_FRAMEWORK = new TelemetryDataProvider({ - id: 'appmap.project.test_framework', - async value({ project }: { project: ProjectMetadata }) { - return project?.testFramework?.name; - }, -}); - -export const PROJECT_LANGUAGE_DISTRIBUTION = new TelemetryDataProvider({ - id: 'appmap.project.language_distribution', - cache: true, - async value({ rootDirectory }: { rootDirectory: string }) { - const languageDistribution = await LanguageResolver.getLanguageDistribution(rootDirectory); - return JSON.stringify(languageDistribution); - }, -}); - export const PROJECT_PATH = new TelemetryDataProvider<{ rootDirectory: string }, string>({ id: 'appmap.project.path', cache: false, @@ -154,13 +85,6 @@ export const VERSION_CONTROL_REPOSITORY = new TelemetryDataProvider({ }, }); -export const IS_INSTALLABLE = new TelemetryDataProvider({ - id: 'appmap.project.installable', - async value({ project }: { project: ProjectMetadata }) { - return String(isLanguageSupported(project)); - }, -}); - export const DEFAULT_TERMINALS = new TelemetryDataProvider({ id: 'vscode.workspace.default_terminals', cache: true, diff --git a/src/tree/index.ts b/src/tree/index.ts index 0ce7412a..c19b4dbe 100644 --- a/src/tree/index.ts +++ b/src/tree/index.ts @@ -1,10 +1,8 @@ import * as vscode from 'vscode'; import AppMapCollectionFile from '../services/appmapCollectionFile'; import Links from './links'; -import { DocsPages, InstructionsTreeDataProvider } from './instructionsTreeDataProvider'; import { AppMapTreeDataProvider } from './appMapTreeDataProvider'; import { LinkTreeDataProvider } from './linkTreeDataProvider'; -import { ProjectStateServiceInstance } from '../services/projectStateService'; import { AppmapUptodateService } from '../services/appmapUptodateService'; import { AppMapTreeDataProviders } from '../appMapService'; import { FindingsTreeDataProvider } from './findingsTreeDataProvider'; @@ -14,17 +12,11 @@ import ClassMapIndex from '../services/classMapIndex'; export default function registerTrees( context: vscode.ExtensionContext, appmapCollection: AppMapCollectionFile, - projectStates: ProjectStateServiceInstance[], classMapIndex: ClassMapIndex, appmapsUptodate?: AppmapUptodateService ): AppMapTreeDataProviders { LinkTreeDataProvider.registerCommands(context); - const instructionsTreeProvider = new InstructionsTreeDataProvider(context, projectStates); - const instructionsTree = vscode.window.createTreeView('appmap.views.instructions', { - treeDataProvider: instructionsTreeProvider, - }); - const localAppMapsProvider = new AppMapTreeDataProvider(appmapCollection, appmapsUptodate); const localAppMapsTree = vscode.window.createTreeView('appmap.views.appmaps', { treeDataProvider: localAppMapsProvider, @@ -70,16 +62,6 @@ export default function registerTrees( }) ); - context.subscriptions.push( - vscode.commands.registerCommand('appmap.view.focusInstructions', (index = 0) => { - setTimeout(() => { - // TODO: (KEG) Here is where we would show the repo state to determine which step should be - // shown by default. - instructionsTree.reveal(DocsPages[index]); - }, 0); - }) - ); - context.subscriptions.push( vscode.commands.registerCommand('appmap.applyFilter', async () => { const filter = await vscode.window.showInputBox({ diff --git a/src/tree/instructionsTreeDataProvider.ts b/src/tree/instructionsTreeDataProvider.ts deleted file mode 100644 index 4579fea1..00000000 --- a/src/tree/instructionsTreeDataProvider.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import svgPending from '../../web/static/media/tree/pending.svg'; -import { ProjectStateServiceInstance } from '../services/projectStateService'; -import InstallGuideWebView from '../webviews/installGuideWebview'; -import AnalysisManager from '../services/analysisManager'; - -export const ProjectPicker = 'project-picker'; -export const RecordAppMaps = 'record-appmaps'; - -export const DocsPages = [ - { - id: ProjectPicker, - title: 'Add AppMap to your project', - completion: 'agentInstalled', - }, - { - id: RecordAppMaps, - title: 'Record AppMaps', - completion: 'appMapsRecorded', - }, - { - id: 'navie', - title: 'Ask AppMap Navie AI', - completion: 'openedNavie', - }, -] as const; - -type DocPage = (typeof DocsPages)[number]; -export type DocPageId = DocPage['id']; - -const icons = { - lock: new vscode.ThemeIcon('lock'), - unknown: new vscode.ThemeIcon('circle-large-outline'), - pending: path.join(__dirname, svgPending), - done: new vscode.ThemeIcon('pass-filled', new vscode.ThemeColor('terminal.ansiGreen')), -}; - -export class InstructionsTreeDataProvider implements vscode.TreeDataProvider { - public onDidChangeTreeData?: vscode.Event; - - private projectState?: ProjectStateServiceInstance; - private completion = new Map(); - - constructor( - context: vscode.ExtensionContext, - protected readonly projectStates: ProjectStateServiceInstance[] - ) { - if (projectStates.length === 1) { - [this.projectState] = projectStates; - // TreeDataProvider is not Disposable, so we can't dispose of this event emitter. - const emitter = new vscode.EventEmitter(); - context.subscriptions.push(emitter); - this.onDidChangeTreeData = emitter.event; - // TODO: Is this the same as adding the return value of this function to context.subscriptions? - this.projectState.onStateChange(() => emitter.fire(), null, context.subscriptions); - AnalysisManager.onAnalysisToggled(() => emitter.fire(), null, context.subscriptions); - } - } - - public getTreeItem({ title, id }: DocPage): vscode.TreeItem { - const item = new vscode.TreeItem(title); - item.command = { - title, - command: id === 'navie' ? 'appmap.explain' : InstallGuideWebView.command, - arguments: id === 'navie' ? [] : [id], - }; - item.iconPath = this.getIcon(id); - - return item; - } - - getIcon(id: DocPageId): string | vscode.ThemeIcon { - switch (this.completion.get(id)) { - case true: - return icons.done; - case false: - return icons.pending; - default: - return icons.unknown; - } - } - - public getChildren(): DocPage[] { - const metadata = this.projectState?.metadata; - - if (!metadata) this.completion.clear(); - else - this.completion = new Map(DocsPages.map(({ id, completion }) => [id, metadata[completion]])); - - return [...DocsPages]; - } -} diff --git a/src/webviews/chatSearchWebview.ts b/src/webviews/chatSearchWebview.ts index a448a757..926f7f63 100644 --- a/src/webviews/chatSearchWebview.ts +++ b/src/webviews/chatSearchWebview.ts @@ -3,7 +3,6 @@ import getWebviewContent from './getWebviewContent'; import appmapMessageHandler from './appmapMessageHandler'; import FilterStore, { SavedFilter } from './filterStore'; import WebviewList from './WebviewList'; -import { ProjectPicker, RecordAppMaps } from '../tree/instructionsTreeDataProvider'; import { getApiKey } from '../authentication'; import ExtensionSettings from '../configuration/extensionSettings'; import { CodeSelection } from '../commands/quickSearch'; @@ -132,11 +131,12 @@ export default class ChatSearchWebview { suggestion, }); break; - case 'open-record-instructions': - await vscode.commands.executeCommand('appmap.openInstallGuide', RecordAppMaps); + case 'open-new-chat': + void vscode.commands.executeCommand('appmap.explain'); break; + case 'open-record-instructions': case 'open-install-instructions': - await vscode.commands.executeCommand('appmap.openInstallGuide', ProjectPicker); + await vscode.commands.executeCommand('appmap.openInstallGuide'); break; case 'open-appmap': { const uri = vscode.Uri.file(message.path); diff --git a/src/webviews/installGuideWebview.ts b/src/webviews/installGuideWebview.ts index 6e18ef7b..03b0ba31 100644 --- a/src/webviews/installGuideWebview.ts +++ b/src/webviews/installGuideWebview.ts @@ -1,26 +1,9 @@ import * as vscode from 'vscode'; import { InstallAgent } from '../commands/installAgent'; import { ProjectStateServiceInstance } from '../services/projectStateService'; -import ProjectMetadata from '../workspace/projectMetadata'; -import { DocPageId, ProjectPicker, RecordAppMaps } from '../tree/instructionsTreeDataProvider'; import AnalysisManager from '../services/analysisManager'; import { AUTHN_PROVIDER_NAME } from '../authentication'; import getWebviewContent from './getWebviewContent'; -import { workspaceServices } from '../services/workspaceServices'; -import { RunConfigService, RunConfigServiceInstance } from '../services/runConfigService'; - -type PageMessage = { - page: string; - project?: ProjectMetadata; - projects?: ProjectMetadata[]; -}; - -function defaultPageId(projectStates: ProjectStateServiceInstance[]): DocPageId { - const anyInstalled = projectStates.some((project) => project.metadata.agentInstalled); - if (!anyInstalled) return ProjectPicker; - - return RecordAppMaps; -} export default class InstallGuideWebView { public static readonly viewType = 'appmap.views.installGuide'; @@ -32,161 +15,119 @@ export default class InstallGuideWebView { projectStates: ProjectStateServiceInstance[] ): void { context.subscriptions.push( - vscode.commands.registerCommand( - this.command, - async (page?: DocPageId, focusCommand?: string) => { - if (!page) page = defaultPageId(projectStates); - - // Short circuit if no project is open. The project picker has the correct prompts - // to handle this case. - if (!vscode.workspace.workspaceFolders?.length) page = ProjectPicker; - - // Attempt to re-use an existing webview for this project if one exists - if (this.existingPanel) { - this.existingPanel.reveal(vscode.ViewColumn.One); - this.existingPanel.webview.postMessage({ - type: 'page', - page, - focusCommand, - }); - return; - } - - const panel = vscode.window.createWebviewPanel( - this.viewType, - 'Using AppMap', - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - } - ); - - this.existingPanel = panel; - - panel.webview.html = getWebviewContent( - panel.webview, - context, - 'Using AppMap', - 'install-guide' - ); + vscode.commands.registerCommand(this.command, async (focusCommand?: string) => { + // Short circuit if no project is open. The project picker has the correct prompts + // to handle this case. + + // Attempt to re-use an existing webview for this project if one exists + if (this.existingPanel) { + this.existingPanel.reveal(vscode.ViewColumn.One); + this.existingPanel.webview.postMessage({ + type: 'page', + focusCommand, + }); + return; + } - const collectProjects = () => projectStates.map((project) => project.metadata); - const disposables = projectStates.map((projectState) => - projectState.onStateChange(() => { + const panel = vscode.window.createWebviewPanel( + this.viewType, + 'Recording AppMap data', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + } + ); + + this.existingPanel = panel; + + panel.webview.html = getWebviewContent( + panel.webview, + context, + 'Recording AppMap data', + 'install-guide' + ); + + const collectProjects = () => projectStates.map((project) => project.metadata); + const disposables = projectStates.map((projectState) => + projectState.onStateChange(() => { + panel.webview.postMessage({ + type: 'projects', + projects: collectProjects(), + }); + }) + ); + + disposables.push( + AnalysisManager.onAnalysisToggled((e) => { + panel.webview.postMessage({ + type: 'analysis-toggle', + ...e, + }); + }), + vscode.authentication.onDidChangeSessions(async (e) => { + if (e.provider.id !== AUTHN_PROVIDER_NAME) return; + panel.webview.postMessage({ + type: 'user-authenticated', + userAuthenticated: await AnalysisManager.isUserAuthenticated(), + }); + }) + ); + + // If the user closes the panel, make sure it's no longer cached + panel.onDidDispose(() => { + this.existingPanel = undefined; + disposables.forEach((disposable) => disposable.dispose()); + }); + + panel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case 'ready': { + const isUserAuthenticated = await AnalysisManager.isUserAuthenticated(); panel.webview.postMessage({ - type: 'projects', + type: 'init', projects: collectProjects(), + userAuthenticated: isUserAuthenticated, + debugConfigurationStatus: 1, }); - }) - ); - disposables.push( - AnalysisManager.onAnalysisToggled((e) => { - panel.webview.postMessage({ - type: 'analysis-toggle', - ...e, - }); - }), - vscode.authentication.onDidChangeSessions(async (e) => { - if (e.provider.id !== AUTHN_PROVIDER_NAME) return; - panel.webview.postMessage({ - type: 'user-authenticated', - userAuthenticated: await AnalysisManager.isUserAuthenticated(), - }); - }) - ); + break; + } - // If the user closes the panel, make sure it's no longer cached - panel.onDidDispose(() => { - this.existingPanel = undefined; - disposables.forEach((disposable) => disposable.dispose()); - }); + case 'clipboard': + break; - panel.webview.onDidReceiveMessage(async (message) => { - switch (message.command) { - case 'ready': { - const isUserAuthenticated = await AnalysisManager.isUserAuthenticated(); - panel.webview.postMessage({ - type: 'init', - projects: collectProjects(), - page, - userAuthenticated: isUserAuthenticated, - debugConfigurationStatus: 1, - }); - - break; + case 'view-problems': + { + vscode.commands.executeCommand('workbench.panel.markers.view.focus'); } + break; - case 'open-page': - { - const { page, project } = message as PageMessage; - if (page === 'open-appmaps') { - vscode.commands.executeCommand(focusCommand ?? 'appmap.view.focusCodeObjects'); - } else if (page === 'investigate-findings') { - if (!project) break; - - const workspaceFolder = vscode.workspace.getWorkspaceFolder( - vscode.Uri.parse(project.path) - ); - if (!workspaceFolder) break; - } - } - break; - - case 'clipboard': - break; - - case 'view-problems': - { - vscode.commands.executeCommand('workbench.panel.markers.view.focus'); - } - break; - - case 'perform-install': - { - const { path, language } = message as { path: string; language: string }; - vscode.commands.executeCommand(InstallAgent, path, language); - } - break; - - case 'add-java-configs': - { - const { projectPath } = message as { projectPath: string }; - const workspaceFolder = vscode.workspace.getWorkspaceFolder( - vscode.Uri.parse(projectPath) - ); - if (!workspaceFolder) break; - - const serviceInstance: RunConfigServiceInstance | undefined = - workspaceServices().getServiceInstanceFromClass( - RunConfigService, - workspaceFolder - ); - - serviceInstance?.addMissingConfigs(); - } - break; - - case 'open-navie': - vscode.commands.executeCommand('appmap.explain'); - break; - - case 'submit-to-navie': - { - const { suggestion } = message as { - suggestion: { label: string; prompt: string }; - }; - vscode.commands.executeCommand('appmap.explain', { suggestion }); - } - break; - - default: - break; - } - }); - } - ) + case 'perform-install': + { + const { path, language } = message as { path: string; language: string }; + vscode.commands.executeCommand(InstallAgent, path, language); + } + break; + + case 'open-navie': + vscode.commands.executeCommand('appmap.explain'); + break; + + case 'submit-to-navie': + { + const { suggestion } = message as { + suggestion: { label: string; prompt: string }; + }; + vscode.commands.executeCommand('appmap.explain', { suggestion }); + } + break; + + default: + break; + } + }); + }) ); } } diff --git a/src/workspace/projectMetadata.ts b/src/workspace/projectMetadata.ts index 49a9d0f2..fab80529 100644 --- a/src/workspace/projectMetadata.ts +++ b/src/workspace/projectMetadata.ts @@ -1,12 +1,6 @@ -import { SUPPORTED_LANGUAGES } from '../services/languageResolver'; -import { RunConfigStatus } from '../services/runConfigService'; -import Feature from './feature'; - export default interface ProjectMetadata { name: string; path: string; - score?: number; - hasNode?: boolean; agentInstalled?: boolean; appMapsRecorded?: boolean; openedNavie?: boolean; @@ -15,34 +9,4 @@ export default interface ProjectMetadata { numFindings?: number; numHttpRequests?: number; numAppMaps?: number; - // Most preferred language available. - language?: Feature; - // All recognized languages. - languages?: Feature[]; - // Test framework recognized in the preferred language, if any. - testFramework?: Feature; - // Web framework recognized in the preferred language, if any. - webFramework?: Feature; - debugConfigurationStatus?: RunConfigStatus; -} - -export function isLanguageSupported(project?: ProjectMetadata): boolean { - if (!project) return false; - - return !!project.languages?.some( - (language) => - language.name && - SUPPORTED_LANGUAGES.includes( - language.name.toLowerCase() as (typeof SUPPORTED_LANGUAGES)[number] - ) - ); -} - -export function hasSupportedFramework(project?: ProjectMetadata): boolean { - if (!project) return false; - - return !!( - (project.webFramework && project.webFramework.score > 0) || - (project.testFramework && project.testFramework.score > 0) - ); } diff --git a/test/integration/languageResolver.test.ts b/test/integration/languageResolver.test.ts deleted file mode 100644 index 2ee8b8d9..00000000 --- a/test/integration/languageResolver.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -// @project project-ruby -import assert from 'assert'; -import MockExtensionContext from '../mocks/mockExtensionContext'; -import { initializeWorkspace, ProjectRuby } from './util'; -import LanguageResolver from '../../src/services/languageResolver'; -import { promises as fs } from 'fs'; -import { join } from 'path'; - -const createOrReplace = async (f: () => Promise): Promise => { - try { - await f(); - } catch (e) { - const err = e as { code: string }; - if (err.code !== 'EEXIST') { - throw e; - } - } -}; - -describe('LanguageResolver', () => { - describe('getLanguage', () => { - let context: MockExtensionContext; - - beforeEach(() => { - context = new MockExtensionContext(); - initializeWorkspace(); - LanguageResolver.clearCache(); - }); - - afterEach(() => { - context.dispose(); - initializeWorkspace(); - LanguageResolver.clearCache(); - }); - - it('correctly identifies the project language', async () => { - const languages = await LanguageResolver.getLanguages(ProjectRuby); - const language = languages[0]; - assert.strictEqual(language, 'ruby'); - }); - - it('correctly identifies the project language with nested * gitignore filter', async () => { - // Add a child directory with a .gitignore file ignoring all files. - createOrReplace(async () => { - await fs.mkdir(join(ProjectRuby, 'ignored_dir')); - }); - - createOrReplace(async () => { - await fs.writeFile(join(ProjectRuby, 'ignored_dir', '.gitignore'), '*'); - }); - - const languages = await LanguageResolver.getLanguages(ProjectRuby); - const language = languages[0]; - assert.strictEqual(language, 'ruby'); - }); - }); -}); diff --git a/test/integration/projectAnalyzers/javascript.test.ts b/test/integration/projectAnalyzers/javascript.test.ts deleted file mode 100644 index 6bd45a3d..00000000 --- a/test/integration/projectAnalyzers/javascript.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import assert from 'assert'; -import jsAnalyzer from '../../../src/analyzers/javascript'; -import { withTmpDir } from '../util'; -import { promises as fs } from 'fs'; -import * as path from 'path'; -import * as vscode from 'vscode'; - -describe('JavaScript project analyzer', () => { - describe('dependency identification', () => { - it('identifies the latest version of mocha', async () => { - await withTmpDir(async (tmpDir) => { - const packageJson = { - devDependencies: { - mocha: 'latest', - }, - }; - - await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(packageJson), 'utf-8'); - - const workspaceFolder: vscode.WorkspaceFolder = { - name: path.basename(tmpDir), - uri: vscode.Uri.parse(tmpDir), - index: -1, - }; - const result = await jsAnalyzer(workspaceFolder); - - assert(result); - assert(result.features.test); - assert(result.features.test.score == 'early-access'); - }); - }); - }); -}); diff --git a/test/integration/runConfigs/runConfigsJava.test.ts b/test/integration/runConfigs/runConfigsJava.test.ts deleted file mode 100644 index 43a2cfb6..00000000 --- a/test/integration/runConfigs/runConfigsJava.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @project project-java - -import * as vscode from 'vscode'; - -import assert from 'assert'; -import { existsSync } from 'fs'; -import path from 'path'; -import { SinonSandbox, createSandbox, SinonStub } from 'sinon'; - -import { RunConfigServiceInstance } from '../../../src/services/runConfigService'; -import ExtensionState from '../../../src/configuration/extensionState'; -import { ExpectedLaunchConfig, ExpectedTestConfig, FakeConfig } from './util'; -import { ProjectJava, initializeWorkspace, waitFor, waitForExtension } from '../util'; - -describe('run config service in a Java Project', () => { - let sinon: SinonSandbox; - let runConfigServiceInstance: RunConfigServiceInstance; - let state: ExtensionState; - let getConfigStub: SinonStub; - - // NOTE: This is a before 'all' because each of the tests is designed to run in sequence in the same workspace instance. - before(async () => { - await initializeWorkspace(); - - sinon = createSandbox(); - - // This needs to be faked because the Test Runner for Java extension is not installed during testing - // and VS Code will throw an error when attempting to update an unregistered config ("java.test.config") - getConfigStub = sinon.stub(vscode.workspace, 'getConfiguration'); - getConfigStub.withArgs('java.test').returns(FakeConfig); - getConfigStub.callThrough(); - - const { workspaceServices, runConfigService, extensionState } = await waitForExtension(); - state = extensionState; - - const runConfigServiceInstances = workspaceServices.getServiceInstances(runConfigService); - assert.strictEqual(runConfigServiceInstances.length, 1); - runConfigServiceInstance = runConfigServiceInstances[0]; - - // Test Runner for Java extension is installed - sinon.stub(runConfigServiceInstance, 'hasJavaTestExtension').returns(true); - // Run config hasn't already been created - sinon.stub(runConfigServiceInstance, 'hasPreviouslyUpdatedLaunchConfig').returns(false); - - // This is tested in testConfigsJava.test.ts so we can ignore it here - sinon.stub(runConfigServiceInstance, 'updateTestConfig'); - }); - - after(async () => { - sinon.restore(); - await initializeWorkspace(); - }); - - it('correctly generates the expected configurations', () => { - assert.deepStrictEqual(runConfigServiceInstance.appmapLaunchConfig, ExpectedLaunchConfig); - assert.deepStrictEqual(runConfigServiceInstance.appmapTestConfig, ExpectedTestConfig); - }); - - it('creates a new run configuration', async () => { - await waitFor('launch.json to be created', () => - existsSync(path.join(ProjectJava, '.vscode', 'launch.json')) - ); - - let configs; - await waitFor('launch config to be created', () => { - const config = vscode.workspace.getConfiguration('launch'); - configs = config.get('configurations'); - return configs.length > 0; - }); - - assert.deepStrictEqual(configs, [runConfigServiceInstance.appmapLaunchConfig]); - }); - - it('saves that the launch config was created', () => { - assert(state.getUpdatedLaunchConfig(runConfigServiceInstance.folder)); - }); -}); diff --git a/test/integration/runConfigs/runConfigsJavaNoTestExt.test.ts b/test/integration/runConfigs/runConfigsJavaNoTestExt.test.ts deleted file mode 100644 index 2e5c5413..00000000 --- a/test/integration/runConfigs/runConfigsJavaNoTestExt.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @project project-java - -import * as vscode from 'vscode'; - -import assert from 'assert'; -import { existsSync } from 'fs'; -import path from 'path'; -import { SinonSandbox, createSandbox, SinonStub, SinonSpy } from 'sinon'; - -import { RunConfigServiceInstance } from '../../../src/services/runConfigService'; -import ExtensionState from '../../../src/configuration/extensionState'; -import { ExpectedLaunchConfig, ExpectedTestConfig, FakeConfig } from './util'; -import { ProjectJava, initializeWorkspace, waitFor, waitForExtension } from '../util'; - -describe('run config service in a Java Project without the Test Runner for Java extension', () => { - let sinon: SinonSandbox; - let runConfigServiceInstance: RunConfigServiceInstance; - let state: ExtensionState; - let fakeConfigUpdateSpy: SinonSpy; - let getConfigStub: SinonStub; - - // NOTE: This is a before 'all' because each of the tests is designed to run in sequence in the same workspace instance. - before(async () => { - await initializeWorkspace(); - - sinon = createSandbox(); - - fakeConfigUpdateSpy = sinon.spy(FakeConfig, 'update'); - getConfigStub = sinon.stub(vscode.workspace, 'getConfiguration'); - getConfigStub.withArgs('java.test').returns(FakeConfig); - getConfigStub.callThrough(); - - const { workspaceServices, runConfigService, extensionState } = await waitForExtension(); - state = extensionState; - - const runConfigServiceInstances = workspaceServices.getServiceInstances(runConfigService); - assert.strictEqual(runConfigServiceInstances.length, 1); - runConfigServiceInstance = runConfigServiceInstances[0]; - }); - - after(async () => { - sinon.restore(); - await initializeWorkspace(); - }); - - it('correctly generates the expected configurations', () => { - assert.deepStrictEqual(runConfigServiceInstance.appmapLaunchConfig, ExpectedLaunchConfig); - assert.deepStrictEqual(runConfigServiceInstance.appmapTestConfig, ExpectedTestConfig); - }); - - it('creates a new run configuration', async () => { - await waitFor('launch.json to be created', () => - existsSync(path.join(ProjectJava, '.vscode', 'launch.json')) - ); - - let configs; - await waitFor('launch config to be created', () => { - const config = vscode.workspace.getConfiguration('launch'); - configs = config.get('configurations'); - return configs.length > 0; - }); - - assert.deepStrictEqual(configs, [runConfigServiceInstance.appmapLaunchConfig]); - }); - - it('saves that the launch config was created', () => { - assert(state.getUpdatedLaunchConfig(runConfigServiceInstance.folder)); - }); - - it('does not create a new test configuration', () => { - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 0); - }); - - it('saves that the test config was not created', () => { - assert(!state.getUpdatedTestConfig(runConfigServiceInstance.folder)); - }); -}); diff --git a/test/integration/runConfigs/runConfigsRuby.test.ts b/test/integration/runConfigs/runConfigsRuby.test.ts deleted file mode 100644 index 6ff02c40..00000000 --- a/test/integration/runConfigs/runConfigsRuby.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as vscode from 'vscode'; - -import assert from 'assert'; -import { existsSync } from 'fs'; -import path from 'path'; -import { SinonSandbox, createSandbox, SinonStub, SinonSpy } from 'sinon'; - -import { RunConfigServiceInstance } from '../../../src/services/runConfigService'; -import ExtensionState from '../../../src/configuration/extensionState'; -import { FakeConfig } from './util'; -import { ProjectA, initializeWorkspace, waitForExtension } from '../util'; - -describe('run config service in a Ruby project', () => { - let sinon: SinonSandbox; - let runConfigServiceInstance: RunConfigServiceInstance; - let state: ExtensionState; - let fakeConfigGetSpy: SinonSpy; - let fakeConfigUpdateSpy: SinonSpy; - let getConfigStub: SinonStub; - - // NOTE: This is a before 'all' because each of the tests is designed to run in sequence in the same workspace instance. - before(async () => { - await initializeWorkspace(); - - sinon = createSandbox(); - - fakeConfigGetSpy = sinon.spy(FakeConfig, 'get'); - fakeConfigUpdateSpy = sinon.spy(FakeConfig, 'update'); - getConfigStub = sinon.stub(vscode.workspace, 'getConfiguration'); - getConfigStub.withArgs('java.test').returns(FakeConfig); - getConfigStub.withArgs('launch').returns(FakeConfig); - getConfigStub.callThrough(); - - const { workspaceServices, runConfigService, extensionState } = await waitForExtension(); - state = extensionState; - - const runConfigServiceInstances = workspaceServices.getServiceInstances(runConfigService); - assert.strictEqual(runConfigServiceInstances.length, 1); - runConfigServiceInstance = runConfigServiceInstances[0]; - }); - - after(async () => { - sinon.restore(); - await initializeWorkspace(); - }); - - it('does not create a new run configuration or test configuration', async () => { - assert(!existsSync(path.join(ProjectA, '.vscode', 'launch.json'))); - assert.deepStrictEqual(fakeConfigGetSpy.callCount, 0); - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 0); - }); - - it('saves that the launch config was not created', () => { - assert(!state.getUpdatedLaunchConfig(runConfigServiceInstance.folder)); - }); - - it('saves that the test config was not created', () => { - assert(!state.getUpdatedTestConfig(runConfigServiceInstance.folder)); - }); -}); diff --git a/test/integration/runConfigs/testConfigsJava.test.ts b/test/integration/runConfigs/testConfigsJava.test.ts deleted file mode 100644 index 835dde3d..00000000 --- a/test/integration/runConfigs/testConfigsJava.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -// @project project-java - -import * as vscode from 'vscode'; - -import assert from 'assert'; -import path from 'path'; -import { SinonSandbox, createSandbox, SinonStub, SinonSpy } from 'sinon'; - -import { RunConfigServiceInstance } from '../../../src/services/runConfigService'; -import ExtensionState from '../../../src/configuration/extensionState'; -import { ProjectJava, initializeWorkspace } from '../util'; -import { ProjectStateServiceInstance } from '../../../src/services/projectStateService'; - -class MockExtensionState { - updatedTestConfig = false; - getUpdatedTestConfig(): boolean { - return this.updatedTestConfig; - } - setUpdatedTestConfig(_workspaceFolder: vscode.WorkspaceFolder, value: boolean): void { - this.updatedTestConfig = value; - } - getUpdatedLaunchConfig(): boolean { - return true; - } -} - -class MockConfig { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data = [] as any; - get() { - return this.data; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - update(_section: string, value: any) { - this.data = value; - } -} - -const fakeProjectState = { - onStateChange() { - // not implemented - }, - metadata: { - language: { - name: 'Java', - }, - }, -} as unknown; - -describe('test configs in a Java Project', () => { - let sinon: SinonSandbox; - let runConfigServiceInstance: RunConfigServiceInstance; - let getConfigStub: SinonStub; - let extensionState: ExtensionState; - let fakeConfigGetSpy: SinonSpy; - let fakeConfigUpdateSpy: SinonSpy; - - const fakeConfig = new MockConfig(); - - beforeEach(async () => { - sinon = createSandbox(); - - fakeConfigGetSpy = sinon.spy(fakeConfig, 'get'); - fakeConfigUpdateSpy = sinon.spy(fakeConfig, 'update'); - - const workspaceFolder: vscode.WorkspaceFolder = { - name: path.basename(ProjectJava), - uri: vscode.Uri.file(ProjectJava), - index: -1, - }; - const mockExtensionState = new MockExtensionState() as unknown; - extensionState = mockExtensionState as ExtensionState; - runConfigServiceInstance = new RunConfigServiceInstance( - workspaceFolder, - fakeProjectState as ProjectStateServiceInstance, - extensionState, - new vscode.EventEmitter() - ); - - // Test Runner for Java extension is installed - sinon.stub(runConfigServiceInstance, 'hasJavaTestExtension').returns(true); - // Test config hasn't already been created - sinon.stub(runConfigServiceInstance, 'hasPreviouslyUpdatedTestConfig').returns(false); - - // This is tested in runConfigsJava.test.ts so we can ignore it here - sinon.stub(runConfigServiceInstance, 'updateLaunchConfig'); - - // This needs to be faked because the Test Runner for Java extension is not installed during testing - // and VS Code will throw an error when attempting to update an unregistered config ("java.test.config") - getConfigStub = sinon.stub(vscode.workspace, 'getConfiguration'); - getConfigStub.withArgs('java.test').returns(fakeConfig); - getConfigStub.callThrough(); - }); - - afterEach(async () => { - sinon.restore(); - await initializeWorkspace(); - fakeConfig.data = []; - }); - - it('correctly generates a test config', async () => { - await runConfigServiceInstance.updateConfigs(); - - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 1); - assert.deepStrictEqual(fakeConfigGetSpy.getCall(0).args[0], 'config'); - assert.deepStrictEqual(fakeConfigUpdateSpy.getCall(0).args, [ - 'config', - [runConfigServiceInstance.appmapTestConfig], - ]); - assert.deepStrictEqual(fakeConfig.get(), [runConfigServiceInstance.appmapTestConfig]); - }); - - it('correctly generates a test config when another test config is already present', async () => { - const preexistingTestConfig = { name: 'Test' }; - fakeConfig.data = [preexistingTestConfig]; - - await runConfigServiceInstance.updateConfigs(); - - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 1); - assert.deepStrictEqual(fakeConfigGetSpy.getCall(0).args[0], 'config'); - assert.deepStrictEqual(fakeConfigUpdateSpy.getCall(0).args, [ - 'config', - [preexistingTestConfig, runConfigServiceInstance.appmapTestConfig], - ]); - assert.deepStrictEqual(fakeConfig.get(), [ - preexistingTestConfig, - runConfigServiceInstance.appmapTestConfig, - ]); - }); - - it('does not add a test config if one was previously generated', async () => { - fakeConfig.data = [runConfigServiceInstance.appmapTestConfig]; - extensionState.setUpdatedTestConfig({} as vscode.WorkspaceFolder, true); - - await runConfigServiceInstance.updateConfigs(); - - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 1); - assert.deepStrictEqual(fakeConfigGetSpy.getCall(0).args[0], 'config'); - assert.deepStrictEqual(fakeConfigUpdateSpy.getCall(0).args, [ - 'config', - [runConfigServiceInstance.appmapTestConfig], - ]); - assert.deepStrictEqual(fakeConfig.get(), [runConfigServiceInstance.appmapTestConfig]); - }); - - it('updates a previously added test config if it does not have the output dir vmArg', async () => { - const incompleteAppmapConfig = { - name: 'Test with AppMap', - vmArgs: ['-javaagent:${userHome}/.appmap/lib/java/appmap.jar'], - }; - fakeConfig.data = [incompleteAppmapConfig]; - extensionState.setUpdatedTestConfig({} as vscode.WorkspaceFolder, true); - - await runConfigServiceInstance.updateConfigs(); - - assert.deepStrictEqual(fakeConfigUpdateSpy.callCount, 1); - assert.deepStrictEqual(fakeConfigGetSpy.getCall(0).args[0], 'config'); - assert.deepStrictEqual(fakeConfigUpdateSpy.getCall(0).args, [ - 'config', - [runConfigServiceInstance.appmapTestConfig], - ]); - assert.deepStrictEqual(fakeConfig.get(), [runConfigServiceInstance.appmapTestConfig]); - }); -}); diff --git a/test/integration/runConfigs/util.ts b/test/integration/runConfigs/util.ts deleted file mode 100644 index b49be7bd..00000000 --- a/test/integration/runConfigs/util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import path from 'path'; - -const expectedJarPath = path.join('${userHome}', '.appmap', 'lib', 'java', 'appmap.jar'); - -export const FakeConfig = { - get() { - return []; - }, - update() { - return; - }, -}; - -export const ExpectedLaunchConfig = { - type: 'java', - name: 'Run with AppMap', - request: 'launch', - mainClass: '', - vmArgs: `-javaagent:${expectedJarPath}`, -}; - -export const ExpectedTestConfig = { - name: 'Test with AppMap', - vmArgs: [ - `-javaagent:${expectedJarPath}`, - '-Dappmap.output.directory=${command:appmap.getAppmapDir}', - ], -}; diff --git a/test/system/tests/support/project.ts b/test/projectDirectory.ts similarity index 100% rename from test/system/tests/support/project.ts rename to test/projectDirectory.ts diff --git a/test/system/src/app.ts b/test/system/src/app.ts deleted file mode 100644 index 239a7907..00000000 --- a/test/system/src/app.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { BrowserContext, ElectronApplication, Page, _electron as Electron } from '@playwright/test'; -import { ConsoleReporter, downloadAndUnzipVSCode } from '@vscode/test-electron'; -import * as path from 'path'; - -async function getElectronPath(installDirectory: string): Promise { - const os = process.platform; - if (os === 'darwin') { - return path.join(installDirectory, 'Electron'); - } else if (os === 'linux') { - const product = await import(path.join(installDirectory, 'resources', 'app', 'product.json')); - return path.join(installDirectory, product.applicationName); - } else if (os === 'win32') { - const product = await import(path.join(installDirectory, 'resources', 'app', 'product.json')); - return path.join(installDirectory, `${product.nameShort}.exe`); - } else { - throw new Error(`Unsupported platform: ${os}`); - } -} - -export interface CodeHarness { - app: ElectronApplication; - page: Page; - context: BrowserContext; -} - -interface LaunchOptions { - workspacePath?: string; - verbose?: boolean; -} - -export async function downloadCode(): Promise { - return await downloadAndUnzipVSCode({ - reporter: new ConsoleReporter(false), - }); -} - -export async function launchCode( - codePath: string, - extensionDevelopmentPath: string, - userDataDir: string, - options: LaunchOptions -): Promise { - const baseCodePath = path.join(codePath, '..'); - const executablePath = await getElectronPath(baseCodePath); - const app = await Electron.launch({ - executablePath, - args: [ - '--no-cached-data', - '--disable-keytar', - '--disable-crash-reporter', - '--verbose', - '--no-sandbox', - '--disable-telemetry', - '--disable-extensions', - '--disable-updates', - '--disable-gpu', - '--skip-welcome', - '--skip-release-notes', - '--disable-workspace-trust', - '--enable-smoke-test-driver', - `--user-data-dir=${userDataDir}`, - `--extensionDevelopmentPath=${extensionDevelopmentPath}`, - options?.workspacePath || '', - ], - env: { - ...process.env, - APPMAP_TEST_API_KEY: 'Zm9vQGdtYWlsLmNvbTpmYzYwNTRiNi0xOTAzLTQ0MDEtOTJhNy0wMDAzNWFjOGI2MGMK', - APPMAP_SYSTEM_TEST: '1', - }, - }); - - const context = app.context(); - const page = await app.firstWindow(); - - if (options?.verbose) { - page.on('console', (msg) => console.log(`Electron [${msg.type()}]: ${msg.text()}`)); - page.on('pageerror', (msg) => console.log(`Electron [error] ${msg}`)); - page.on('crash', () => console.log('Electron [error] PAGE CRASH')); - page.on('close', () => console.log(`Electron [info] page has been closed`)); - } - - return { app, page, context }; -} diff --git a/test/system/src/appMap.ts b/test/system/src/appMap.ts deleted file mode 100644 index f517923d..00000000 --- a/test/system/src/appMap.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Locator, Page } from '@playwright/test'; -import FindingDetailsWebview from './findingDetailsWebview'; -import FindingsOverviewWebview from './findingsOverviewWebview'; -import InstructionsWebview from './instructionsWebview'; - -export enum InstructionStep { - InstallAppMapAgent, - RecordAppMaps, - NavieIntroduction, -} - -export enum InstructionStepStatus { - Pending, - Complete, - None, -} - -export default class AppMap { - constructor( - protected readonly page: Page, - protected readonly instructionsWebview: InstructionsWebview, - protected readonly findingsOverviewWebview: FindingsOverviewWebview, - protected readonly findingDetailsWebview: FindingDetailsWebview - ) {} - - get actionPanelButton(): Locator { - return this.page.locator('.action-item:has(a.action-label[aria-label="AppMap"])').first(); - } - - get instructionsTree(): Locator { - return this.page.locator('.pane:has(.title:text("AppMap Recording Instructions"))'); - } - - get findingsTree(): Locator { - return this.page.locator('.pane:has(.title:text("Runtime Analysis"))'); - } - - get appMapTree(): Locator { - return this.page.locator('.pane:has(.title:text("AppMap Data"))'); - } - - public instructionsTreeItem(step: InstructionStep): Locator { - return this.instructionsTree.locator('.pane-body >> [role="treeitem"]').nth(step); - } - - public findingsTreeItem(nth?: number): Locator { - return this.findingsTree.locator('.pane-body >> [role="treeitem"]').nth(nth || 0); - } - - public finding(nth?: number): Locator { - return this.findingsTree.locator('[role="treeitem"][aria-level="3"]').nth(nth || 0); - } - - public appMapTreeItem(): Locator { - return this.appMapTree.locator('.pane-body >> [role="treeitem"]:not([aria-expanded])').first(); - } - - public async expandFindings(): Promise { - if (await this.findingsTree.locator('.pane-body').isHidden()) { - await this.findingsTree.click(); - } - } - - public async expandInstructions(): Promise { - if (await this.instructionsTree.locator('.pane-body').isHidden()) { - await this.instructionsTree.click(); - } - } - - public async openActionPanel(): Promise { - await this.actionPanelButton.click(); - } - - public async ready(): Promise { - const welcomeView = this.page.locator( - '.split-view-view:has(.title:text("AppMap Data")) >> .welcome-view' - ); - await welcomeView.first().waitFor({ state: 'visible' }); - } - - public async openInstruction(step: InstructionStep): Promise { - await this.instructionsTreeItem(step).click(); - if (step !== InstructionStep.NavieIntroduction) await this.instructionsWebview.ready(); - } - - public async assertInstructionStepStatus( - step: InstructionStep, - status: InstructionStepStatus - ): Promise { - let selector = '.custom-view-tree-node-item-icon'; - switch (status) { - case InstructionStepStatus.Complete: - selector += '.codicon-pass-filled'; - break; - - case InstructionStepStatus.None: - selector += '.codicon-circle-large-outline'; - break; - - default: - selector += ':not(.codicon)'; - break; - } - - return await this.instructionsTreeItem(step).locator(selector).waitFor(); - } -} diff --git a/test/system/src/appMapWebview.ts b/test/system/src/appMapWebview.ts deleted file mode 100644 index 80c59d54..00000000 --- a/test/system/src/appMapWebview.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FrameLocator, Locator, Page } from '@playwright/test'; -import { waitFor } from '../../waitFor'; - -export default class AppMapWebview { - constructor(protected readonly page: Page) {} - - private frameSelector = 'iframe.webview.ready'; - private frame?: FrameLocator; - private initializeErrorMsg = 'No frame found. Call initialize() first'; - - public async initialize(expectedFrames: number): Promise { - const checkForIFrames = async () => { - const iframes = await this.page.locator(this.frameSelector).count(); - return iframes === expectedFrames; - }; - - await waitFor('waiting for second iframe', checkForIFrames.bind(this)); - // this assumes that the last iframe is the appmap - const outerFrame = this.page.frameLocator(this.frameSelector).last(); - await outerFrame.locator('iframe#active-frame').waitFor(); - this.frame = outerFrame.frameLocator('#active-frame'); - await this.frame.locator('#app').waitFor(); - } - - public async activeTab(): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - return await this.frame.locator('.tab-btn--active').innerText(); - } - - public get highlightedElement(): Locator { - if (!this.frame) throw Error(this.initializeErrorMsg); - - return this.frame.locator('.highlight'); - } - - public async traceFilterValue(): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - return await this.frame.locator('.trace-filter__input').inputValue(); - } -} diff --git a/test/system/src/driver.ts b/test/system/src/driver.ts deleted file mode 100644 index f71b6bf8..00000000 --- a/test/system/src/driver.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { BrowserContext, ElectronApplication, Locator, Page } from '@playwright/test'; -import { glob } from 'glob'; -import AppMap from './appMap'; -import AppMapWebview from './appMapWebview'; -import FindingDetailsWebview from './findingDetailsWebview'; -import FindingsOverviewWebview from './findingsOverviewWebview'; -import InstructionsWebview from './instructionsWebview'; -import Panel from './panel'; -import { getOsShortcut } from './util'; - -async function tryClick(elem: Locator, timeout = 5000) { - try { - await elem.click({ timeout }); - } catch (err) { - console.warn(`problem clicking ${elem}: ${err}`); - } -} - -export default class Driver { - public readonly instructionsWebview = new InstructionsWebview(this.page); - public readonly findingsOverviewWebview = new FindingsOverviewWebview(this.page); - public readonly findingDetailsWebview = new FindingDetailsWebview(this.page); - public readonly appMap = new AppMap( - this.page, - this.instructionsWebview, - this.findingsOverviewWebview, - this.findingDetailsWebview - ); - public readonly appMapWebview = new AppMapWebview(this.page); - public readonly panel = new Panel(this.page); - - constructor( - protected readonly app: ElectronApplication, - protected readonly context: BrowserContext, - protected readonly page: Page - ) {} - - public async runCommand(cmd: string, allowMissing?: boolean): Promise { - await this.page.press('body', getOsShortcut('Control+Shift+P')); - - const input = this.page.locator('.quick-input-box input'); - await input.type(cmd); - await input.press('Enter'); - - if (allowMissing) { - await this.page.keyboard.press('Escape'); - } - - await input.waitFor({ state: 'hidden' }); - } - - public async waitForReady(): Promise { - await tryClick( - this.page.locator('[id="status.notifications"] a[role="button"][aria-label="Notifications"]') - ); - await tryClick( - this.page.locator( - '.notifications-list-container .monaco-list-row:has(span:text("AppMap: Ready")) >> a[role="button"]:text("OK")' - ) - ); - } - - public async waitForFile(pattern: string): Promise { - const cwd = process.cwd(); - let retryCount = 0; - for (;;) { - if (retryCount > 30) { - throw new Error(`timed out waiting for ${pattern} in ${cwd}`); - } - - const fileFound = await new Promise((resolve) => - glob(pattern, (err, matches) => { - if (err) { - throw err; - } - resolve(matches.length > 0); - }) - ); - - if (fileFound) return; - - await new Promise((resolve) => setTimeout(resolve, 1000)); - retryCount += 1; - } - } - - public async closeAllEditorWindows(): Promise { - await this.runCommand('View: Close All Editors'); - } - - public async resetUsage(): Promise { - await this.runCommand('AppMap: Reset usage state'); - } - - public async closePanel(): Promise { - await this.runCommand('View: Close Panel', true); - } - - public async reload(): Promise { - await this.openActionView('Explorer'); - await this.closeAllEditorWindows(); - await this.runCommand('Developer: Reload Window'); - await this.waitForReady(); - } - - public async openActionView(name: string): Promise { - await this.page.locator(`.action-item a.action-label[aria-label~="${name}"]`).first().click(); - } - - public async tabCount(): Promise { - return await this.page.locator('.tabs-and-actions-container >> [role="tab"]').count(); - } - - public async closeFolder(): Promise { - await this.page.press('body', getOsShortcut('Control+K')); - await this.page.press('body', getOsShortcut('F')); - } -} diff --git a/test/system/src/findingDetailsWebview.ts b/test/system/src/findingDetailsWebview.ts deleted file mode 100644 index b31b873f..00000000 --- a/test/system/src/findingDetailsWebview.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { FrameLocator, Page } from '@playwright/test'; -import { strictEqual } from 'assert'; -import { waitFor } from '../../waitFor'; - -export default class FindingDetailsWebview { - constructor(protected readonly page: Page) {} - - private frameSelector = 'iframe.webview.ready'; - private frame?: FrameLocator; - private initializeErrorMsg = 'No frame found. Call initialize() first'; - - public async assertTitleRenders(expectedTitle: string): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - const title = this.frame.locator('[data-cy="title"]'); - strictEqual(await title.count(), 1, 'Expected one title element'); - strictEqual(await title.innerText(), expectedTitle); - } - - public async initialize(expectedFrames: number): Promise { - const checkForIFrames = async () => { - const iframes = await this.page.locator(this.frameSelector).count(); - return iframes === expectedFrames; - }; - - await waitFor('waiting for second iframe', checkForIFrames.bind(this)); - // this assumes that the last iframe is the finding details page - const outerFrame = this.page.frameLocator(this.frameSelector).last(); - await outerFrame.locator('iframe#active-frame').waitFor(); - this.frame = outerFrame.frameLocator('#active-frame'); - } - - public async clickNthAssociatedMap(nth: number): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - await this.frame - .locator('[data-cy="associated-map"]') - .nth(nth || 0) - .locator('a') - .click(); - } -} diff --git a/test/system/src/findingsOverviewWebview.ts b/test/system/src/findingsOverviewWebview.ts deleted file mode 100644 index 820e9671..00000000 --- a/test/system/src/findingsOverviewWebview.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { FrameLocator, Page } from '@playwright/test'; -import { strictEqual } from 'assert'; -import { waitFor } from '../../waitFor'; - -export default class FindingsOverviewWebview { - constructor(protected readonly page: Page) {} - - private title = 'Runtime Analysis'; - private frameSelector = 'iframe.webview.ready'; - private frame?: FrameLocator; - private initializeErrorMsg = 'No frame found. Call initialize() first'; - - public async assertNumberOfFindingsInOverview(expected: number): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - const count = await this.frame.locator('[data-cy="finding"]').count(); - strictEqual(count, expected, `Expected number of findings to be ${expected}`); - } - - public async assertTitleRenders(): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - const title = this.frame.locator('[data-cy="title"]'); - strictEqual(await title.count(), 1, 'Expected one title element'); - strictEqual(await title.innerText(), this.title); - } - - public async openFirstFindingDetail(): Promise { - if (!this.frame) throw Error(this.initializeErrorMsg); - - this.frame.locator('[data-cy="finding"]').first().locator('ul').click(); - } - - public async initialize(expectedFrames: number): Promise { - const checkForIFrames = async () => { - const iframes = await this.page.locator(this.frameSelector).count(); - return iframes === expectedFrames; - }; - - await waitFor('waiting for second iframe', checkForIFrames.bind(this)); - // this assumes that the last iframe is the findings page - const outerFrame = this.page.frameLocator(this.frameSelector).last(); - await outerFrame.locator('iframe#active-frame').waitFor(); - this.frame = outerFrame.frameLocator('#active-frame'); - } -} diff --git a/test/system/src/instructionsWebview.ts b/test/system/src/instructionsWebview.ts deleted file mode 100644 index d60de680..00000000 --- a/test/system/src/instructionsWebview.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { FrameLocator, Locator, Page } from '@playwright/test'; - -export default class InstructionsWebview { - constructor(protected readonly page: Page) {} - - private frameSelector = 'iframe.webview.ready'; - - private get frame(): FrameLocator { - return ( - this.page - .frameLocator(this.frameSelector) - // this assumes that the first iframe is the instructions page - .first() - .frameLocator('#active-frame') - ); - } - - public get currentPage(): Locator { - return this.frame.locator('.qs:visible').first(); - } - - public getPageByTitle(title: string): Locator { - return this.currentPage.locator('header, .qs-step__head').locator(`text="${title}"`).first(); - } - - public async ready(): Promise { - await this.currentPage.waitFor(); - } - - public async pageTitle(): Promise { - return this.frame - .locator('.qs:visible') - .locator('header h1, [data-cy="title"]') - .first() - .innerText(); - } - - public async copyClipboardText(): Promise { - await this.frame.locator('.qs:visible .code-snippet button').click(); - } - - public async clickButton(label: string): Promise { - // This is hacky, but it works for now. - // Hit escape to clear any notifications that may hide a button. - await this.page.keyboard.press('Escape'); - await this.frame.locator(`.qs:visible button:has-text("${label}"):visible >> nth=0`).click(); - } - - public async selectProjectPickerRow(projectName: string): Promise { - await this.page.keyboard.press('Escape'); - const row = this.frame.locator(`.qs:visible div.project-picker-row:has-text("${projectName}")`); - await row.evaluate((el) => { - const element = el as HTMLElement; - element.click(); - }); - } -} diff --git a/test/system/src/panel.ts b/test/system/src/panel.ts deleted file mode 100644 index bfae38ac..00000000 --- a/test/system/src/panel.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Locator, Page } from '@playwright/test'; - -export default class Panel { - constructor(protected readonly page: Page) {} - - private get panel(): Locator { - return this.page.locator('.panel[id="workbench.parts.panel"]'); - } - - public get problems(): Locator { - return this.panel.locator('.markers-panel-container'); - } - - public async open(): Promise { - await this.panel.click(); - } - - public async close(): Promise { - await this.panel.click(); - } -} diff --git a/test/system/src/util.ts b/test/system/src/util.ts deleted file mode 100644 index b8efab56..00000000 --- a/test/system/src/util.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as os from 'os'; - -export function getOsShortcut(shortcut: string): string { - return os.platform() === 'darwin' ? shortcut.replace(/[Cc]ontrol/, 'Meta') : shortcut; -} - -export interface TimeoutOptions { - timeoutMs?: number; - retryIntervalMs?: number; -} - -export function timeout(fn: () => T, options?: TimeoutOptions): Promise { - const timeoutMs = options?.timeoutMs ?? 30000; - const retryIntervalMs = options?.retryIntervalMs ?? 100; - - return new Promise((resolve, reject) => { - const valueResolver = (value: T) => { - if (value) { - clearInterval(intervalHandle); - resolve(value); - } - }; - - const intervalHandle = setInterval(() => { - console.error('trying timeout fn'); - const value = fn(); - value instanceof Promise ? value.then(valueResolver) : valueResolver(value); - }, retryIntervalMs); - - setTimeout(() => { - clearInterval(intervalHandle); - reject(new Error(`timeout reached (${timeoutMs}ms)`)); - }, timeoutMs); - }); -} - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export async function ignoreNotFound(fn: (...args: any[]) => any): Promise { - try { - return await fn(); - } catch (e) { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - if ((e as any).code !== 'ENOENT') { - throw e; - } - } -} diff --git a/test/system/tests/findings.test.ts b/test/system/tests/findings.test.ts deleted file mode 100644 index 8a2cc9b4..00000000 --- a/test/system/tests/findings.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as path from 'path'; - -describe('Findings and scanning', function () { - beforeEach(async function () { - const { driver, project } = this; - - const pidfile = path.join(project.workspacePath, '**', 'index.pid'); - await project.reset('**/*.appmap.json', 'appmap.yml', '**/appmap-findings.json'); - await driver.closePanel(); - await driver.resetUsage(); - await driver.reload(); - await project.restoreFiles('appmap.yml'); - await driver.waitForFile(pidfile); - }); - - it('automatically identifies findings as AppMaps are created', async function () { - const { driver, project } = this; - - await project.restoreFiles('**/*.appmap.json'); - await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer - - await driver.appMap.openActionPanel(); - await driver.appMap.ready(); - await driver.appMap.expandFindings(); - await driver.appMap.findingsTree.click(); - await driver.appMap.findingsTreeItem().waitFor(); - await driver.appMap.findingsTreeItem(1).waitFor(); - }); -}); diff --git a/test/system/tests/instructions.test.ts b/test/system/tests/instructions.test.ts deleted file mode 100644 index e42134c7..00000000 --- a/test/system/tests/instructions.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import assert from 'assert'; -import * as path from 'path'; -import { waitFor } from '../../waitFor'; -import { InstructionStep, InstructionStepStatus } from '../src/appMap'; - -describe('Instructions tree view', function () { - beforeEach(async function () { - const { driver, project } = this; - await project.reset('**/*.appmap.json', 'appmap.yml', '**/appmap-findings.json'); - - await driver.closePanel(); - await driver.resetUsage(); - await driver.reload(); - }); - - it('accurately depicts the installation state', async function () { - const { driver, project } = this; - await driver.appMap.openActionPanel(); - await driver.appMap.expandInstructions(); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.InstallAppMapAgent, - InstructionStepStatus.Pending - ); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.RecordAppMaps, - InstructionStepStatus.Pending - ); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.NavieIntroduction, - InstructionStepStatus.Pending - ); - - await project.simulateAppMapInstall(); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.InstallAppMapAgent, - InstructionStepStatus.Complete - ); - const pidfile = path.join(project.workspacePath, '**', 'index.pid'); - await driver.waitForFile(pidfile); - - await project.restoreFiles('**/*.appmap.json'); - - await driver.appMap.assertInstructionStepStatus( - InstructionStep.RecordAppMaps, - InstructionStepStatus.Complete - ); - - await driver.appMap.openInstruction(InstructionStep.NavieIntroduction); - - await driver.appMap.assertInstructionStepStatus( - InstructionStep.NavieIntroduction, - InstructionStepStatus.Complete - ); - - // "Delete All AppMaps" is working, but this assertion fails for other reasons. - // FIXME: https://github.com/getappmap/vscode-appland/issues/716 - // - // await driver.runCommand('AppMap: Delete All AppMaps'); - // await driver.appMap.assertInstructionStepStatus( - // InstructionStep.RecordAppMaps, - // InstructionStepStatus.Pending - // ); - }); - - it('opens up the expected web views', async function () { - const { driver } = this; - - await driver.appMap.openActionPanel(); - await driver.appMap.expandInstructions(); - - const pages = [ - { step: InstructionStep.InstallAppMapAgent, title: 'Add AppMap to your project' }, - { - step: InstructionStep.RecordAppMaps, - title: 'Record AppMaps', - }, - ]; - for (let i = 0; i < pages.length; i++) { - const page = pages[i]; - await driver.appMap.openInstruction(page.step); - await waitFor( - `Expected page '${page.title}' to be visible`, - async (): Promise => (await driver.instructionsWebview.pageTitle()) === page.title - ); - } - }); - - it('re-uses web views', async function () { - const { driver } = this; - - const assertTabs = async (expectedCount: number): Promise => { - const numTabs = await driver.tabCount(); - assert.strictEqual(numTabs, expectedCount, 'Wrong number of tabs'); - }; - - await driver.appMap.openActionPanel(); - await driver.appMap.expandInstructions(); - await driver.appMap.openInstruction(InstructionStep.InstallAppMapAgent); - await assertTabs(1); - - await driver.appMap.openInstruction(InstructionStep.RecordAppMaps); - await assertTabs(1); - }); - - it('can be stepped through as expected', async function () { - const { driver, project } = this; - - await driver.appMap.openActionPanel(); - await driver.appMap.expandInstructions(); - await driver.appMap.ready(); - - await driver.appMap.assertInstructionStepStatus( - InstructionStep.InstallAppMapAgent, - InstructionStepStatus.Pending - ); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.RecordAppMaps, - InstructionStepStatus.Pending - ); - await driver.appMap.assertInstructionStepStatus( - InstructionStep.NavieIntroduction, - InstructionStepStatus.Pending - ); - - await driver.appMap.openInstruction(InstructionStep.InstallAppMapAgent); - await driver.instructionsWebview.getPageByTitle('Add AppMap to your project').waitFor(); - - await project.simulateAppMapInstall(); - const pidfile = path.join(project.workspacePath, '**', 'index.pid'); - await driver.waitForFile(pidfile); - - await driver.instructionsWebview.selectProjectPickerRow('project-system'); - await driver.instructionsWebview.clickButton('Next'); - await driver.instructionsWebview.getPageByTitle('Record AppMaps').waitFor(); - - await project.restoreFiles('**/*.appmap.json'); - - await driver.instructionsWebview.clickButton('Next'); - - // Wait five seconds for the second tab to open. - // The second tab is the Navie chat interface. - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for chat interface')); - }, 5_000); - const interval = setInterval(async () => { - if ((await driver.tabCount()) === 2) { - clearTimeout(timeout); - clearInterval(interval); - resolve(undefined); - } - }, 100); - }); - }); - - it('always opens the project picker in an empty/undefined workspace', async function () { - const { driver } = this; - await driver.closeFolder(); - await driver.waitForReady(); - await driver.appMap.openActionPanel(); - await driver.appMap.ready(); - await driver.appMap.expandInstructions(); - - const expectProjectPicker = () => - waitFor( - 'Expected project picker to be visible', - async (): Promise => - (await driver.instructionsWebview.pageTitle()) === 'Add AppMap to your project' - ); - - const steps = [ - InstructionStep.InstallAppMapAgent, - InstructionStep.RecordAppMaps, - InstructionStep.NavieIntroduction, - ]; - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - await driver.appMap.openInstruction(step); - await expectProjectPicker(); - } - }); -}); diff --git a/test/system/tests/support/context.ts b/test/system/tests/support/context.ts deleted file mode 100644 index 6d1e6bce..00000000 --- a/test/system/tests/support/context.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BrowserContext, ElectronApplication, Page } from '@playwright/test'; -import Driver from '../../src/driver'; -import ProjectDirectory from './project'; - -export default interface Context { - app: ElectronApplication; - context: BrowserContext; - page: Page; - driver: Driver; - project: ProjectDirectory; -} diff --git a/test/systemTest.ts b/test/systemTest.ts deleted file mode 100644 index 312094e2..00000000 --- a/test/systemTest.ts +++ /dev/null @@ -1,128 +0,0 @@ -import glob from 'glob'; -import Mocha from 'mocha'; -import * as path from 'path'; -import { downloadCode, launchCode } from './system/src/app'; -import Driver from './system/src/driver'; -import ProjectDirectory from './system/tests/support/project'; -import { TestStatus } from './TestStatus'; -import projectRootDirectory from 'project-root-directory'; -import { ElectronApplication } from '@playwright/test'; -import { access } from 'fs/promises'; -import { join } from 'path'; - -declare module 'mocha' { - export interface Context { - app: ElectronApplication; - driver: Driver; - project: ProjectDirectory; - } -} - -async function main(): Promise { - try { - const extensionDevelopmentPath = projectRootDirectory; - - await access(join(extensionDevelopmentPath, 'out', 'extension.js')).catch(() => { - throw new Error('Please build the extension first with `yarn compile`.'); - }); - - const userDataDir = path.resolve(projectRootDirectory, '.vscode-test', 'user-data'); - const projectPath = path.join( - projectRootDirectory, - 'test', - 'fixtures', - 'workspaces', - 'project-system' - ); - - let verbose = false; - let testPath = path.join(__dirname, 'system', 'tests', '**', '*.test.[jt]s'); - const patterns: string[] = []; - const cliArgs = process.argv.slice(2); - while (cliArgs.length > 0) { - const arg = cliArgs.shift() as string; - switch (arg) { - case '--verbose': - verbose = true; - break; - - case '--pattern': - case '-p': - { - const pattern = cliArgs.shift(); - if (!pattern) { - throw new Error('Missing pattern'); - } - patterns.push(pattern); - } - break; - - default: - testPath = path.isAbsolute(arg) ? arg : path.resolve(arg); - } - } - - const TIMEOUT = Number(process.env.TEST_TIMEOUT || 80000); - const beforeAll: Mocha.Func = async function () { - this.codePath = await downloadCode(); - }; - const beforeEach: Mocha.Func = async function () { - const project = new ProjectDirectory(projectPath); - - const { app, context, page } = await launchCode( - this.codePath, - extensionDevelopmentPath, - userDataDir, - { - workspacePath: project.workspacePath, - verbose, - } - ); - const driver = new Driver(app, context, page); - context.setDefaultTimeout(TIMEOUT); - - await driver.waitForReady(); - this.app = app; - this.driver = driver; - this.project = project; - }; - const afterEach: Mocha.Func = async function () { - await Promise.all([this.app.waitForEvent('close'), this.app.process().kill()]); - }; - - const mocha = new Mocha({ - ui: 'bdd', - color: true, - timeout: TIMEOUT, - reporter: Mocha.reporters.Spec, - rootHooks: { - beforeAll, - beforeEach, - afterEach, - }, - }); - patterns.forEach((pattern) => mocha.grep(pattern)); - - return new Promise((_, reject) => { - glob(testPath, { cwd: __dirname }, (err, files) => { - if (err) { - return reject(err); - } - - // Add files to the test suite - files.forEach((f) => { - mocha.addFile(f); - }); - - mocha.run((failures) => { - process.exitCode = failures === 0 ? TestStatus.Ok : TestStatus.Failed; - }); - }); - }); - } catch (err) { - console.error(err); - process.exitCode = TestStatus.Error; - } -} - -main(); diff --git a/test/unit/analyzers/javascript.test.ts b/test/unit/analyzers/javascript.test.ts deleted file mode 100644 index 3e3cd9e0..00000000 --- a/test/unit/analyzers/javascript.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import '../mock/vscode'; -import { Uri, workspace, WorkspaceFolder } from 'vscode'; - -import { expect } from 'chai'; -import sinon from 'sinon'; - -import analyze, { parseYarnPkgSpec } from '../../../src/analyzers/javascript'; -import assert from 'assert'; -import { Features } from '../../../src/analyzers'; - -describe('JavaScript analyzer', () => { - test('notes the lack of package.json', {}, (f) => - expect(f.lang.text).to.include( - 'You can add AppMap to this project by creating a package.json file' - ) - ); - - test('detects express.js', { pkg: { dependencies: { express: 'latest' } } }, (f) => - expect(f.web?.score).to.equal('early-access') - ); - - test( - 'detects express.js in package-lock.json', - { pkg: {}, pkgLock: { dependencies: { express: { version: '4.18.1' } } } }, - (f) => expect(f.web?.score).to.equal('early-access') - ); - - test( - 'detects express.js in yarn.lock', - { pkg: {}, yarnLock: 'express@npm:^4: { version: 4.18.1 }' }, - (f) => expect(f.web?.score).to.equal('early-access') - ); - - test( - 'detects express.js in old-style yarn.lock', - { pkg: {}, yarnLock: `# yarn lockfile v1\nexpress@^4:\n version "4.18.1"\n` }, - (f) => expect(f.web?.score).to.equal('early-access') - ); - - test('detects mocha', { pkg: { devDependencies: { mocha: 'latest' } } }, (f) => - expect(f.test).to.include({ title: 'Mocha', score: 'early-access' }) - ); - - test('rejects old mocha', { pkg: { devDependencies: { mocha: '^7' } } }, (f) => - expect(f.test).to.include({ title: 'Mocha', score: 'unsupported' }) - ); - - test('detects jest', { pkg: { devDependencies: { jest: 'latest' } } }, (f) => - expect(f.test).to.include({ title: 'Jest', score: 'early-access' }) - ); - - test('rejects old jest', { pkg: { devDependencies: { jest: '^24' } } }, (f) => - expect(f.test).to.include({ title: 'Jest', score: 'unsupported' }) - ); - - type DepFiles = { - pkg?: unknown; - pkgLock?: unknown; - yarnLock?: string; - }; - - function test(title: string, deps: DepFiles, verifier: (f: Features) => void) { - it(title, async () => { - useDepFiles(deps); - const features = (await analyze(testFolder))?.features; - expect(features).to.exist; - assert(features); - verifier(features); - }); - } - - const testFolder: WorkspaceFolder = { - uri: Uri.parse('test:///project'), - name: 'test-project', - index: 0, - }; - - function useDepFiles(deps: DepFiles) { - sinon.stub(workspace.fs, 'readFile').callsFake(async (uri) => { - let result: string | undefined = undefined; - switch (uri.toString()) { - case 'test:/project/package.json': - result = deps.pkg ? JSON.stringify(deps.pkg) : undefined; - break; - case 'test:/project/package-lock.json': - result = deps.pkgLock ? JSON.stringify(deps.pkgLock) : undefined; - break; - case 'test:/project/yarn.lock': - result = deps.yarnLock ? deps.yarnLock : undefined; - break; - default: - throw new Error(`unexpected read of ${uri.toString()}`); - } - - if (result) return new TextEncoder().encode(result); - throw new Error(`mock read error of ${uri.toString()}`); - }); - } - - afterEach(sinon.restore); -}); - -describe('parseYarnPkgSpec', () => { - test('pkg', 'pkg'); - test('pkg@^4.2', 'pkg'); - test('pkg@source:^4.2', 'pkg'); - test('@org/pkg', '@org/pkg'); - test('@org/pkg@^4.2', '@org/pkg'); - test('@org/pkg@source:^4.2', '@org/pkg'); - - function test(example: string, name: string) { - it(`parses ${example} correctly`, () => expect(parseYarnPkgSpec(example).name).to.equal(name)); - } -}); diff --git a/test/unit/project.test.ts b/test/unit/project.test.ts index 6773438f..f09906f7 100644 --- a/test/unit/project.test.ts +++ b/test/unit/project.test.ts @@ -5,7 +5,7 @@ chai.config.truncateThreshold = 0; import { join } from 'path'; -import ProjectDirectory from '../system/tests/support/project'; +import ProjectDirectory from '../projectDirectory'; describe('ProjectDirectory', () => { describe('with default options', () => { diff --git a/test/unit/proxySettings.test.ts b/test/unit/proxySettings.test.ts index f21b7ac3..43787d9c 100644 --- a/test/unit/proxySettings.test.ts +++ b/test/unit/proxySettings.test.ts @@ -22,7 +22,7 @@ describe('proxySettings', () => { if (key === 'http.noProxy') return ['localhost']; return undefined; }), - } as any); + } as any); // eslint-disable-line @typescript-eslint/no-explicit-any // Set mock environment variables process.env.http_proxy = 'http://env-http-proxy'; diff --git a/test/unit/services/runConfigServiceInstance.test.ts b/test/unit/services/runConfigServiceInstance.test.ts deleted file mode 100644 index a804a3eb..00000000 --- a/test/unit/services/runConfigServiceInstance.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { default as chai, expect } from 'chai'; -import { default as chaiFs } from 'chai-fs'; -import MockVSCode from '../mock/vscode'; -import { URI } from 'vscode-uri'; -import { RunConfigServiceInstance } from '../../../src/services/runConfigService'; -import path from 'path'; -import { tmpdir } from 'os'; -import type { ProjectStateServiceInstance } from '../../../src/services/projectStateService'; -import type ExtensionState from '../../../src/configuration/extensionState'; -import Sinon from 'sinon'; -import sinonChai from 'sinon-chai'; - -chai.use(chaiFs).use(sinonChai); - -describe('RunConfigServiceInstance', () => { - let instance: RunConfigServiceInstance; - const workspaceFolderName = 'runConfigServiceInstance-test'; - const workspaceFolderPath = path.join(tmpdir(), workspaceFolderName); - const workspaceFolder = { - uri: URI.file(workspaceFolderPath), - index: 0, - name: workspaceFolderName, - }; - const mockProjectStateServiceInstance = { - metadata: { - language: { - name: 'Java', - }, - }, - } as unknown as ProjectStateServiceInstance; - const mockExtensionState = {} as unknown as ExtensionState; - const mockEventEmitter = new MockVSCode.EventEmitter(); - - beforeEach(() => { - instance = new RunConfigServiceInstance( - workspaceFolder, - mockProjectStateServiceInstance, - mockExtensionState, - mockEventEmitter - ); - }); - - afterEach(() => { - Sinon.restore(); - }); - - describe('addMissingConfigs', () => { - it('creates missing configs', async () => { - Sinon.stub(instance, 'hasLaunchConfig').returns(false); - Sinon.stub(instance, 'hasTestConfig').returns(false); - - const updateLaunchConfig = Sinon.spy(instance, 'updateLaunchConfig'); - const updateTestConfig = Sinon.spy(instance, 'updateTestConfig'); - - await instance.addMissingConfigs(); - - expect(updateLaunchConfig).to.have.been.calledOnce; - expect(updateTestConfig).to.have.been.calledOnce; - }); - - it('does not update configs that already exist', async () => { - Sinon.stub(instance, 'hasLaunchConfig').returns(false); - Sinon.stub(instance, 'hasTestConfig').returns(true); - - const updateLaunchConfig = Sinon.spy(instance, 'updateLaunchConfig'); - const updateTestConfig = Sinon.spy(instance, 'updateTestConfig'); - - await instance.addMissingConfigs(); - - expect(updateLaunchConfig).to.have.been.calledOnce; - expect(updateTestConfig).to.have.not.been.called; - }); - }); -}); diff --git a/web/src/chatSearchView.js b/web/src/chatSearchView.js index 9691c457..cbdf24a1 100644 --- a/web/src/chatSearchView.js +++ b/web/src/chatSearchView.js @@ -24,6 +24,9 @@ export default function mountChatSearchView() { appmapYmlPresent: this.appmapYmlPresent, targetAppmapData: initialData.targetAppmap, targetAppmapFsPath: initialData.targetAppmapFsPath, + openNewChat() { + vscode.postMessage({ command: 'open-new-chat' }); + }, }, }); }, diff --git a/web/src/installGuideView.js b/web/src/installGuideView.js index 41a6452c..adbe2599 100644 --- a/web/src/installGuideView.js +++ b/web/src/installGuideView.js @@ -8,7 +8,6 @@ export default function mountInstallGuide() { const messages = new MessagePublisher(vscode); messages.on('init', (initialData) => { - let currentPage = initialData.page; let currentProject; const app = new Vue({ @@ -34,36 +33,18 @@ export default function mountInstallGuide() { javaAgentStatus: initialData.javaAgentStatus, }; }, - beforeCreate() { - this.$on('open-page', async (pageId) => { - // Wait until next frame if there's no current project. It may take some time for the - // view to catch up. - if (!currentProject) await new Promise((resolve) => requestAnimationFrame(resolve)); - - currentPage = pageId; - - vscode.postMessage({ - command: 'open-page', - page: currentPage, - project: currentProject, - projects: this.projects, - }); - }); - }, mounted() { document.querySelectorAll('a[href]').forEach((el) => { el.addEventListener('click', (e) => { vscode.postMessage({ command: 'click-link', uri: e.target.href }); }); }); - this.$refs.ui.jumpTo(initialData.page); }, }); app.$on('clipboard', (text) => { vscode.postMessage({ command: 'clipboard', - page: currentPage, project: currentProject, text, }); @@ -77,18 +58,10 @@ export default function mountInstallGuide() { messages.rpc('open-navie'); }); - app.$on('open-instruction', (pageId) => { - app.$refs.ui.jumpTo(pageId); - }); - app.$on('perform-install', (path, language) => { vscode.postMessage({ command: 'perform-install', path, language }); }); - app.$on('add-java-configs', (projectPath) => { - vscode.postMessage({ command: 'add-java-configs', projectPath }); - }); - app.$on('view-output', () => { vscode.postMessage({ command: 'view-output' }); }); @@ -97,10 +70,6 @@ export default function mountInstallGuide() { vscode.postMessage({ command: 'submit-to-navie', suggestion }); }); - messages.on('page', ({ page }) => { - app.$refs.ui.jumpTo(page); - }); - messages.on('projects', ({ projects }) => { app.projects = projects; app.$forceUpdate(); diff --git a/web/static/styles/navie-integration.css b/web/static/styles/navie-integration.css index b03125ca..e0c6b2a0 100644 --- a/web/static/styles/navie-integration.css +++ b/web/static/styles/navie-integration.css @@ -9,10 +9,6 @@ box-shadow: none !important; } -.recording-method-grid .popper__body { - overflow: hidden; - padding: 1rem; - background-color: #141924; - border-radius: 1rem; - border: 1px solid #2d3546; +.recording-method__ask-navie-button { + display: none; } diff --git a/yarn.lock b/yarn.lock index 0f8d8666..9ff0a2c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,9 +151,9 @@ __metadata: languageName: node linkType: hard -"@appland/components@npm:^4.12.0": - version: 4.29.0 - resolution: "@appland/components@npm:4.29.0" +"@appland/components@npm:^4.12.0, @appland/components@npm:^4.31.0": + version: 4.31.0 + resolution: "@appland/components@npm:4.31.0" dependencies: "@appland/client": ^1.12.0 "@appland/diagrams": ^1.7.0 @@ -177,37 +177,7 @@ __metadata: sql-formatter: ^4.0.2 vue: ^2.7.14 vuex: ^3.6.0 - checksum: 919a575678769784b06874f1c3c0ae5560339edceb9bfcf171d7cf04a9ebb4d1055ae9912dc2c70113bc0de10ddb168393c268b8971e13ef84b639984165a7df - languageName: node - linkType: hard - -"@appland/components@npm:^4.30.0": - version: 4.30.0 - resolution: "@appland/components@npm:4.30.0" - dependencies: - "@appland/client": ^1.12.0 - "@appland/diagrams": ^1.7.0 - "@appland/models": ^2.10.2 - "@appland/rpc": ^1.9.0 - "@appland/sequence-diagram": ^1.11.0 - buffer: ^6.0.3 - d3: ^7.8.4 - d3-flame-graph: ^4.1.3 - diff: ^5.1.0 - dom-to-svg: ^0.12.2 - dompurify: ^3.0.6 - events: ^3.3.0 - highlight.js: ^11.9.0 - jayson: ^4.1.0 - js-base64: ^3.7.7 - marked: ^10.0.0 - marked-highlight: ^2.0.7 - mermaid: ^10.9.1 - pako: ^2.1.0 - sql-formatter: ^4.0.2 - vue: ^2.7.14 - vuex: ^3.6.0 - checksum: 1e64ce3e8d6cac3f956b25247a1ad3717c9d461a4b5d23c6e0e8b24383c62026197027bd967d4b10aa3142f462158ce19e34eb8c0993bafabb2c20b739725ede + checksum: de74e7e8e2526d538319dd913eec2be7468b72038cd9a9cd29afab92655fab3d1a69bc6f9ccce0d40b19760996f8639b607ff144cda8e5634fa62b746395e29c languageName: node linkType: hard @@ -3269,7 +3239,7 @@ __metadata: dependencies: "@appland/appmap": ^3.129.0 "@appland/client": ^1.14.1 - "@appland/components": ^4.30.0 + "@appland/components": ^4.31.0 "@appland/diagrams": ^1.8.0 "@appland/models": ^2.10.2 "@appland/rpc": ^1.7.0