diff --git a/src/Parser.ts b/src/Parser.ts index acdd1015c..d3816f939 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -124,6 +124,19 @@ export const choice = (parsers: Parser[]) => async (context: Context): Promise async (context: Context): Promise> => { + const results = await parser(context); + return results.slice(results.length - 1); +}; + +const shortCircuitOnEmptyInput = (parser: Parser): Parser => async(context: Context): Promise> => { + if (context.input.length === 0) { + return []; + } + + return parser(context); +}; + export const many1 = (parser: Parser): Parser => choice([parser, bind(parser, async () => many1(parser))]); export const decorate = (parser: Parser, decorator: (s: Suggestion) => Suggestion) => async (context: Context): Promise> => { @@ -153,7 +166,7 @@ export const many = compose(many1, optional); * @example cd ../ * @example cd - */ -export const noisySuggestions = (parser: Parser) => decorateResult( +export const noisySuggestions = (parser: Parser) => shortCircuitOnEmptyInput(decorateResult( parser, result => Object.assign( {}, @@ -162,8 +175,8 @@ export const noisySuggestions = (parser: Parser) => decorateResult( suggestions: (result.progress !== Progress.OnStart && result.progress !== Progress.Failed) ? result.suggestions : [], } ) -); -export const spacesWithoutSuggestion = withoutSuggestions(many1(string(" "))); +)); +export const spacesWithoutSuggestion = withoutSuggestions(last(many1(string(" ")))); export const runtime = (producer: (context: Context) => Promise) => async (context: Context): Promise> => { const parser = await producer(context); diff --git a/src/plugins/autocompletion_providers/Cd.ts b/src/plugins/autocompletion_providers/Cd.ts index ed798bcb4..da6a2a6a7 100644 --- a/src/plugins/autocompletion_providers/Cd.ts +++ b/src/plugins/autocompletion_providers/Cd.ts @@ -1,41 +1,27 @@ import {executable, sequence, decorate, string, noisySuggestions, runtime, choice} from "../../Parser"; -import {directoryAlias, pathPart} from "./Common"; import {expandHistoricalDirectory} from "../../Command"; import {description, styles, style} from "./Suggestions"; import * as _ from "lodash"; import {relativeDirectoryPath} from "./File"; +import {pathIn} from "./Common"; const historicalDirectory = runtime(async (context) => - noisySuggestions( - decorate( - choice( - _.take(["-", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9"], context.historicalCurrentDirectoriesStack.length - 1) - .map(alias => decorate(string(alias), description(expandHistoricalDirectory(alias, context.historicalCurrentDirectoriesStack)))) - ), - style(styles.directory) - ) + decorate( + choice( + _.take(["-", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9"], context.historicalCurrentDirectoriesStack.length - 1) + .map(alias => decorate(string(alias), description(expandHistoricalDirectory(alias, context.historicalCurrentDirectoriesStack)))) + ), + style(styles.directory) ) ); const cdpathDirectory = runtime( - async (context) => { - const directoriesToBe = context.environment.cdpath(context.directory).map(async (directory) => { - const file = await pathPart(directory, info => info.stat.isDirectory()); - - if (directory === context.directory) { - return file; - } else { - return noisySuggestions(decorate(file, description(`In ${directory}`))); - } - }); - - return choice(await Promise.all(directoriesToBe)); - } + async (context) => choice(context.environment.cdpath(context.directory).filter(directory => directory !== context.directory).map(directory => + decorate(pathIn(directory, info => info.stat.isDirectory()), description(`In ${directory}`)))) ); export const cd = sequence(decorate(executable("cd"), description("Change the working directory")), choice([ - historicalDirectory, - directoryAlias, - cdpathDirectory, + noisySuggestions(historicalDirectory), + noisySuggestions(cdpathDirectory), relativeDirectoryPath, ])); diff --git a/src/plugins/autocompletion_providers/Common.ts b/src/plugins/autocompletion_providers/Common.ts index c25f323d9..ec0cab353 100644 --- a/src/plugins/autocompletion_providers/Common.ts +++ b/src/plugins/autocompletion_providers/Common.ts @@ -2,46 +2,46 @@ import {statsIn, resolveDirectory} from "../../utils/Common"; import { string, choice, - append, - decorateResult, decorate, noisySuggestions, Parser, - withoutSuggestions, runtime, many1, + withoutSuggestions, runtime, sequence, } from "../../Parser"; import {styles, style} from "./Suggestions"; import {FileInfo} from "../../Interfaces"; -const changingContextDirectory = (parser: Parser) => decorateResult( - parser, - result => Object.assign({}, result, {context: Object.assign({}, result.context, {directory: resolveDirectory(result.context.directory, result.parse)})}) -); +type FileFilter = (info: FileInfo) => boolean; -export const directoryAlias = noisySuggestions( - choice([ - changingContextDirectory(withoutSuggestions(string("/"))), - append("/", choice(["~", "..", "."].map(directory => changingContextDirectory(string(directory))))), - ]) +const pathParser = (name: string) => { + const parser = name.startsWith(".") ? noisySuggestions(string(name)) : string(name); + return decorate(parser, suggestion => suggestion.withDisplayValue(name).withValue(suggestion.value.replace(/\s/g, "\\ "))); +}; +const fileParser = (info: FileInfo) => decorate(pathParser(info.name), style(styles.file(info))); +const directoryParser = (name: string) => decorate(pathParser(name), style(styles.directory)); +const directoryAlias = (workingDirectory: string, filter: FileFilter) => (name: string) => sequence( + withoutSuggestions(directoryParser(name)), + pathIn(resolveDirectory(workingDirectory, name), filter) ); -const fileName = (name: string) => decorate(string(name), suggestion => suggestion.withDisplayValue(name).withValue(suggestion.value.replace(/\s/g, "\\ "))); - -export const pathPart = async(directory: string, filter: (info: FileInfo) => boolean) => { +export const pathIn = (directory: string, filter: (info: FileInfo) => boolean): Parser => runtime(async() => { const stats = await statsIn(directory); - return choice(stats.filter(filter).map(info => { - if (info.stat.isDirectory()) { - const styledDirectory = decorate(fileName(`${info.name}/`), style(styles.directory)); - return changingContextDirectory(info.name.startsWith(".") ? noisySuggestions(styledDirectory) : styledDirectory); - } else { - const styled = decorate(fileName(info.name), style(styles.file(info))); - return info.name.startsWith(".") ? noisySuggestions(styled) : styled; - } - })); -}; + return choice([ + ...stats.filter(filter).map(info => { + if (info.stat.isDirectory()) { + return sequence( + directoryParser(`${info.name}/`), + pathIn(resolveDirectory(directory, info.name), filter) + ); + } else { + return fileParser(info); + } + }), + ...["./", "../"].map(directoryAlias(directory, filter)) + ]); +}); -export const pathInCurrentDirectory = (filter: (info: FileInfo) => boolean) => many1( - runtime( - async(context) => choice([directoryAlias].concat(await pathPart(context.directory, filter))) - ) -); +export const pathInCurrentDirectory = (filter: FileFilter) => runtime(async(context) => choice([ + ...["/", "~/"].map(directoryAlias(context.directory, filter)), + pathIn(context.directory, filter), +]));