From 447e8310f2f27cc56b687bac3de2ac22bfbccd6c Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Fri, 24 Jan 2025 16:10:41 -0600 Subject: [PATCH 1/5] Adds Data Explorer action to open as text file --- .../browser/positronDataExplorerActions.ts | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts index bc3892421e3..dc429b08254 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { IEditorPane } from '../../../common/editor.js'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, IEditorPane } from '../../../common/editor.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; @@ -20,6 +20,9 @@ import { IPositronDataExplorerService, PositronDataExplorerLayout } from '../../ import { PositronDataExplorerEditorInput } from './positronDataExplorerEditorInput.js'; import { POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING, POSITRON_DATA_EXPLORER_LAYOUT } from './positronDataExplorerContextKeys.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { PositronDataExplorerUri } from '../../../services/positronDataExplorer/common/positronDataExplorerUri.js'; +import { URI } from '../../../../base/common/uri.js'; +import { EditorOpenSource } from '../../../../platform/editor/common/editor.js'; /** * Positron data explorer action category. @@ -47,7 +50,8 @@ export const enum PositronDataExplorerCommandId { ExpandSummaryAction = 'workbench.action.positronDataExplorer.expandSummary', SummaryOnLeftAction = 'workbench.action.positronDataExplorer.summaryOnLeft', SummaryOnRightAction = 'workbench.action.positronDataExplorer.summaryOnRight', - ClearColumnSortingAction = 'workbench.action.positronDataExplorer.clearColumnSorting' + ClearColumnSortingAction = 'workbench.action.positronDataExplorer.clearColumnSorting', + OpenAsPlaintext = 'workbench.action.positronDataExplorer.openAsPlaintext' } /** @@ -679,6 +683,59 @@ class PositronDataExplorerClearColumnSortingAction extends Action2 { } } +/** + * PositronDataExplorerOpenAsPlaintextAction action. + */ +class PositronDataExplorerOpenAsPlaintextAction extends Action2 { + /** + * Constructor. + */ + constructor() { + super({ + id: PositronDataExplorerCommandId.OpenAsPlaintext, + title: { + value: localize('positronDataExplorer.openAsPlaintext', 'Open as Plain Text File'), + original: 'Open as Plain Text File' + }, + displayTitleOnActionBar: true, + category, + f1: true, + precondition: ContextKeyExpr.and( + POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR + ), + icon: Codicon.fileText, + menu: [ + { + id: MenuId.EditorActionsLeft, + when: POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + }, + { + id: MenuId.EditorTitle, + group: 'navigation', + when: POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + } + ] + }); + } + + /** + * Runs the action. + * @param accessor The services accessor. + */ + async run(accessor: ServicesAccessor): Promise { + // Access the services we need. + // const textEditorService = accessor.get(ITextEditorService); + const editorService = accessor.get(IEditorService); + const dataExplorerUri = URI.parse(PositronDataExplorerUri.parse(EditorResourceAccessor.getOriginalUri(editorService.activeEditor)!)!); + + if (dataExplorerUri.scheme !== 'duckdb') { + return; + } + + await editorService.openEditor({ resource: URI.parse(dataExplorerUri.fsPath), options: { override: DEFAULT_EDITOR_ASSOCIATION.id, source: EditorOpenSource.USER } }); + } +} + /** * Registers Positron data explorer actions. */ @@ -690,4 +747,5 @@ export function registerPositronDataExplorerActions() { registerAction2(PositronDataExplorerSummaryOnLeftAction); registerAction2(PositronDataExplorerSummaryOnRightAction); registerAction2(PositronDataExplorerClearColumnSortingAction); + registerAction2(PositronDataExplorerOpenAsPlaintextAction); } From f582e2c220ea2f6515e79200ef9cb919c558bffc Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Mon, 27 Jan 2025 14:14:07 -0600 Subject: [PATCH 2/5] Updates Data Explorer plaintext option to only open .csv and .tsv files --- .../browser/positronDataExplorerActions.ts | 10 +++----- .../positronDataExplorerContextKeys.ts | 4 ++++ .../browser/positronDataExplorerEditor.tsx | 24 ++++++++++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts index dc429b08254..ff7a2b613c3 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts @@ -18,7 +18,7 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { IPositronDataExplorerEditor } from './positronDataExplorerEditor.js'; import { IPositronDataExplorerService, PositronDataExplorerLayout } from '../../../services/positronDataExplorer/browser/interfaces/positronDataExplorerService.js'; import { PositronDataExplorerEditorInput } from './positronDataExplorerEditorInput.js'; -import { POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING, POSITRON_DATA_EXPLORER_LAYOUT } from './positronDataExplorerContextKeys.js'; +import { POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING, POSITRON_DATA_EXPLORER_IS_PLAINTEXT, POSITRON_DATA_EXPLORER_LAYOUT } from './positronDataExplorerContextKeys.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { PositronDataExplorerUri } from '../../../services/positronDataExplorer/common/positronDataExplorerUri.js'; import { URI } from '../../../../base/common/uri.js'; @@ -701,7 +701,8 @@ class PositronDataExplorerOpenAsPlaintextAction extends Action2 { category, f1: true, precondition: ContextKeyExpr.and( - POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR + POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + POSITRON_DATA_EXPLORER_IS_PLAINTEXT ), icon: Codicon.fileText, menu: [ @@ -727,11 +728,6 @@ class PositronDataExplorerOpenAsPlaintextAction extends Action2 { // const textEditorService = accessor.get(ITextEditorService); const editorService = accessor.get(IEditorService); const dataExplorerUri = URI.parse(PositronDataExplorerUri.parse(EditorResourceAccessor.getOriginalUri(editorService.activeEditor)!)!); - - if (dataExplorerUri.scheme !== 'duckdb') { - return; - } - await editorService.openEditor({ resource: URI.parse(dataExplorerUri.fsPath), options: { override: DEFAULT_EDITOR_ASSOCIATION.id, source: EditorOpenSource.USER } }); } } diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerContextKeys.ts b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerContextKeys.ts index f48648fa95c..f358a402343 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerContextKeys.ts +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerContextKeys.ts @@ -26,3 +26,7 @@ export const POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING = new RawContextKey( + 'positronDataExplorerIsPlaintext', + false +); diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx index 0694299ac13..4da426c968d 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerEditor.tsx @@ -35,7 +35,8 @@ import { PositronDataExplorerUri } from '../../../services/positronDataExplorer/ import { IPositronDataExplorerService, PositronDataExplorerLayout } from '../../../services/positronDataExplorer/browser/interfaces/positronDataExplorerService.js'; import { PositronDataExplorerEditorInput } from './positronDataExplorerEditorInput.js'; import { PositronDataExplorerClosed, PositronDataExplorerClosedStatus } from '../../../browser/positronDataExplorer/components/dataExplorerClosed/positronDataExplorerClosed.js'; -import { POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING, POSITRON_DATA_EXPLORER_LAYOUT } from './positronDataExplorerContextKeys.js'; +import { POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING, POSITRON_DATA_EXPLORER_IS_PLAINTEXT, POSITRON_DATA_EXPLORER_LAYOUT } from './positronDataExplorerContextKeys.js'; +import { URI } from '../../../../base/common/uri.js'; /** * IPositronDataExplorerEditorOptions interface. @@ -96,6 +97,11 @@ export class PositronDataExplorerEditor extends EditorPane implements IPositronD */ private readonly _isColumnSortingContextKey: IContextKey; + /** + * Gets the is plaintext editable context key. + */ + private readonly _isPlaintextContextKey: IContextKey; + /** * The onSizeChanged event emitter. */ @@ -251,6 +257,9 @@ export class PositronDataExplorerEditor extends EditorPane implements IPositronD this._isColumnSortingContextKey = POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING.bindTo( this._group.scopedContextKeyService ); + this._isPlaintextContextKey = POSITRON_DATA_EXPLORER_IS_PLAINTEXT.bindTo( + this._group.scopedContextKeyService + ); } /** @@ -353,6 +362,14 @@ export class PositronDataExplorerEditor extends EditorPane implements IPositronD positronDataExplorerInstance.tableDataDataGridInstance.isColumnSorting ); + const uri = URI.parse(this._identifier); + if (uri.scheme === 'duckdb') { + this._isPlaintextContextKey.set(PLAINTEXT_EXTS.some(ext => uri.path.endsWith(ext))); + } else { + this._isPlaintextContextKey.reset(); + } + + // Render the PositronDataExplorer. this._positronReactRenderer.render( Date: Mon, 27 Jan 2025 15:20:41 -0600 Subject: [PATCH 3/5] Updated Data Explorer to hide plaintext button when unavailable --- .../browser/positronDataExplorerActions.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts index ff7a2b613c3..eaa88c20648 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts @@ -708,12 +708,18 @@ class PositronDataExplorerOpenAsPlaintextAction extends Action2 { menu: [ { id: MenuId.EditorActionsLeft, - when: POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + when: ContextKeyExpr.and( + POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + POSITRON_DATA_EXPLORER_IS_PLAINTEXT + ) }, { id: MenuId.EditorTitle, group: 'navigation', - when: POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + when: ContextKeyExpr.and( + POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + POSITRON_DATA_EXPLORER_IS_PLAINTEXT + ) } ] }); From cce1e1f64de522acbe7b099fb27cd1cec878a539 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Tue, 28 Jan 2025 14:06:26 -0600 Subject: [PATCH 4/5] Adds e2e tests for "Open as Plaintext" in Data Explorer --- test/e2e/tests/data-explorer/data-explorer-headless.test.ts | 6 ++++++ test/e2e/tests/data-explorer/helpers/100x100.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/test/e2e/tests/data-explorer/data-explorer-headless.test.ts b/test/e2e/tests/data-explorer/data-explorer-headless.test.ts index ed045356487..286824c59c4 100644 --- a/test/e2e/tests/data-explorer/data-explorer-headless.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-headless.test.ts @@ -62,5 +62,11 @@ async function testBody(app: Application, logger: Logger, fileName: string) { const lastRow = tableData.at(-1); const lastHour = lastRow!['time_hour']; expect(lastHour).toBe(LAST_CELL_CONTENTS); + + // If file is plaintext (csv, tsv), check for the plaintext button in the actiobar + // Otherwise, ensure the button is not present + const shouldHavePlaintext = fileName.endsWith('.csv') || fileName.endsWith('.tsv'); + const plaintextEl = app.code.driver.page.getByLabel('Open as Plain Text File'); + expect(await plaintextEl.isVisible()).toBe(shouldHavePlaintext); }).toPass(); } diff --git a/test/e2e/tests/data-explorer/helpers/100x100.ts b/test/e2e/tests/data-explorer/helpers/100x100.ts index 5777224f7f4..cad79afe63b 100644 --- a/test/e2e/tests/data-explorer/helpers/100x100.ts +++ b/test/e2e/tests/data-explorer/helpers/100x100.ts @@ -98,6 +98,10 @@ export const testDataExplorer = async ( // Return to Stacked layout await app.workbench.layouts.enterLayout('stacked'); + + // Check that "open as plaintext" button is not available + const plaintextEl = app.code.driver.page.getByLabel('Open as Plain Text File'); + expect(await plaintextEl.isVisible()).toBe(false); }; export const parquetFilePath = (app: Application) => { From 1cafce09270ead4e732e5b44582b63c9eda76a18 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Tue, 28 Jan 2025 14:49:22 -0600 Subject: [PATCH 5/5] Adds e2e test to ensure Data Explorer plaintext function operates as expected --- .../data-explorer-headless.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/e2e/tests/data-explorer/data-explorer-headless.test.ts b/test/e2e/tests/data-explorer/data-explorer-headless.test.ts index 286824c59c4..3e7e76fd57b 100644 --- a/test/e2e/tests/data-explorer/data-explorer-headless.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-headless.test.ts @@ -41,6 +41,20 @@ test.describe('Headless Data Explorer - Large Data Frame', { test('Verifies headless data explorer functionality with large gzipped tsv file', async function ({ app, logger }) { await testBody(app, logger, 'flights.tsv.gz'); }); + + test('Verifies headless data explorer can open csv file as plaintext', async function ({ app, logger }) { + const fileName = 'flights.csv'; + const searchString = ',year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,carrier,flight,tailnum,origin,dest,air_time,distance,hour,minute,time_hour'; + + await openAsPlaintext(app, fileName, searchString); + }); + + test('Verifies headless data explorer can open tsv file as plaintext', async function ({ app, logger }) { + const fileName = 'flights.tsv'; + const searchString = /\s+year\s+month\s+day\s+dep_time\s+sched_dep_time\s+dep_delay\s+arr_time\s+sched_arr_time\s+arr_delay\s+carrier\s+flight\s+tailnum\s+origin\s+dest\s+air_time\s+distance\s+hour\s+minute\s+time_hour/; + + await openAsPlaintext(app, fileName, searchString); + }); }); async function testBody(app: Application, logger: Logger, fileName: string) { @@ -70,3 +84,15 @@ async function testBody(app: Application, logger: Logger, fileName: string) { expect(await plaintextEl.isVisible()).toBe(shouldHavePlaintext); }).toPass(); } + +async function openAsPlaintext(app: Application, fileName: string, searchString: string | RegExp) { + await app.workbench.quickaccess.openDataFile(join(app.workspacePathOrFolder, 'data-files', 'flights', fileName)); + await app.workbench.quickaccess.runCommand('workbench.action.positronDataExplorer.openAsPlaintext'); + await app.workbench.editor.waitForEditorContents(fileName, (contents) => { + if (searchString instanceof RegExp) { + return contents.search(searchString) !== -1; + } else { + return contents.includes(searchString); + } + }); +}