diff --git a/src/parser/argument/PhutilArgumentSpellingCorrector.php b/src/parser/argument/PhutilArgumentSpellingCorrector.php index dd999123..47218a5c 100644 --- a/src/parser/argument/PhutilArgumentSpellingCorrector.php +++ b/src/parser/argument/PhutilArgumentSpellingCorrector.php @@ -1,155 +1,170 @@ setInsertCost(4) ->setDeleteCost(4) ->setReplaceCost(3) ->setTransposeCost(2); return id(new self()) ->setEditDistanceMatrix($matrix) ->setMode(self::MODE_COMMANDS) ->setMaximumDistance($max_distance); } /** * Build a new corrector with parameters for correcting flags, like * fixing "--nolint" into "--no-lint". * * @return PhutilArgumentSpellingCorrector Configured corrector. */ public static function newFlagCorrector() { // When correcting flag spelling, we're stricter than we are when // correcting command spelling: we allow only one inserted or deleted // character. It is mainly to handle cases like "--no-lint" versus // "--nolint" or "--reviewer" versus "--reviewers". $max_distance = 1; $matrix = id(new PhutilEditDistanceMatrix()) ->setInsertCost(1) ->setDeleteCost(1) ->setReplaceCost(10); return id(new self()) ->setEditDistanceMatrix($matrix) ->setMode(self::MODE_FLAGS) ->setMaximumDistance($max_distance); } public function setMode($mode) { $this->mode = $mode; return $this; } public function getMode() { return $this->mode; } public function setEditDistanceMatrix(PhutilEditDistanceMatrix $matrix) { $this->editDistanceMatrix = $matrix; return $this; } public function getEditDistanceMatrix() { return $this->editDistanceMatrix; } public function setMaximumDistance($maximum_distance) { $this->maximumDistance = $maximum_distance; return $this; } public function getMaximumDistance() { return $this->maximumDistance; } public function correctSpelling($input, array $options) { $matrix = $this->getEditDistanceMatrix(); if (!$matrix) { throw new PhutilInvalidStateException('setEditDistanceMatrix'); } $max_distance = $this->getMaximumDistance(); if (!$max_distance) { throw new PhutilInvalidStateException('setMaximumDistance'); } // If we're correcting commands, never correct an input which begins // with "-", since this is almost certainly intended to be a flag. if ($this->getMode() === self::MODE_COMMANDS) { if (preg_match('/^-/', $input)) { return array(); } } $input = $this->normalizeString($input); foreach ($options as $key => $option) { $options[$key] = $this->normalizeString($option); } + // In command mode, accept any unique prefix of a command as a shorthand + // for that command. + if ($this->getMode() === self::MODE_COMMANDS) { + $prefixes = array(); + foreach ($options as $option) { + if (!strncmp($input, $option, strlen($input))) { + $prefixes[] = $option; + } + } + + if (count($prefixes) === 1) { + return $prefixes; + } + } + $distances = array(); $inputv = phutil_utf8v($input); foreach ($options as $option) { $optionv = phutil_utf8v($option); $matrix->setSequences($optionv, $inputv); $distances[$option] = $matrix->getEditDistance(); } asort($distances); $best = min($max_distance, head($distances)); foreach ($distances as $option => $distance) { if ($distance > $best) { unset($distances[$option]); } } // Before filtering, check if we have multiple equidistant matches and // return them if we do. This prevents us from, e.g., matching "alnd" with // both "land" and "amend", then dropping "land" for being too short, and // incorrectly completing to "amend". if (count($distances) > 1) { return array_keys($distances); } foreach ($distances as $option => $distance) { if (phutil_utf8_strlen($option) < $distance) { unset($distances[$option]); } } return array_keys($distances); } private function normalizeString($string) { return phutil_utf8_strtolower($string); } }