Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add arrows selection to typeahead #3386

Merged
merged 11 commits into from
May 4, 2017
61 changes: 61 additions & 0 deletions packages/jest-cli/src/PatternPrompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/

'use strict';

import type {ScrollOptions} from './lib/scrollList';

const chalk = require('chalk');
const ansiEscapes = require('ansi-escapes');
const Prompt = require('./lib/Prompt');

const usage = (entity: string) =>
`\n${chalk.bold('Pattern Mode Usage')}\n` +
` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` +
` ${chalk.dim('\u203A Press')} Enter ` +
`${chalk.dim(`to apply pattern to all ${entity}.`)}\n` +
`\n`;

const usageRows = usage('').split('\n').length;

module.exports = class PatternPrompt {
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
_entityName: string;
_currentUsageRows: number;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
this._currentUsageRows = usageRows;
}

run(onSuccess: Function, onCancel: Function, options?: {header: string}) {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(ansiEscapes.clearScreen);

if (options && options.header) {
this._pipe.write(options.header + '\n');
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {
this._currentUsageRows = usageRows;
}

this._pipe.write(usage(this._entityName));
this._pipe.write(ansiEscapes.cursorShow);

this._prompt.enter(this._onChange.bind(this), onSuccess, onCancel);
}

_onChange(pattern: string, options: ScrollOptions) {
this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
}
};
109 changes: 35 additions & 74 deletions packages/jest-cli/src/TestNamePatternPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,106 +11,67 @@
'use strict';

import type {TestResult} from 'types/TestResult';
import type {ScrollOptions} from './lib/scrollList';

const ansiEscapes = require('ansi-escapes');
const chalk = require('chalk');
const scroll = require('./lib/scrollList');
const {getTerminalWidth} = require('./lib/terminalUtils');
const stringLength = require('string-length');
const Prompt = require('./lib/Prompt');
const formatTestNameByPattern = require('./lib/formatTestNameByPattern');

const pluralizeTest = (total: number) => (total === 1 ? 'test' : 'tests');

const usage = () =>
`\n${chalk.bold('Pattern Mode Usage')}\n` +
` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` +
` ${chalk.dim('\u203A Press')} Enter ` +
`${chalk.dim('to apply pattern to all tests.')}\n` +
`\n`;

const usageRows = usage().split('\n').length;

