diff --git a/lana/package.json b/lana/package.json index 867fd01d6..e150a2c76 100644 --- a/lana/package.json +++ b/lana/package.json @@ -97,8 +97,7 @@ "menus": { "commandPalette": [ { - "command": "lana.showLogAnalysis", - "when": "resourceLangId == apexlog || lana.isApexLog" + "command": "lana.showLogAnalysis" } ], "editor/context": [ @@ -386,7 +385,8 @@ "dependencies": { "@apexdevtools/apex-parser": "5.1.0", "@salesforce/vscode-services": "^67.12.0", - "effect": "^3.22.0" + "effect": "^3.22.0", + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/jest": "^30.0.0", diff --git a/lana/src/__tests__/helpers/test-builders.ts b/lana/src/__tests__/helpers/test-builders.ts index d6e3476ba..8c17cd991 100644 --- a/lana/src/__tests__/helpers/test-builders.ts +++ b/lana/src/__tests__/helpers/test-builders.ts @@ -179,6 +179,7 @@ export interface MockContext { context: MockExtensionContext; display: MockDisplay; workspaces: { uri: { fsPath: string }; name: string }[]; + workspaceManager?: unknown; } /** diff --git a/lana/src/__tests__/mocks/vscode.ts b/lana/src/__tests__/mocks/vscode.ts index bcbe9432e..a0c6c401a 100644 --- a/lana/src/__tests__/mocks/vscode.ts +++ b/lana/src/__tests__/mocks/vscode.ts @@ -12,6 +12,7 @@ // a drift from `@types/vscode` surfaces as ONE error at the factory, not at // every call site. import type { EndOfLine, TextDocument } from 'vscode'; +import { URI, Utils } from 'vscode-uri'; // Track subscriptions for cleanup const subscriptions: { dispose: jest.Mock }[] = []; @@ -110,34 +111,11 @@ export const ViewColumn = { } as const; export type ViewColumn = (typeof ViewColumn)[keyof typeof ViewColumn]; -// Mock Uri class +// Delegate URI semantics to vscode-uri so virtual URI tests match VS Code. export const Uri = { - file: jest.fn((path: string) => ({ - scheme: 'file', - authority: '', - path, - fsPath: path, - query: '', - fragment: '', - with: jest.fn(), - toString: jest.fn(() => `file://${path}`), - toJSON: jest.fn(() => ({ scheme: 'file', path, fsPath: path })), - })), - parse: jest.fn((value: string) => ({ - scheme: value.startsWith('file://') ? 'file' : 'unknown', - authority: '', - path: value.replace('file://', ''), - fsPath: value.replace('file://', ''), - query: '', - fragment: '', - with: jest.fn(), - toString: jest.fn(() => value), - })), - joinPath: jest.fn((base, ...pathSegments) => ({ - ...base, - path: [base.path, ...pathSegments].join('/'), - fsPath: [base.fsPath, ...pathSegments].join('/'), - })), + file: (path: string) => URI.file(path), + parse: (value: string) => URI.parse(value), + joinPath: (base: URI, ...pathSegments: string[]) => Utils.joinPath(base, ...pathSegments), }; // Mock RelativePattern (constructor used for glob searches) @@ -300,6 +278,10 @@ export const workspace = { }, }; +export const extensions = { + getExtension: jest.fn(), +}; + // Mock window export const window = { showInformationMessage: jest.fn().mockResolvedValue(undefined), @@ -342,6 +324,10 @@ export const window = { replace: jest.fn(), })), createWebviewPanel: jest.fn(), + tabGroups: { + activeTabGroup: { activeTab: undefined }, + onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })), + }, activeTextEditor: undefined as unknown, visibleTextEditors: [], onDidChangeActiveTextEditor: jest.fn(() => ({ dispose: jest.fn() })), @@ -558,6 +544,7 @@ export default { ThemeColor, ConfigurationTarget, workspace, + extensions, window, commands, languages, diff --git a/lana/src/cache/LogEventCache.ts b/lana/src/cache/LogEventCache.ts index 896ecedd6..5412da1e0 100644 --- a/lana/src/cache/LogEventCache.ts +++ b/lana/src/cache/LogEventCache.ts @@ -1,12 +1,12 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ -import { readFile } from 'fs/promises'; import { workspace } from 'vscode'; import { parse, type ApexLog, type LogEvent } from 'apex-log-parser'; import type { Context } from '../Context.js'; +import { readFile } from '../services/salesforceServices.js'; export interface EventSearchResult { event: LogEvent; @@ -17,17 +17,17 @@ export class LogEventCache { private static readonly MAX_CACHE_SIZE = 10; private static cache = new Map(); - static async getApexLog(filePath: string): Promise { - const cached = LogEventCache.cache.get(filePath); + static async getApexLog(uriString: string): Promise { + const cached = LogEventCache.cache.get(uriString); if (cached) { // Move to end (most recently used) - LogEventCache.cache.delete(filePath); - LogEventCache.cache.set(filePath, cached); + LogEventCache.cache.delete(uriString); + LogEventCache.cache.set(uriString, cached); return cached; } try { - const content = await readFile(filePath, 'utf-8'); + const content = await readFile(uriString); const apexLog = parse(content); // Evict oldest if at capacity @@ -38,7 +38,7 @@ export class LogEventCache { } } - LogEventCache.cache.set(filePath, apexLog); + LogEventCache.cache.set(uriString, apexLog); return apexLog; } catch { return null; @@ -49,15 +49,15 @@ export class LogEventCache { return LogEventCache.searchEvents(apexLog.children, timestamp, 0); } - static clearCache(filePath: string): void { - LogEventCache.cache.delete(filePath); + static clearCache(uriString: string): void { + LogEventCache.cache.delete(uriString); } static apply(context: Context): void { context.context.subscriptions.push( workspace.onDidCloseTextDocument((doc) => { if (doc.languageId === 'apexlog') { - LogEventCache.clearCache(doc.uri.fsPath); + LogEventCache.clearCache(doc.uri.toString()); } }), ); diff --git a/lana/src/cache/__tests__/LogEventCache.test.ts b/lana/src/cache/__tests__/LogEventCache.test.ts index 8ca68198e..6a8964a20 100644 --- a/lana/src/cache/__tests__/LogEventCache.test.ts +++ b/lana/src/cache/__tests__/LogEventCache.test.ts @@ -2,7 +2,6 @@ * Copyright (c) 2026 Certinia Inc. All rights reserved. */ import { beforeEach, describe, expect, it } from '@jest/globals'; - import { workspace } from 'vscode'; import { @@ -12,18 +11,17 @@ import { } from '../../__tests__/helpers/test-builders.js'; import { LogEventCache } from '../LogEventCache.js'; -// Mock fs/promises -jest.mock('fs/promises', () => ({ - readFile: jest.fn(), -})); - // Mock apex-log-parser jest.mock('apex-log-parser', () => ({ parse: jest.fn(), })); import { parse } from 'apex-log-parser'; -import { readFile } from 'fs/promises'; +import { readFile } from '../../services/salesforceServices.js'; + +jest.mock('../../services/salesforceServices.js', () => ({ + readFile: jest.fn(), +})); const mockReadFile = readFile as jest.Mock; const mockParse = parse as jest.Mock; @@ -373,8 +371,8 @@ describe('LogEventCache', () => { await LogEventCache.getApexLog('/test/file.log'); // Capture the callback - let closeCallback: ((doc: { languageId: string; uri: { fsPath: string } }) => void) | null = - null; + let closeCallback: + ((doc: { languageId: string; uri: { toString: () => string } }) => void) | null = null; (workspace.onDidCloseTextDocument as jest.Mock).mockImplementationOnce((cb) => { closeCallback = cb; return { dispose: jest.fn() }; @@ -386,7 +384,7 @@ describe('LogEventCache', () => { // Simulate closing an apexlog document closeCallback!({ languageId: 'apexlog', - uri: { fsPath: '/test/file.log' }, + uri: { toString: () => '/test/file.log' }, }); // @ts-expect-error - accessing private static for testing @@ -401,8 +399,8 @@ describe('LogEventCache', () => { await LogEventCache.getApexLog('/test/file.log'); // Capture the callback - let closeCallback: ((doc: { languageId: string; uri: { fsPath: string } }) => void) | null = - null; + let closeCallback: + ((doc: { languageId: string; uri: { toString: () => string } }) => void) | null = null; (workspace.onDidCloseTextDocument as jest.Mock).mockImplementationOnce((cb) => { closeCallback = cb; return { dispose: jest.fn() }; @@ -414,7 +412,7 @@ describe('LogEventCache', () => { // Simulate closing a non-apexlog document closeCallback!({ languageId: 'javascript', - uri: { fsPath: '/test/file.log' }, + uri: { toString: () => '/test/file.log' }, }); // @ts-expect-error - accessing private static for testing diff --git a/lana/src/codelenses/ShowAnalysisCodeLens.ts b/lana/src/codelenses/ShowAnalysisCodeLens.ts index f215820fa..820a4a6b9 100644 --- a/lana/src/codelenses/ShowAnalysisCodeLens.ts +++ b/lana/src/codelenses/ShowAnalysisCodeLens.ts @@ -28,11 +28,7 @@ class ShowAnalysisCodeLens implements CodeLensProvider { } static apply(context: Context): void { - const docSelector = [ - { scheme: 'file', language: 'apexlog' }, - { scheme: 'file', pattern: '**/*.log' }, - { scheme: 'file', pattern: '**/*.txt' }, - ]; + const docSelector = [{ language: 'apexlog' }, { pattern: '**/*.log' }, { pattern: '**/*.txt' }]; const codeLensProviderDisposable = languages.registerCodeLensProvider( docSelector, diff --git a/lana/src/commands/LogView.ts b/lana/src/commands/LogView.ts index d025542fa..d6192ba89 100644 --- a/lana/src/commands/LogView.ts +++ b/lana/src/commands/LogView.ts @@ -1,16 +1,14 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { createReadStream, existsSync } from 'fs'; -import { writeFile } from 'fs/promises'; -import { homedir } from 'os'; -import { basename, dirname, join, parse } from 'path'; import { Uri, commands, window as vscWindow, workspace, type WebviewPanel } from 'vscode'; +import { Utils } from 'vscode-uri'; import type { Context } from '../Context.js'; import { OpenFileInPackage } from '../display/OpenFileInPackage.js'; import { WebView } from '../display/WebView.js'; import { RawLogNavigation } from '../log-features/RawLogNavigation.js'; +import { fileOrFolderExists, readFile, writeFile } from '../services/salesforceServices.js'; import { PRIVATE_SECTIONS, getColumnOverrides, @@ -32,7 +30,7 @@ interface WebViewLogFileRequest { export class LogView { private static helpUrl = 'https://certinia.github.io/debug-log-analyzer/'; private static currentPanel: WebviewPanel | undefined; - private static currentLogPath: string | undefined; + private static currentLogUri: Uri | undefined; private static pendingNavigationTimestamp: number | undefined; static getCurrentView() { @@ -40,7 +38,11 @@ export class LogView { } static getLogPath() { - return LogView.currentLogPath; + return LogView.currentLogUri?.toString(); + } + + static getLogUri(): Uri | undefined { + return LogView.currentLogUri; } static setPendingNavigation(timestamp: number): void { @@ -50,22 +52,24 @@ export class LogView { static async createView( context: Context, beforeSendLog?: Promise, - logPath?: string, + logUri?: Uri, logData?: string, ): Promise { - const panel = WebView.apply('logFile', `Log: ${logPath ? basename(logPath) : 'Untitled'}`, [ - Uri.file(join(context.context.extensionPath, 'out')), - Uri.file(dirname(logPath || '')), + const logName = logUri ? Utils.basename(logUri) : 'Untitled'; + const logDir = logUri ? Utils.dirname(logUri) : context.context.extensionUri; + const panel = WebView.apply('logFile', `Log: ${logName}`, [ + Utils.joinPath(context.context.extensionUri, 'out'), + logDir, ]); this.currentPanel = panel; - this.currentLogPath = logPath; + this.currentLogUri = logUri; - const logViewerRoot = join(context.context.extensionPath, 'out'); - const index = join(logViewerRoot, 'index.html'); - const bundleUri = panel.webview.asWebviewUri(Uri.file(join(logViewerRoot, 'bundle.js'))); - const codiconUri = panel.webview.asWebviewUri(Uri.file(join(logViewerRoot, 'codicon.css'))); + const logViewerRoot = Utils.joinPath(context.context.extensionUri, 'out'); + const index = Utils.joinPath(logViewerRoot, 'index.html'); + const bundleUri = panel.webview.asWebviewUri(Utils.joinPath(logViewerRoot, 'bundle.js')); + const codiconUri = panel.webview.asWebviewUri(Utils.joinPath(logViewerRoot, 'codicon.css')); const indexSrc = await this.getFile(index); - panel.iconPath = Uri.file(join(logViewerRoot, 'certinia-icon-color.png')); + panel.iconPath = Utils.joinPath(logViewerRoot, 'certinia-icon-color.png'); panel.webview.html = indexSrc .replace(/bundle\.js/gi, bundleUri.toString(true)) .replace(/codicon\.css/gi, codiconUri.toString(true)); @@ -90,7 +94,7 @@ export class LogView { () => { configListener.dispose(); this.currentPanel = undefined; - this.currentLogPath = undefined; + this.currentLogUri = undefined; }, undefined, context.context.subscriptions, @@ -98,27 +102,31 @@ export class LogView { panel.webview.onDidReceiveMessage( async (msg: WebViewLogFileRequest) => { + if (!isWebViewLogFileRequest(msg)) { + return; + } const { cmd, requestId, payload } = msg; switch (cmd) { case 'fetchLog': { + if (!requestId) { + break; + } await beforeSendLog; - LogView.sendLog(requestId, panel, context, logPath, logData); + await LogView.sendLog(requestId, panel, context, logUri, logData); break; } case 'openPath': { - const filePath = payload as string; - if (filePath) { - context.display.showFile(filePath); + if (logUri) { + context.display.showFile(logUri); } break; } case 'openType': { - const symbol = payload as string; - if (symbol) { - await OpenFileInPackage.openFileForSymbol(context, symbol); + if (typeof payload === 'string' && payload) { + await OpenFileInPackage.openFileForSymbol(context, payload); } break; } @@ -148,8 +156,8 @@ export class LogView { } case 'updateConfig': { - const { section, value } = payload as { section: string; value: unknown }; - if (section) { + if (isConfigUpdate(payload)) { + const { section, value } = payload; if ((PRIVATE_SECTIONS as readonly string[]).includes(section)) { updatePrivateSection(context.context.globalState, section, value); } else { @@ -160,20 +168,16 @@ export class LogView { } case 'saveFile': { - const { fileContent, options } = payload as { - fileContent: string; - options: { defaultFileName?: string }; - }; - - if (fileContent && options?.defaultFileName) { + if (isSaveFileRequest(payload)) { + const { fileContent, options } = payload; const defaultWorkspace = (workspace.workspaceFolders || [])[0]; - const defaultDir = defaultWorkspace?.uri.path || homedir(); + const defaultDir = defaultWorkspace?.uri ?? context.context.extensionUri; const destinationFile = await vscWindow.showSaveDialog({ - defaultUri: Uri.file(join(defaultDir, options.defaultFileName)), + defaultUri: Utils.joinPath(defaultDir, options.defaultFileName), }); if (destinationFile) { - writeFile(destinationFile.fsPath, fileContent).catch((error) => { + writeFile(destinationFile, fileContent).catch((error) => { const msg = error instanceof Error ? error.message : String(error); vscWindow.showErrorMessage(`Unable to save file: ${msg}`); }); @@ -183,17 +187,15 @@ export class LogView { } case 'showError': { - const { text } = payload as { text: string }; - if (text) { - vscWindow.showErrorMessage(text); + if (isTextPayload(payload)) { + vscWindow.showErrorMessage(payload.text); } break; } case 'goToLogLine': { - const { timestamp } = payload as { timestamp: number }; - if (timestamp && LogView.currentLogPath) { - RawLogNavigation.goToLineByTimestamp(LogView.currentLogPath, timestamp); + if (isTimestampPayload(payload) && logUri) { + await RawLogNavigation.goToLineByTimestamp(logUri, payload.timestamp); } break; } @@ -226,36 +228,24 @@ export class LogView { return config; } - private static async getFile(filePath: string): Promise { - let data = ''; - return new Promise((resolve, reject) => { - createReadStream(filePath) - .on('error', (error) => { - reject(error); - }) - .on('data', (row) => { - data += row; - }) - .on('end', () => { - resolve(data); - }); - }); + private static async getFile(fileUri: Uri): Promise { + return readFile(fileUri); } - private static sendLog( + private static async sendLog( requestId: string, panel: WebviewPanel, context: Context, - logFilePath?: string, + logUri?: Uri, logData?: string, ) { - if (!logData && !existsSync(logFilePath || '')) { + if (!logData && logUri && !(await fileOrFolderExists(logUri))) { context.display.showErrorMessage('Log file could not be found.', { modal: true, }); + return; } - const filePath = parse(logFilePath || ''); const navigateToTimestamp = LogView.pendingNavigationTimestamp; LogView.pendingNavigationTimestamp = undefined; @@ -263,12 +253,72 @@ export class LogView { requestId, cmd: 'fetchLog', payload: { - logName: filePath.base, - logUri: logFilePath ? panel.webview.asWebviewUri(Uri.file(logFilePath)).toString(true) : '', - logPath: logFilePath, + logName: logUri ? Utils.basename(logUri) : '', + logUri: logUri ? panel.webview.asWebviewUri(logUri).toString(true) : '', + logPath: logUri?.toString(), logData: logData, navigateToTimestamp, }, }); } } + +function isWebViewLogFileRequest(value: unknown): value is WebViewLogFileRequest { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof (value as Record).cmd === 'string' && + ((value as Record).requestId === undefined || + typeof (value as Record).requestId === 'string') + ); +} + +function isConfigUpdate(value: unknown): value is { section: string; value: unknown } { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof (value as Record).section === 'string' && + Boolean((value as Record).section) + ); +} + +function isSaveFileRequest( + value: unknown, +): value is { fileContent: string; options: { defaultFileName: string } } { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false; + } + const payload = value as Record; + const options = payload.options; + return ( + typeof payload.fileContent === 'string' && + Boolean(payload.fileContent) && + typeof options === 'object' && + options !== null && + !Array.isArray(options) && + typeof (options as Record).defaultFileName === 'string' && + Boolean((options as Record).defaultFileName) + ); +} + +function isTextPayload(value: unknown): value is { text: string } { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof (value as Record).text === 'string' && + Boolean((value as Record).text) + ); +} + +function isTimestampPayload(value: unknown): value is { timestamp: number } { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof (value as Record).timestamp === 'number' && + Number.isFinite((value as Record).timestamp) + ); +} diff --git a/lana/src/commands/RetrieveLogFile.ts b/lana/src/commands/RetrieveLogFile.ts index 801026ee0..c7b340e68 100644 --- a/lana/src/commands/RetrieveLogFile.ts +++ b/lana/src/commands/RetrieveLogFile.ts @@ -1,13 +1,14 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { join } from 'path'; import { + Uri, window, type QuickPick as VSCodeQuickPick, type QuickPickItem, type WebviewPanel, } from 'vscode'; +import { Utils } from 'vscode-uri'; import { appName } from '../AppSettings.js'; import type { Context } from '../Context.js'; @@ -56,14 +57,14 @@ export class RetrieveLogFile { } private static async command(context: Context): Promise { - const workspacePath = await QuickPickWorkspace.pickOrReturn(context); + const workspace = await QuickPickWorkspace.pickOrReturn(context); const loadingPicker = RetrieveLogFile.showLoadingPicker(); try { const logFiles = await listLogs(); const logFileId = await RetrieveLogFile.getLogFile(logFiles); if (logFileId) { - const logFilePath = join( - workspacePath, + const logUri = Utils.joinPath( + Uri.parse(workspace.uri), '.sfdx', 'tools', 'debug', @@ -73,12 +74,12 @@ export class RetrieveLogFile { const logData = await getLogBody(logFileId); this.assertRetrievedLog(logFileId, logData); try { - await writeFile(logFilePath, logData); + await writeFile(logUri, logData); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); context.display.output(`Unable to cache retrieved log: ${message}`, true); } - return LogView.createView(context, undefined, logFilePath, logData); + return LogView.createView(context, undefined, logUri, logData); } } finally { loadingPicker.dispose(); diff --git a/lana/src/commands/ShowInLogAnalysis.ts b/lana/src/commands/ShowInLogAnalysis.ts index 7a87a6bcf..b1a9879a6 100644 --- a/lana/src/commands/ShowInLogAnalysis.ts +++ b/lana/src/commands/ShowInLogAnalysis.ts @@ -1,7 +1,7 @@ /* * Copyright (c) 2025 Certinia Inc. All rights reserved. */ -import { window } from 'vscode'; +import { Uri, window } from 'vscode'; import type { Context } from '../Context.js'; import { Command } from './Command.js'; @@ -30,21 +30,21 @@ export class ShowInLogAnalysis { } const panel = LogView.getCurrentView(); - const logPath = LogView.getLogPath(); + const currentLogUri = LogView.getLogUri(); // If panel doesn't exist, open the log analysis view first if (!panel) { const activeEditor = window.activeTextEditor; - const logFilePath = filePath ?? activeEditor?.document.uri.fsPath; + const logUri = filePath ? Uri.parse(filePath) : activeEditor?.document.uri; - if (!logFilePath) { + if (!logUri) { context.display.showInformationMessage('No active Apex log file.'); return; } // Set pending navigation so it's sent after log is parsed LogView.setPendingNavigation(timestamp); - await LogView.createView(context, Promise.resolve(), logFilePath); + await LogView.createView(context, Promise.resolve(), logUri); return; // Navigation will happen via fetchLog payload } else { // Panel exists - reveal it first @@ -52,10 +52,14 @@ export class ShowInLogAnalysis { // Verify we're navigating to the same log const activeEditor = window.activeTextEditor; - if (logPath && activeEditor && activeEditor.document.uri.fsPath !== logPath) { + if ( + currentLogUri && + activeEditor && + activeEditor.document.uri.toString() !== currentLogUri.toString() + ) { // Different log file is active, open the active one LogView.setPendingNavigation(timestamp); - await LogView.createView(context, Promise.resolve(), activeEditor.document.uri.fsPath); + await LogView.createView(context, Promise.resolve(), activeEditor.document.uri); return; // Navigation will happen via fetchLog payload } } diff --git a/lana/src/commands/ShowLogAnalysis.ts b/lana/src/commands/ShowLogAnalysis.ts index 1f9028968..5e7344dbf 100644 --- a/lana/src/commands/ShowLogAnalysis.ts +++ b/lana/src/commands/ShowLogAnalysis.ts @@ -1,12 +1,11 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { existsSync } from 'fs'; -import type { Uri } from 'vscode'; -import { window } from 'vscode'; +import { TabInputText, window, type Uri } from 'vscode'; import { appName } from '../AppSettings.js'; import type { Context } from '../Context.js'; +import { fileOrFolderExists } from '../services/salesforceServices.js'; import { Command } from './Command.js'; import { LogView } from './LogView.js'; @@ -33,12 +32,13 @@ export class ShowLogAnalysis { } private static async command(context: Context, uri: Uri): Promise { - const filePath = uri?.fsPath || window?.activeTextEditor?.document.fileName || ''; - const fileContent = !existsSync(filePath) ? window?.activeTextEditor?.document.getText() : ''; + const activeTab = window.tabGroups.activeTabGroup.activeTab; + const logUri = + uri || + window.activeTextEditor?.document.uri || + (activeTab?.input instanceof TabInputText ? activeTab.input.uri : undefined); - if (filePath || fileContent) { - LogView.createView(context, Promise.resolve(), filePath, fileContent); - } else { + if (!logUri) { context.display.showErrorMessage( 'No file selected or the file is too large. Try again using the file explorer or text editor command.', ); @@ -46,5 +46,10 @@ export class ShowLogAnalysis { 'No file selected or the file is too large. Try again using the file explorer or text editor command.', ); } + + const fileContent = (await fileOrFolderExists(logUri)) + ? undefined + : window.activeTextEditor?.document.getText(); + await LogView.createView(context, Promise.resolve(), logUri, fileContent); } } diff --git a/lana/src/commands/__tests__/RetrieveLogFile.test.ts b/lana/src/commands/__tests__/RetrieveLogFile.test.ts index 05a2c1499..3d9cc62f1 100644 --- a/lana/src/commands/__tests__/RetrieveLogFile.test.ts +++ b/lana/src/commands/__tests__/RetrieveLogFile.test.ts @@ -66,7 +66,7 @@ const log = (id: string, startTime = '2024-01-01T00:00:00.000Z', durationMillise describe('RetrieveLogFile', () => { beforeEach(() => { jest.clearAllMocks(); - mockPickWorkspace.mockResolvedValue('/test/workspace'); + mockPickWorkspace.mockResolvedValue({ uri: 'file:///test/workspace' }); mockListLogs.mockResolvedValue([]); mockPick.mockResolvedValue([]); mockGetLogBody.mockResolvedValue('log body'); @@ -105,13 +105,13 @@ describe('RetrieveLogFile', () => { expect(mockGetLogBody).toHaveBeenCalledWith('selected-log'); expect(mockWriteFile).toHaveBeenCalledWith( - expect.stringContaining('selected-log.log'), + expect.objectContaining({ path: expect.stringContaining('selected-log.log') }), 'log body', ); expect(mockCreateView).toHaveBeenCalledWith( context, undefined, - expect.stringContaining('selected-log.log'), + expect.objectContaining({ path: expect.stringContaining('selected-log.log') }), 'log body', ); }); diff --git a/lana/src/decorations/RawLogLineDecoration.ts b/lana/src/decorations/RawLogLineDecoration.ts index daf8d264a..5a9048840 100644 --- a/lana/src/decorations/RawLogLineDecoration.ts +++ b/lana/src/decorations/RawLogLineDecoration.ts @@ -88,7 +88,7 @@ export class RawLogLineDecoration { } const timestamp = parseInt(match[1], 10); - const filePath = document.uri.fsPath; + const filePath = document.uri.toString(); const apexLog = await LogEventCache.getApexLog(filePath); if (!apexLog) { diff --git a/lana/src/display/Display.ts b/lana/src/display/Display.ts index 3a33b33d0..fe1c50fab 100644 --- a/lana/src/display/Display.ts +++ b/lana/src/display/Display.ts @@ -23,7 +23,7 @@ export class Display { window.showErrorMessage(s, options); } - showFile(path: string, options: TextDocumentShowOptions = {}): void { - commands.executeCommand('vscode.open', Uri.file(path.trim()), options); + showFile(uri: Uri | string, options: TextDocumentShowOptions = {}): void { + commands.executeCommand('vscode.open', typeof uri === 'string' ? Uri.parse(uri) : uri, options); } } diff --git a/lana/src/display/OpenFileInPackage.ts b/lana/src/display/OpenFileInPackage.ts index ab8f6ce48..482ee7479 100644 --- a/lana/src/display/OpenFileInPackage.ts +++ b/lana/src/display/OpenFileInPackage.ts @@ -1,8 +1,8 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { basename } from 'path'; import { Position, Selection, ViewColumn, workspace, type TextDocumentShowOptions } from 'vscode'; +import { Utils } from 'vscode-uri'; import type { Context } from '../Context.js'; import { getMethodLine, parseApex } from '../salesforce/ApexParser/ApexSymbolLocator.js'; @@ -31,7 +31,7 @@ export class OpenFileInPackage { if (!symbolLocation.isExactMatch) { context.display.showErrorMessage( - `Symbol '${symbolLocation.missingSymbol}' could not be found in file '${basename(uri.fsPath)}'`, + `Symbol '${symbolLocation.missingSymbol}' could not be found in file '${Utils.basename(uri)}'`, ); } const zeroIndexedLineNumber = symbolLocation.line - 1; @@ -44,7 +44,7 @@ export class OpenFileInPackage { selection: new Selection(pos, pos), }; - context.display.showFile(uri.fsPath, options); + context.display.showFile(uri, options); } catch (err) { const message = err instanceof Error ? err.message : String(err); context.display.showErrorMessage(`Unable to open '${symbolName}': ${message}`); diff --git a/lana/src/display/QuickPickWorkspace.ts b/lana/src/display/QuickPickWorkspace.ts index 0f81f10c6..16763b750 100644 --- a/lana/src/display/QuickPickWorkspace.ts +++ b/lana/src/display/QuickPickWorkspace.ts @@ -1,32 +1,47 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { parse } from 'path'; import { window } from 'vscode'; +import { Utils } from 'vscode-uri'; import type { Context } from '../Context.js'; +import { VSWorkspace } from '../workspace/VSWorkspace.js'; import { Item, Options, QuickPick } from './QuickPick.js'; export class QuickPickWorkspace { - static async pickOrReturn(context: Context): Promise { + static async pickOrReturn(context: Context): Promise { const workspaceFolders = context.workspaceManager.workspaceFolders; if (workspaceFolders.length > 1) { const [workspace] = await QuickPick.pick( - workspaceFolders.map((ws) => new Item(ws.name(), ws.path(), '')), + workspaceFolders.map((ws) => new Item(ws.name(), ws.uri, '')), new Options('Select a workspace:'), ); if (workspace) { - return workspace.description; + const selectedWorkspace = workspaceFolders.find((ws) => ws.uri === workspace.description); + if (!selectedWorkspace) { + throw new Error('Selected workspace not found'); + } + return selectedWorkspace; } else { throw new Error('No workspace selected'); } } else if (workspaceFolders.length === 1) { - return workspaceFolders[0]?.path() || ''; + const selectedWorkspace = workspaceFolders[0]; + if (!selectedWorkspace) { + throw new Error('No workspace available'); + } + return selectedWorkspace; } else { if (window.activeTextEditor) { - return parse(window.activeTextEditor.document.fileName).dir; + const documentUri = window.activeTextEditor.document.uri; + const folderUri = Utils.dirname(documentUri); + return new VSWorkspace({ + uri: folderUri, + name: Utils.basename(folderUri), + index: 0, + }); } else { throw new Error('No workspace selected'); } diff --git a/lana/src/display/__tests__/OpenFileInPackage.test.ts b/lana/src/display/__tests__/OpenFileInPackage.test.ts index d76f6ec3a..6a0aa871e 100644 --- a/lana/src/display/__tests__/OpenFileInPackage.test.ts +++ b/lana/src/display/__tests__/OpenFileInPackage.test.ts @@ -81,7 +81,7 @@ describe('OpenFileInPackage.openFileForSymbol', () => { const { context, workspaceManager, display } = createContext(); workspaceManager.findSymbol.mockResolvedValue({ status: 'found', - uri: { fsPath: '/ws/force-app/MyClass.cls' }, + uri: { path: '/ws/force-app/MyClass.cls', fsPath: '/ws/force-app/MyClass.cls' }, }); mockGetMethodLine.mockReturnValue({ line: 12, character: 4, isExactMatch: true }); @@ -93,8 +93,8 @@ describe('OpenFileInPackage.openFileForSymbol', () => { ); expect(display.showErrorMessage).not.toHaveBeenCalled(); expect(display.showFile).toHaveBeenCalledTimes(1); - const [path, options] = display.showFile.mock.calls[0]; - expect(path).toBe('/ws/force-app/MyClass.cls'); + const [uri, options] = display.showFile.mock.calls[0]; + expect(uri).toEqual(expect.objectContaining({ fsPath: '/ws/force-app/MyClass.cls' })); // line is converted to zero-indexed; character used as-is expect(options.selection.start).toEqual(expect.objectContaining({ line: 11, character: 4 })); expect(options.viewColumn).toBe(-1); @@ -104,7 +104,7 @@ describe('OpenFileInPackage.openFileForSymbol', () => { const { context, workspaceManager, display } = createContext(); workspaceManager.findSymbol.mockResolvedValue({ status: 'found', - uri: { fsPath: '/ws/MyClass.cls' }, + uri: { path: '/ws/MyClass.cls', fsPath: '/ws/MyClass.cls' }, }); mockGetMethodLine.mockReturnValue({ line: 3, isExactMatch: true }); @@ -118,7 +118,7 @@ describe('OpenFileInPackage.openFileForSymbol', () => { const { context, workspaceManager, display } = createContext(); workspaceManager.findSymbol.mockResolvedValue({ status: 'found', - uri: { fsPath: '/ws/force-app/MyClass.cls' }, + uri: { path: '/ws/force-app/MyClass.cls', fsPath: '/ws/force-app/MyClass.cls' }, }); mockGetMethodLine.mockReturnValue({ line: 1, diff --git a/lana/src/folding/RawLogFoldingProvider.ts b/lana/src/folding/RawLogFoldingProvider.ts index a2998675c..e69ac74e6 100644 --- a/lana/src/folding/RawLogFoldingProvider.ts +++ b/lana/src/folding/RawLogFoldingProvider.ts @@ -28,8 +28,7 @@ class RawLogFoldingProvider implements FoldingRangeProvider { document: TextDocument, _context: FoldingContext, ): Promise { - const filePath = document.uri.fsPath; - const apexLog = await LogEventCache.getApexLog(filePath); + const apexLog = await LogEventCache.getApexLog(document.uri.toString()); if (!apexLog) { return []; @@ -87,11 +86,11 @@ class RawLogFoldingProvider implements FoldingRangeProvider { * unrelated action forces a re-evaluation. */ private warmAndSignal(document: TextDocument): void { - if (document.uri.scheme !== 'file' || !isApexLogContent(document)) { + if (!isApexLogContent(document)) { return; } - void LogEventCache.getApexLog(document.uri.fsPath).then((apexLog) => { + void LogEventCache.getApexLog(document.uri.toString()).then((apexLog) => { if (apexLog) { this.changeEmitter.fire(); } @@ -99,7 +98,7 @@ class RawLogFoldingProvider implements FoldingRangeProvider { } static apply(context: Context): void { - const docSelector = [{ scheme: 'file', language: 'apexlog' }]; + const docSelector = [{ language: 'apexlog' }]; const provider = new RawLogFoldingProvider(); context.context.subscriptions.push( diff --git a/lana/src/folding/__tests__/RawLogFoldingProvider.test.ts b/lana/src/folding/__tests__/RawLogFoldingProvider.test.ts index 6012b0c89..610daef18 100644 --- a/lana/src/folding/__tests__/RawLogFoldingProvider.test.ts +++ b/lana/src/folding/__tests__/RawLogFoldingProvider.test.ts @@ -312,7 +312,7 @@ describe('RawLogFoldingProvider', () => { expect(languages.registerFoldingRangeProvider).toHaveBeenCalledTimes(1); expect(languages.registerFoldingRangeProvider).toHaveBeenCalledWith( - [{ scheme: 'file', language: 'apexlog' }], + [{ language: 'apexlog' }], expect.any(RawLogFoldingProvider), ); }); @@ -354,7 +354,7 @@ describe('RawLogFoldingProvider', () => { return { registeredProvider, openHandler, activeEditorHandler }; } - const flush = () => new Promise((resolve) => setImmediate(resolve)); + const flush = () => new Promise((resolve) => queueMicrotask(resolve)); it('warms the cache and fires onDidChangeFoldingRanges when an apex log opens', async () => { const { registeredProvider, openHandler } = applyAndCapture(); @@ -366,7 +366,7 @@ describe('RawLogFoldingProvider', () => { openHandler(doc); await flush(); - expect(mockGetApexLog).toHaveBeenCalledWith('/test/file.log'); + expect(mockGetApexLog).toHaveBeenCalledWith('file:///test/file.log'); expect(fired).toHaveBeenCalledTimes(1); }); @@ -380,7 +380,7 @@ describe('RawLogFoldingProvider', () => { activeEditorHandler({ document: doc }); await flush(); - expect(mockGetApexLog).toHaveBeenCalledWith('/test/file.log'); + expect(mockGetApexLog).toHaveBeenCalledWith('file:///test/file.log'); expect(fired).toHaveBeenCalledTimes(1); }); @@ -407,7 +407,7 @@ describe('RawLogFoldingProvider', () => { openHandler(doc); await flush(); - expect(mockGetApexLog).toHaveBeenCalledWith('/test/file.log'); + expect(mockGetApexLog).toHaveBeenCalledWith('file:///test/file.log'); expect(fired).not.toHaveBeenCalled(); }); }); diff --git a/lana/src/hovers/RawLogHoverProvider.ts b/lana/src/hovers/RawLogHoverProvider.ts index 8403eb510..0cd1b8dad 100644 --- a/lana/src/hovers/RawLogHoverProvider.ts +++ b/lana/src/hovers/RawLogHoverProvider.ts @@ -25,7 +25,7 @@ class RawLogHoverProvider implements HoverProvider { } const timestamp = parseInt(match[1], 10); - return this.buildHover(document.uri.fsPath, timestamp); + return this.buildHover(document.uri.toString(), timestamp); } private async buildHover(filePath: string, timestamp: number): Promise { @@ -51,7 +51,7 @@ class RawLogHoverProvider implements HoverProvider { } static apply(context: Context): void { - const docSelector = [{ scheme: 'file', language: 'apexlog' }]; + const docSelector = [{ language: 'apexlog' }]; const hoverProviderDisposable = languages.registerHoverProvider( docSelector, diff --git a/lana/src/language/ApexLogLanguageDetector.ts b/lana/src/language/ApexLogLanguageDetector.ts index b9a021172..fa0d69603 100644 --- a/lana/src/language/ApexLogLanguageDetector.ts +++ b/lana/src/language/ApexLogLanguageDetector.ts @@ -1,9 +1,6 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ -import { closeSync, openSync, readSync } from 'node:fs'; -import { extname } from 'node:path'; - import { TabInputText, commands, @@ -13,14 +10,17 @@ import { type TextDocument, type Uri, } from 'vscode'; +import { Utils } from 'vscode-uri'; import type { Context } from '../Context.js'; +import { readFile } from '../services/salesforceServices.js'; export const APEXLOG_HEADER = /^(\d\d\.\d.+?)?APEX_CODE,\w.+$/; const EXECUTION_STARTED = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|EXECUTION_STARTED$/; const USER_INFO = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|USER_INFO\|/; const DETECT_EXTENSIONS = new Set(['.log', '.txt']); const MAX_LINES_TO_CHECK = 100; +export const APEX_LOG_URI_SCHEMES = ['file', 'vscode-vfs', 'memfs'] as const; export function isApexLogContent(doc: TextDocument): boolean { if (doc.lineCount === 0) { @@ -38,18 +38,9 @@ export function isApexLogContent(doc: TextDocument): boolean { return false; } -function isApexLogFile(fsPath: string): boolean { - let fd: number; +async function isApexLogFile(uri: Uri): Promise { try { - fd = openSync(fsPath, 'r'); - } catch { - return false; - } - - try { - const buf = Buffer.alloc(4096); - const bytesRead = readSync(fd, buf, 0, 4096, 0); - const text = buf.toString('utf8', 0, bytesRead); + const text = (await readFile(uri)).slice(0, 4096); const lines = text.split(/\r?\n/); const linesToCheck = Math.min(MAX_LINES_TO_CHECK, lines.length); @@ -60,13 +51,13 @@ function isApexLogFile(fsPath: string): boolean { } } return false; - } finally { - closeSync(fd); + } catch { + return false; } } function hasDetectExtension(uri: Uri): boolean { - return DETECT_EXTENSIONS.has(extname(uri.fsPath).toLowerCase()); + return DETECT_EXTENSIONS.has(Utils.extname(uri).toLowerCase()); } function getActiveTabUri(): Uri | undefined { @@ -79,7 +70,12 @@ function getActiveTabUri(): Uri | undefined { function updateContextKey(): void { const editor = window.activeTextEditor; - if (editor && editor.document.uri.scheme === 'file') { + if ( + editor && + APEX_LOG_URI_SCHEMES.includes( + editor.document.uri.scheme as (typeof APEX_LOG_URI_SCHEMES)[number], + ) + ) { const doc = editor.document; if (hasDetectExtension(doc.uri)) { const detected = isApexLogContent(doc); @@ -92,9 +88,14 @@ function updateContextKey(): void { // Fallback to tab API for large files where activeTextEditor is undefined const tabUri = getActiveTabUri(); - if (tabUri && tabUri.scheme === 'file' && hasDetectExtension(tabUri)) { - const detected = isApexLogFile(tabUri.fsPath); - commands.executeCommand('setContext', 'lana.isApexLog', detected); + if ( + tabUri && + APEX_LOG_URI_SCHEMES.includes(tabUri.scheme as (typeof APEX_LOG_URI_SCHEMES)[number]) && + hasDetectExtension(tabUri) + ) { + void isApexLogFile(tabUri).then((detected) => { + commands.executeCommand('setContext', 'lana.isApexLog', detected); + }); return; } @@ -132,7 +133,10 @@ export class ApexLogLanguageDetector { } function detectAndSetLanguage(doc: TextDocument): void { - if (doc.languageId === 'apexlog' || doc.uri.scheme !== 'file') { + if ( + doc.languageId === 'apexlog' || + !APEX_LOG_URI_SCHEMES.includes(doc.uri.scheme as (typeof APEX_LOG_URI_SCHEMES)[number]) + ) { return; } diff --git a/lana/src/log-features/RawLogNavigation.ts b/lana/src/log-features/RawLogNavigation.ts index b14eba9b0..c37f84967 100644 --- a/lana/src/log-features/RawLogNavigation.ts +++ b/lana/src/log-features/RawLogNavigation.ts @@ -1,7 +1,9 @@ /* * Copyright (c) 2020 Certinia Inc. All rights reserved. */ -import { Selection, Uri, commands, window, workspace } from 'vscode'; +import { Selection, commands, window, type Uri } from 'vscode'; + +import { readFile } from '../services/salesforceServices.js'; /** * Handles navigation within raw Apex log files. @@ -15,12 +17,10 @@ export class RawLogNavigation { * @param logPath - Path to the log file * @param timestamp - Nanosecond timestamp to find (from log event) */ - public static async goToLineByTimestamp(logPath: string, timestamp: number): Promise { + public static async goToLineByTimestamp(logUri: Uri, timestamp: number): Promise { try { - const uri = Uri.file(logPath); - // Read file (no normalization - avoids doubling memory for large files) - const text = new TextDecoder().decode(await workspace.fs.readFile(uri)); + const text = await readFile(logUri); // Find the exact timestamp pattern: (nanoseconds)| const index = text.indexOf(`(${timestamp})|`); @@ -45,7 +45,7 @@ export class RawLogNavigation { } // Open file with line selected (cursor ends up at end - VS Code limitation) - await commands.executeCommand('vscode.open', uri, { + await commands.executeCommand('vscode.open', logUri, { preview: false, selection: new Selection(lineNumber, 0, lineNumber, lineLength), }); diff --git a/lana/src/salesforce/codesymbol/SfdxProject.ts b/lana/src/salesforce/codesymbol/SfdxProject.ts index 182337a2f..a46b40c20 100644 --- a/lana/src/salesforce/codesymbol/SfdxProject.ts +++ b/lana/src/salesforce/codesymbol/SfdxProject.ts @@ -1,8 +1,8 @@ /* * Copyright (c) 2025 Certinia Inc. All rights reserved. */ -import path from 'path'; import { RelativePattern, type Uri, workspace } from 'vscode'; +import { Utils } from 'vscode-uri'; export interface PackageDirectory { readonly uri: Uri; @@ -45,7 +45,9 @@ export class SfdxProject { const classIndex = new Map(); for (const uri of allUris) { // uri.path is always '/'-separated (unlike fsPath), so posix basename is safe everywhere - const className = path.posix.basename(uri.path, '.cls').toLowerCase(); + const className = Utils.basename(uri) + .replace(/\.cls$/i, '') + .toLowerCase(); const uris = classIndex.get(className); if (uris) { uris.push(uri); diff --git a/lana/src/salesforce/codesymbol/SfdxProjectReader.ts b/lana/src/salesforce/codesymbol/SfdxProjectReader.ts index 563d2dfac..5c1cc6d36 100644 --- a/lana/src/salesforce/codesymbol/SfdxProjectReader.ts +++ b/lana/src/salesforce/codesymbol/SfdxProjectReader.ts @@ -2,6 +2,7 @@ * Copyright (c) 2025 Certinia Inc. All rights reserved. */ import { RelativePattern, Uri, workspace, type WorkspaceFolder } from 'vscode'; + import { SfdxProject } from './SfdxProject.js'; interface RawPackageDirectory { @@ -45,7 +46,7 @@ export async function getProjects(workspaceFolder: WorkspaceFolder): Promise ({ path, fsPath: path }) as Uri; +const joinPath = (base: string, ...segments: string[]): string => + [base, ...segments].join('/').replace(/\/[^/]+\/\.\.\//g, '/'); + /** Mock the workspace scan so each project file resolves to its own contents, in order. */ function mockProjectFiles(files: { uri: Uri; contents: string }[]): void { (workspace.findFiles as jest.Mock).mockResolvedValue(files.map((file) => file.uri)); @@ -30,7 +32,7 @@ describe('getProjects', () => { jest.clearAllMocks(); // Mirror the real Uri.joinPath: join segments and normalize '..' (Uri.joinPath as jest.Mock).mockImplementation((base: Uri, ...segments: string[]) => - fileUri(posix.join(base.path, ...segments)), + fileUri(joinPath(base.path, ...segments)), ); }); diff --git a/lana/src/symbols/RawLogSymbolProvider.ts b/lana/src/symbols/RawLogSymbolProvider.ts index 2b11e88ab..f4ed09993 100644 --- a/lana/src/symbols/RawLogSymbolProvider.ts +++ b/lana/src/symbols/RawLogSymbolProvider.ts @@ -29,7 +29,7 @@ class RawLogSymbolProvider implements DocumentSymbolProvider { document: TextDocument, _token: CancellationToken, ): Promise { - const apexLog = await LogEventCache.getApexLog(document.uri.fsPath); + const apexLog = await LogEventCache.getApexLog(document.uri.toString()); if (!apexLog) { return []; @@ -89,7 +89,7 @@ class RawLogSymbolProvider implements DocumentSymbolProvider { } static apply(context: Context): void { - const docSelector = [{ scheme: 'file', language: 'apexlog' }]; + const docSelector = [{ language: 'apexlog' }]; context.context.subscriptions.push( languages.registerDocumentSymbolProvider(docSelector, new RawLogSymbolProvider()), diff --git a/lana/src/symbols/__tests__/RawLogSymbolProvider.test.ts b/lana/src/symbols/__tests__/RawLogSymbolProvider.test.ts index f7aad575c..1f25bd1b5 100644 --- a/lana/src/symbols/__tests__/RawLogSymbolProvider.test.ts +++ b/lana/src/symbols/__tests__/RawLogSymbolProvider.test.ts @@ -142,7 +142,7 @@ describe('RawLogSymbolProvider', () => { expect(languages.registerDocumentSymbolProvider).toHaveBeenCalledTimes(1); expect(languages.registerDocumentSymbolProvider).toHaveBeenCalledWith( - [{ scheme: 'file', language: 'apexlog' }], + [{ language: 'apexlog' }], expect.any(RawLogSymbolProvider), ); }); diff --git a/lana/src/workspace/VSWorkspace.ts b/lana/src/workspace/VSWorkspace.ts index 695e09a65..303184530 100644 --- a/lana/src/workspace/VSWorkspace.ts +++ b/lana/src/workspace/VSWorkspace.ts @@ -14,9 +14,11 @@ export class VSWorkspace { this.workspaceFolder = workspaceFolder; } - path(): string { - return this.workspaceFolder.uri.fsPath; + /** URI string for desktop and virtual web workspaces. */ + get uri(): string { + return this.workspaceFolder.uri.toString(); } + name(): string { return this.workspaceFolder.name; } diff --git a/lana/src/workspace/__tests__/VSWorkspace.test.ts b/lana/src/workspace/__tests__/VSWorkspace.test.ts index 866364b07..cbcb3b45c 100644 --- a/lana/src/workspace/__tests__/VSWorkspace.test.ts +++ b/lana/src/workspace/__tests__/VSWorkspace.test.ts @@ -24,9 +24,9 @@ describe('VSWorkspace', () => { vsWorkspace = new VSWorkspace(mockWorkspaceFolder); }); - describe('path', () => { - it('should return workspace folder path', () => { - expect(vsWorkspace.path()).toBe('/workspace'); + describe('uri', () => { + it('should expose the workspace folder URI', () => { + expect(vsWorkspace.workspaceFolder.uri.fsPath).toBe('/workspace'); }); }); diff --git a/lana/tsconfig.json b/lana/tsconfig.json index de63b3700..d7a104bbe 100644 --- a/lana/tsconfig.json +++ b/lana/tsconfig.json @@ -16,7 +16,7 @@ "moduleResolution": "Bundler", "module": "ESNext", "noEmit": true, - "types": ["jest", "node"], + "types": ["jest"], "composite": true, "declarationMap": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9dffa766..2bb11ed60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: effect: specifier: ^3.22.0 version: 3.22.1 + vscode-uri: + specifier: ^3.1.0 + version: 3.1.0 devDependencies: '@types/jest': specifier: ^30.0.0