Skip to content

Commit

Permalink
Adds UI to open Data Explorer files as plaintext (#6132)
Browse files Browse the repository at this point in the history
Adds UI affordance in Data Explorer toolbar to open as text file. The
button is available only when *.[csv/tsv] files are open (and disabled
when binary files or runtime variables are displayed).

Addresses #5206
samclark2015 authored Jan 30, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 379a3f4 commit a6a935f
Showing 5 changed files with 126 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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';
@@ -18,8 +18,11 @@ 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';
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,61 @@ 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,
POSITRON_DATA_EXPLORER_IS_PLAINTEXT
),
icon: Codicon.fileText,
menu: [
{
id: MenuId.EditorActionsLeft,
when: ContextKeyExpr.and(
POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR,
POSITRON_DATA_EXPLORER_IS_PLAINTEXT
)
},
{
id: MenuId.EditorTitle,
group: 'navigation',
when: ContextKeyExpr.and(
POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR,
POSITRON_DATA_EXPLORER_IS_PLAINTEXT
)
}
]
});
}

/**
* Runs the action.
* @param accessor The services accessor.
*/
async run(accessor: ServicesAccessor): Promise<void> {
// 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)!)!);
await editorService.openEditor({ resource: URI.parse(dataExplorerUri.fsPath), options: { override: DEFAULT_EDITOR_ASSOCIATION.id, source: EditorOpenSource.USER } });
}
}

/**
* Registers Positron data explorer actions.
*/
@@ -690,4 +749,5 @@ export function registerPositronDataExplorerActions() {
registerAction2(PositronDataExplorerSummaryOnLeftAction);
registerAction2(PositronDataExplorerSummaryOnRightAction);
registerAction2(PositronDataExplorerClearColumnSortingAction);
registerAction2(PositronDataExplorerOpenAsPlaintextAction);
}
Original file line number Diff line number Diff line change
@@ -26,3 +26,7 @@ export const POSITRON_DATA_EXPLORER_IS_COLUMN_SORTING = new RawContextKey<boolea
'positronDataExplorerIsColumnSorting',
false
);
export const POSITRON_DATA_EXPLORER_IS_PLAINTEXT = new RawContextKey<boolean>(
'positronDataExplorerIsPlaintext',
false
);
Original file line number Diff line number Diff line change
@@ -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<boolean>;

/**
* Gets the is plaintext editable context key.
*/
private readonly _isPlaintextContextKey: IContextKey<boolean>;

/**
* 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(
<PositronDataExplorer
@@ -493,3 +510,8 @@ export class PositronDataExplorerEditor extends EditorPane implements IPositronD

//#endregion Private Methods
}

const PLAINTEXT_EXTS = [
".csv",
".tsv"
]
32 changes: 32 additions & 0 deletions test/e2e/tests/data-explorer/data-explorer-headless.test.ts
Original file line number Diff line number Diff line change
@@ -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) {
@@ -62,5 +76,23 @@ 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();
}

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);
}
});
}
4 changes: 4 additions & 0 deletions test/e2e/tests/data-explorer/helpers/100x100.ts
Original file line number Diff line number Diff line change
@@ -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) => {

0 comments on commit a6a935f

Please sign in to comment.