module.exports = class TestNamePatternPrompt {
const {
formatTypeaheadSelection,
printMore,
printPatternCaret,
printPatternMatches,
printRestoredPatternCaret,
printStartTyping,
printTypeaheadItem,
} = require('./lib/patternModeHelpers');
const PatternPrompt = require('./PatternPrompt');

module.exports = class TestNamePatternPrompt extends PatternPrompt {
_cachedTestResults: Array<TestResult>;
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
_currentUsageRows: number;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
this._currentUsageRows = usageRows;
super(pipe, prompt);
this._entityName = 'tests';
}

run(onSuccess: Function, onCancel: Function, options?: {header: string}) {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(ansiEscapes.clearScreen);
if (options && options.header) {
this._pipe.write(options.header + '\n');
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {
this._currentUsageRows = usageRows;
}
this._pipe.write(usage());
this._pipe.write(ansiEscapes.cursorShow);

this._prompt.enter(this._onChange.bind(this), onSuccess, onCancel);
_onChange(pattern: string, options: ScrollOptions) {
super._onChange(pattern, options);
this._printTypeahead(pattern, options);
}

_onChange(pattern: string) {
this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
this._printTypeahead(pattern, 10);
}

_printTypeahead(pattern: string, max: number) {
_printTypeahead(pattern: string, options: ScrollOptions) {
const {max} = options;
const matchedTests = this._getMatchedTests(pattern);

const total = matchedTests.length;
const results = matchedTests.slice(0, max);
const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`;
const pipe = this._pipe;
const prompt = this._prompt;

this._pipe.write(ansiEscapes.eraseDown);
this._pipe.write(inputText);
this._pipe.write(ansiEscapes.cursorSavePosition);
printPatternCaret(pattern, pipe);

if (pattern) {
if (total) {
this._pipe.write(
`\n\n Pattern matches ${total} ${pluralizeTest(total)}`,
);
} else {
this._pipe.write(`\n\n Pattern matches no tests`);
}

this._pipe.write(' from cached test suites.');
printPatternMatches(total, 'test', pipe, ` from cached test suites`);

const width = getTerminalWidth();
const {start, end, index} = scroll(total, options);

results.forEach(name => {
const testName = formatTestNameByPattern(name, pattern, width - 4);
prompt.setTypeaheadLength(total);

this._pipe.write(`\n ${chalk.dim('\u203A')} ${testName}`);
});
matchedTests
.slice(start, end)
.map(name => formatTestNameByPattern(name, pattern, width - 4))
.map((item, i) => formatTypeaheadSelection(item, i, index, prompt))
.forEach(item => printTypeaheadItem(item, pipe));

if (total > max) {
const more = total - max;
this._pipe.write(
// eslint-disable-next-line max-len
`\n ${chalk.dim(`\u203A and ${more} more ${pluralizeTest(more)}`)}`,
);
printMore('test', pipe, total - max);
}
} else {
this._pipe.write(
// eslint-disable-next-line max-len
`\n\n ${chalk.italic.yellow('Start typing to filter by a test name regex pattern.')}`,
);
printStartTyping('test name', pipe);
}

this._pipe.write(
ansiEscapes.cursorTo(stringLength(inputText), this._currentUsageRows - 1),
);
this._pipe.write(ansiEscapes.cursorRestorePosition);
printRestoredPatternCaret(pattern, this._currentUsageRows, pipe);
}

_getMatchedTests(pattern: string) {
Expand Down
137 changes: 54 additions & 83 deletions packages/jest-cli/src/TestPathPatternPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,101 +12,66 @@

import type {Context} from 'types/Context';
import type {Test} from 'types/TestRunner';
import type {ScrollOptions} from './lib/scrollList';
import type SearchSource from './SearchSource';

const ansiEscapes = require('ansi-escapes');
const chalk = require('chalk');
const scroll = require('./lib/scrollList');
const {getTerminalWidth} = require('./lib/terminalUtils');
const highlight = require('./lib/highlight');
const stringLength = require('string-length');
const {trimAndFormatPath} = require('./reporters/utils');
const Prompt = require('./lib/Prompt');
const {
formatTypeaheadSelection,
printMore,
printPatternCaret,
printPatternMatches,
printRestoredPatternCaret,
printStartTyping,
printTypeaheadItem,
} = require('./lib/patternModeHelpers');
const PatternPrompt = require('./PatternPrompt');

type SearchSources = Array<{|
context: Context,
searchSource: SearchSource,
|}>;

const pluralizeFile = (total: number) => (total === 1 ? 'file' : 'files');

const usage = () =>
`\n${chalk.bold('Pattern Mode Usage')}\n` +
` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` +
` ${chalk.dim('\u203A Press')} Enter ` +
`${chalk.dim('to apply pattern to all filenames.')}\n` +
`\n`;

const usageRows = usage().split('\n').length;

module.exports = class TestPathPatternPrompt {
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
module.exports = class TestPathPatternPrompt extends PatternPrompt {
_searchSources: SearchSources;
_currentUsageRows: number;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
this._currentUsageRows = usageRows;
}

run(onSuccess: Function, onCancel: Function, options?: {header: string}) {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(ansiEscapes.clearScreen);
if (options && options.header) {
this._pipe.write(options.header + '\n');
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {
this._currentUsageRows = usageRows;
}
this._pipe.write(usage());
this._pipe.write(ansiEscapes.cursorShow);

this._prompt.enter(this._onChange.bind(this), onSuccess, onCancel);
super(pipe, prompt);
this._entityName = 'filenames';
}

_onChange(pattern: string) {
let regex;

try {
regex = new RegExp(pattern, 'i');
} catch (e) {}

let tests = [];
if (regex) {
this._searchSources.forEach(({searchSource, context}) => {
tests = tests.concat(searchSource.findMatchingTests(pattern).tests);
});
}

this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
this._printTypeahead(pattern, tests, 10);
_onChange(pattern: string, options: ScrollOptions) {
super._onChange(pattern, options);
this._printTypeahead(pattern, options);
}

_printTypeahead(pattern: string, allResults: Array<Test>, max: number) {
const total = allResults.length;
const results = allResults.slice(0, max);
const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`;
_printTypeahead(pattern: string, options: ScrollOptions) {
const {max} = options;
const matchedTests = this._getMatchedTests(pattern);
const total = matchedTests.length;
const pipe = this._pipe;
const prompt = this._prompt;

this._pipe.write(ansiEscapes.eraseDown);
this._pipe.write(inputText);
this._pipe.write(ansiEscapes.cursorSavePosition);
printPatternCaret(pattern, pipe);

if (pattern) {
if (total) {
this._pipe.write(
`\n\n Pattern matches ${total} ${pluralizeFile(total)}.`,
);
} else {
this._pipe.write(`\n\n Pattern matches no files.`);
}
printPatternMatches(total, 'file', pipe);

const width = getTerminalWidth();
const prefix = ` ${chalk.dim('\u203A')} `;
const padding = stringLength(prefix) + 2;
const width = getTerminalWidth();
const {start, end, index} = scroll(total, options);

prompt.setTypeaheadLength(total);

results
matchedTests
.slice(start, end)
.map(({path, context}) => {
const filePath = trimAndFormatPath(
padding,
Expand All @@ -116,28 +81,34 @@ module.exports = class TestPathPatternPrompt {
);
return highlight(path, filePath, pattern, context.config.rootDir);
})
.forEach(filePath =>
this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`),
);
.map((item, i) => formatTypeaheadSelection(item, i, index, prompt))
.forEach(item => printTypeaheadItem(item, pipe));

if (total > max) {
const more = total - max;
this._pipe.write(
// eslint-disable-next-line max-len
`\n ${chalk.dim(`\u203A and ${more} more ${pluralizeFile(more)}`)}`,
);
printMore('file', pipe, total - max);
}
} else {
this._pipe.write(
// eslint-disable-next-line max-len
`\n\n ${chalk.italic.yellow('Start typing to filter by a filename regex pattern.')}`,
);
printStartTyping('filename', pipe);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could use this._entityName, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, nevermind, it's more of a coincidence that they line up. Let's keep it this way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that I'm mixing singular with plural in couple of places, need to fix it properly.

}

printRestoredPatternCaret(pattern, this._currentUsageRows, pipe);
}

_getMatchedTests(pattern: string): Array<Test> {
let regex;

try {
regex = new RegExp(pattern, 'i');
} catch (e) {}

let tests = [];
if (regex) {
this._searchSources.forEach(({searchSource, context}) => {
tests = tests.concat(searchSource.findMatchingTests(pattern).tests);
});
}

this._pipe.write(
ansiEscapes.cursorTo(stringLength(inputText), this._currentUsageRows - 1),
);
this._pipe.write(ansiEscapes.cursorRestorePosition);
return tests;
}

updateSearchSources(searchSources: SearchSources) {
Expand Down
Loading