From 7e8c4c4cffbfa83f61adf441ff62aa7bf2fb5d93 Mon Sep 17 00:00:00 2001
From: Pokey Rule <755842+pokey@users.noreply.github.com>
Date: Wed, 7 Jun 2023 20:47:39 +0100
Subject: [PATCH] Migrate `functionName` to just pipe `namedFunction` into
 `name`

---
 packages/common/src/index.ts                  |   4 +-
 ...{CommandV5.types.ts => CommandV6.types.ts} |   4 +-
 .../common/src/types/command/command.types.ts |   8 +-
 .../types/command/legacy/CommandV5.types.ts   |  99 +++++
 .../legacy/PartialTargetDescriptorV5.types.ts | 355 ++++++++++++++++++
 .../canonicalizeAndValidateCommand.ts         |   4 +
 .../upgradeV5ToV6/index.ts                    |   1 +
 .../upgradeV5ToV6/upgradeV5ToV6.ts            | 135 +++++++
 8 files changed, 604 insertions(+), 6 deletions(-)
 rename packages/common/src/types/command/{CommandV5.types.ts => CommandV6.types.ts} (94%)
 create mode 100644 packages/common/src/types/command/legacy/CommandV5.types.ts
 create mode 100644 packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts
 create mode 100644 packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts
 create mode 100644 packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts

diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 9949e463f7..e6a848901b 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -70,10 +70,12 @@ export * from "./types/command/legacy/CommandV0V1.types";
 export * from "./types/command/legacy/CommandV2.types";
 export * from "./types/command/legacy/CommandV3.types";
 export * from "./types/command/legacy/CommandV4.types";
+export * from "./types/command/legacy/CommandV5.types";
 export * from "./types/command/legacy/targetDescriptorV2.types";
-export * from "./types/command/CommandV5.types";
+export * from "./types/command/CommandV6.types";
 export * from "./types/command/legacy/PartialTargetDescriptorV3.types";
 export * from "./types/command/legacy/PartialTargetDescriptorV4.types";
+export * from "./types/command/legacy/PartialTargetDescriptorV5.types";
 export * from "./types/CommandServerApi";
 export * from "./util/itertools";
 export * from "./extensionDependencies";
diff --git a/packages/common/src/types/command/CommandV5.types.ts b/packages/common/src/types/command/CommandV6.types.ts
similarity index 94%
rename from packages/common/src/types/command/CommandV5.types.ts
rename to packages/common/src/types/command/CommandV6.types.ts
index 33bd6c2d79..2d9488c0a6 100644
--- a/packages/common/src/types/command/CommandV5.types.ts
+++ b/packages/common/src/types/command/CommandV6.types.ts
@@ -1,11 +1,11 @@
 import type { PartialTargetDescriptor } from "./PartialTargetDescriptor.types";
 import type { ActionCommand } from "./ActionCommand";
 
-export interface CommandV5 {
+export interface CommandV6 {
   /**
    * The version number of the command API
    */
-  version: 5;
+  version: 6;
 
   /**
    * The spoken form of the command if issued from a voice command system
diff --git a/packages/common/src/types/command/command.types.ts b/packages/common/src/types/command/command.types.ts
index 665a9baca8..b222b722b3 100644
--- a/packages/common/src/types/command/command.types.ts
+++ b/packages/common/src/types/command/command.types.ts
@@ -1,14 +1,15 @@
 import type { ActionCommand } from "./ActionCommand";
-import type { CommandV5 } from "./CommandV5.types";
+import type { CommandV6 } from "./CommandV6.types";
 import type { CommandV0, CommandV1 } from "./legacy/CommandV0V1.types";
 import type { CommandV2 } from "./legacy/CommandV2.types";
 import type { CommandV3 } from "./legacy/CommandV3.types";
 import type { CommandV4 } from "./legacy/CommandV4.types";
+import { CommandV5 } from "./legacy/CommandV5.types";
 
 export type CommandComplete = Required<Omit<CommandLatest, "spokenForm">> &
   Pick<CommandLatest, "spokenForm"> & { action: Required<ActionCommand> };
 
-export const LATEST_VERSION = 5 as const;
+export const LATEST_VERSION = 6 as const;
 
 export type CommandLatest = Command & {
   version: typeof LATEST_VERSION;
@@ -20,4 +21,5 @@ export type Command =
   | CommandV2
   | CommandV3
   | CommandV4
-  | CommandV5;
+  | CommandV5
+  | CommandV6;
diff --git a/packages/common/src/types/command/legacy/CommandV5.types.ts b/packages/common/src/types/command/legacy/CommandV5.types.ts
new file mode 100644
index 0000000000..0d6fffd44a
--- /dev/null
+++ b/packages/common/src/types/command/legacy/CommandV5.types.ts
@@ -0,0 +1,99 @@
+import { PartialTargetDescriptorV5 } from "./PartialTargetDescriptorV5.types";
+
+const actionNames = [
+  "callAsFunction",
+  "clearAndSetSelection",
+  "copyToClipboard",
+  "cutToClipboard",
+  "deselect",
+  "editNew",
+  "editNewLineAfter",
+  "editNewLineBefore",
+  "executeCommand",
+  "extractVariable",
+  "findInWorkspace",
+  "foldRegion",
+  "followLink",
+  "generateSnippet",
+  "getText",
+  "highlight",
+  "indentLine",
+  "insertCopyAfter",
+  "insertCopyBefore",
+  "insertEmptyLineAfter",
+  "insertEmptyLineBefore",
+  "insertEmptyLinesAround",
+  "insertSnippet",
+  "moveToTarget",
+  "outdentLine",
+  "pasteFromClipboard",
+  "randomizeTargets",
+  "remove",
+  "rename",
+  "replace",
+  "replaceWithTarget",
+  "revealDefinition",
+  "revealTypeDefinition",
+  "reverseTargets",
+  "rewrapWithPairedDelimiter",
+  "experimental.setInstanceReference",
+  "scrollToBottom",
+  "scrollToCenter",
+  "scrollToTop",
+  "setSelection",
+  "setSelectionAfter",
+  "setSelectionBefore",
+  "showDebugHover",
+  "showHover",
+  "showQuickFix",
+  "showReferences",
+  "sortTargets",
+  "swapTargets",
+  "toggleLineBreakpoint",
+  "toggleLineComment",
+  "unfoldRegion",
+  "wrapWithPairedDelimiter",
+  "wrapWithSnippet",
+] as const;
+
+type ActionType = (typeof actionNames)[number];
+
+interface ActionCommand {
+  /**
+   * The action to run
+   */
+  name: ActionType;
+
+  /**
+   * A list of arguments expected by the given action.
+   */
+  args?: unknown[];
+}
+
+export interface CommandV5 {
+  /**
+   * The version number of the command API
+   */
+  version: 5;
+
+  /**
+   * The spoken form of the command if issued from a voice command system
+   */
+  spokenForm?: string;
+
+  /**
+   * If the command is issued from a voice command system, this boolean indicates
+   * whether we should use the pre phrase snapshot. Only set this to true if the
+   * voice command system issues a pre phrase signal at the start of every
+   * phrase.
+   */
+  usePrePhraseSnapshot: boolean;
+
+  action: ActionCommand;
+
+  /**
+   * A list of targets expected by the action. Inference will be run on the
+   * targets
+   */
+  targets: PartialTargetDescriptorV5[];
+}
diff --git a/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts b/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts
new file mode 100644
index 0000000000..a2be15adcd
--- /dev/null
+++ b/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts
@@ -0,0 +1,355 @@
+interface CursorMark {
+  type: "cursor";
+}
+
+interface ThatMark {
+  type: "that";
+}
+
+interface SourceMark {
+  type: "source";
+}
+
+interface NothingMark {
+  type: "nothing";
+}
+
+interface DecoratedSymbolMark {
+  type: "decoratedSymbol";
+  symbolColor: string;
+  character: string;
+}
+
+type LineNumberType = "absolute" | "relative" | "modulo100";
+
+interface LineNumberMark {
+  type: "lineNumber";
+  lineNumberType: LineNumberType;
+  lineNumber: number;
+}
+
+/**
+ * Constructs a range between {@link anchor} and {@link active}
+ */
+interface RangeMark {
+  type: "range";
+  anchor: PartialMark;
+  active: PartialMark;
+  excludeAnchor?: boolean;
+  excludeActive?: boolean;
+}
+
+type PartialMark =
+  | CursorMark
+  | ThatMark
+  | SourceMark
+  | DecoratedSymbolMark
+  | NothingMark
+  | LineNumberMark
+  | RangeMark;
+
+type SimpleSurroundingPairName =
+  | "angleBrackets"
+  | "backtickQuotes"
+  | "curlyBrackets"
+  | "doubleQuotes"
+  | "escapedDoubleQuotes"
+  | "escapedParentheses"
+  | "escapedSquareBrackets"
+  | "escapedSingleQuotes"
+  | "parentheses"
+  | "singleQuotes"
+  | "squareBrackets";
+type ComplexSurroundingPairName = "string" | "any" | "collectionBoundary";
+type SurroundingPairName =
+  | SimpleSurroundingPairName
+  | ComplexSurroundingPairName;
+
+export type SimpleScopeTypeTypeV5 =
+  | "argumentOrParameter"
+  | "anonymousFunction"
+  | "attribute"
+  | "branch"
+  | "class"
+  | "className"
+  | "collectionItem"
+  | "collectionKey"
+  | "comment"
+  | "functionCall"
+  | "functionCallee"
+  | "functionName"
+  | "ifStatement"
+  | "instance"
+  | "list"
+  | "map"
+  | "name"
+  | "namedFunction"
+  | "regularExpression"
+  | "statement"
+  | "string"
+  | "type"
+  | "value"
+  | "condition"
+  | "section"
+  | "sectionLevelOne"
+  | "sectionLevelTwo"
+  | "sectionLevelThree"
+  | "sectionLevelFour"
+  | "sectionLevelFive"
+  | "sectionLevelSix"
+  | "selector"
+  | "switchStatementSubject"
+  | "unit"
+  | "xmlBothTags"
+  | "xmlElement"
+  | "xmlEndTag"
+  | "xmlStartTag"
+  // Latex scope types
+  | "part"
+  | "chapter"
+  | "subSection"
+  | "subSubSection"
+  | "namedParagraph"
+  | "subParagraph"
+  | "environment"
+  // Text based scopes
+  | "token"
+  | "line"
+  | "notebookCell"
+  | "paragraph"
+  | "document"
+  | "character"
+  | "word"
+  | "identifier"
+  | "nonWhitespaceSequence"
+  | "boundedNonWhitespaceSequence"
+  | "url";
+
+interface SimpleScopeTypeV5 {
+  type: SimpleScopeTypeTypeV5;
+}
+
+interface CustomRegexScopeType {
+  type: "customRegex";
+  regex: string;
+}
+
+type SurroundingPairDirection = "left" | "right";
+interface SurroundingPairScopeType {
+  type: "surroundingPair";
+  delimiter: SurroundingPairName;
+  forceDirection?: SurroundingPairDirection;
+
+  /**
+   * If `true`, then only accept pairs where the pair completely contains the
+   * selection, ie without the edges touching.
+   */
+  requireStrongContainment?: boolean;
+}
+
+interface OneOfScopeType {
+  type: "oneOf";
+  scopeTypes: ScopeTypeV5[];
+}
+
+export type ScopeTypeV5 =
+  | SimpleScopeTypeV5
+  | SurroundingPairScopeType
+  | CustomRegexScopeType
+  | OneOfScopeType;
+
+interface InteriorOnlyModifier {
+  type: "interiorOnly";
+}
+
+interface ExcludeInteriorModifier {
+  type: "excludeInterior";
+}
+
+export interface ContainingScopeModifierV5 {
+  type: "containingScope";
+  scopeType: ScopeTypeV5;
+  ancestorIndex?: number;
+}
+
+export interface EveryScopeModifierV5 {
+  type: "everyScope";
+  scopeType: ScopeTypeV5;
+}
+
+/**
+ * Refer to scopes by absolute index relative to iteration scope, eg "first
+ * funk" to refer to the first function in a class.
+ */
+export interface OrdinalScopeModifierV5 {
+  type: "ordinalScope";
+
+  scopeType: ScopeTypeV5;
+
+  /** The start of the range.  Start from end of iteration scope if `start` is negative */
+  start: number;
+
+  /** The number of scopes to include.  Will always be positive.  If greater than 1, will include scopes after {@link start} */
+  length: number;
+}
+
+type Direction = "forward" | "backward";
+
+/**
+ * Refer to scopes by offset relative to input target, eg "next
+ * funk" to refer to the first function after the function containing the target input.
+ */
+export interface RelativeScopeModifierV5 {
+  type: "relativeScope";
+
+  scopeType: ScopeTypeV5;
+
+  /** Indicates how many scopes away to start relative to the input target.
+   * Note that if {@link direction} is `"backward"`, then this scope will be the
+   * end of the output range.  */
+  offset: number;
+
+  /** The number of scopes to include.  Will always be positive.  If greater
+   * than 1, will include scopes in the direction of {@link direction} */
+  length: number;
+
+  /** Indicates which direction both {@link offset} and {@link length} go
+   * relative to input target  */
+  direction: Direction;
+}
+
+/**
+ * Converts its input to a raw selection with no type information so for
+ * example if it is the destination of a bring or move it should inherit the
+ * type information such as delimiters from its source.
+ */
+interface RawSelectionModifier {
+  type: "toRawSelection";
+}
+
+interface LeadingModifier {
+  type: "leading";
+}
+
+interface TrailingModifier {
+  type: "trailing";
+}
+
+interface KeepContentFilterModifier {
+  type: "keepContentFilter";
+}
+
+interface KeepEmptyFilterModifier {
+  type: "keepEmptyFilter";
+}
+
+interface InferPreviousMarkModifier {
+  type: "inferPreviousMark";
+}
+
+type TargetPosition = "before" | "after" | "start" | "end";
+
+interface PositionModifier {
+  type: "position";
+  position: TargetPosition;
+}
+
+export interface PartialPrimitiveTargetDescriptorV5 {
+  type: "primitive";
+  mark?: PartialMark;
+  modifiers?: ModifierV5[];
+}
+
+interface HeadTailModifier {
+  type: "extendThroughStartOf" | "extendThroughEndOf";
+  modifiers?: ModifierV5[];
+}
+
+/**
+ * Runs {@link modifier} if the target has no explicit scope type, ie if
+ * {@link Target.hasExplicitScopeType} is `false`.
+ */
+interface ModifyIfUntypedModifier {
+  type: "modifyIfUntyped";
+
+  /**
+   * The modifier to apply if the target is untyped
+   */
+  modifier: ModifierV5;
+}
+
+/**
+ * Tries each of the modifiers in {@link modifiers} in turn until one of them
+ * doesn't throw an error, returning the output from the first modifier not
+ * throwing an error.
+ */
+interface CascadingModifier {
+  type: "cascading";
+
+  /**
+   * The modifiers to try in turn
+   */
+  modifiers: ModifierV5[];
+}
+
+/**
+ * First applies {@link anchor} to input, then independently applies
+ * {@link active}, and forms a range between the two resulting targets
+ */
+interface RangeModifier {
+  type: "range";
+  anchor: ModifierV5;
+  active: ModifierV5;
+  excludeAnchor?: boolean;
+  excludeActive?: boolean;
+}
+
+export type ModifierV5 =
+  | PositionModifier
+  | InteriorOnlyModifier
+  | ExcludeInteriorModifier
+  | ContainingScopeModifierV5
+  | EveryScopeModifierV5
+  | OrdinalScopeModifierV5
+  | RelativeScopeModifierV5
+  | HeadTailModifier
+  | LeadingModifier
+  | TrailingModifier
+  | RawSelectionModifier
+  | ModifyIfUntypedModifier
+  | CascadingModifier
+  | RangeModifier
+  | KeepContentFilterModifier
+  | KeepEmptyFilterModifier
+  | InferPreviousMarkModifier;
+
+// continuous is one single continuous selection between the two targets
+// vertical puts a selection on each line vertically between the two targets
+type PartialRangeType = "continuous" | "vertical";
+
+export interface PartialRangeTargetDescriptorV5 {
+  type: "range";
+  anchor: PartialPrimitiveTargetDescriptorV5 | ImplicitTargetDescriptorV5;
+  active: PartialPrimitiveTargetDescriptorV5;
+  excludeAnchor: boolean;
+  excludeActive: boolean;
+  rangeType?: PartialRangeType;
+}
+
+export interface PartialListTargetDescriptorV5 {
+  type: "list";
+  elements: (
+    | PartialPrimitiveTargetDescriptorV5
+    | PartialRangeTargetDescriptorV5
+  )[];
+}
+
+export interface ImplicitTargetDescriptorV5 {
+  type: "implicit";
+}
+
+export type PartialTargetDescriptorV5 =
+  | PartialPrimitiveTargetDescriptorV5
+  | PartialRangeTargetDescriptorV5
+  | PartialListTargetDescriptorV5
+  | ImplicitTargetDescriptorV5;
diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts
index e428649eda..d0d5d49dd4 100644
--- a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts
+++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts
@@ -20,6 +20,7 @@ import { upgradeV1ToV2 } from "./upgradeV1ToV2";
 import { upgradeV2ToV3 } from "./upgradeV2ToV3";
 import { upgradeV3ToV4 } from "./upgradeV3ToV4";
 import { upgradeV4ToV5 } from "./upgradeV4ToV5/upgradeV4ToV5";
+import { upgradeV5ToV6 } from "./upgradeV5ToV6";
 
 /**
  * Given a command argument which comes from the client, normalize it so that it
@@ -78,6 +79,9 @@ function upgradeCommand(command: Command): CommandLatest {
       case 4:
         command = upgradeV4ToV5(command);
         break;
+      case 5:
+        command = upgradeV5ToV6(command);
+        break;
       default:
         throw new Error(
           `Can't upgrade from unknown version ${command.version}`,
diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts
new file mode 100644
index 0000000000..fb04f2a3a5
--- /dev/null
+++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts
@@ -0,0 +1 @@
+export * from "./upgradeV5ToV6";
diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts
new file mode 100644
index 0000000000..1cb160c8f2
--- /dev/null
+++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts
@@ -0,0 +1,135 @@
+import {
+  CommandV5,
+  CommandV6,
+  ImplicitTargetDescriptor,
+  Modifier,
+  ModifierV5,
+  PartialPrimitiveTargetDescriptor,
+  PartialPrimitiveTargetDescriptorV5,
+  PartialRangeTargetDescriptor,
+  PartialTargetDescriptor,
+  PartialTargetDescriptorV5,
+  ScopeTypeV5,
+  SimpleScopeTypeType,
+} from "@cursorless/common";
+
+export function upgradeV5ToV6(command: CommandV5): CommandV6 {
+  return {
+    ...command,
+    version: 6,
+    targets: transformPartialPrimitiveTargets(command.targets, upgradeTarget),
+  };
+}
+
+function upgradeTarget(
+  target: PartialPrimitiveTargetDescriptorV5,
+): PartialPrimitiveTargetDescriptor {
+  return {
+    ...target,
+    modifiers: target.modifiers?.flatMap(upgradeModifier),
+  };
+}
+
+const upgrades: Partial<Record<ScopeTypeV5["type"], SimpleScopeTypeType>> = {
+  functionName: "namedFunction",
+  className: "class",
+};
+
+function upgradeModifier(modifier: ModifierV5): Modifier[] {
+  switch (modifier.type) {
+    case "containingScope":
+    case "everyScope":
+    case "ordinalScope":
+    case "relativeScope": {
+      const upgradedScopeType = upgrades[modifier.scopeType.type];
+
+      if (upgradedScopeType == null) {
+        return [modifier];
+      }
+
+      return [
+        {
+          type: "containingScope",
+          scopeType: {
+            type: "name",
+          },
+        },
+        {
+          ...modifier,
+          scopeType: {
+            type: upgradedScopeType,
+          },
+        },
+      ];
+    }
+    case "extendThroughStartOf":
+    case "extendThroughEndOf":
+      return [
+        {
+          type: modifier.type,
+          modifiers: modifier.modifiers?.flatMap(upgradeModifier),
+        },
+      ];
+    case "modifyIfUntyped":
+      return [
+        {
+          type: "modifyIfUntyped",
+          // TODO: This is a hack
+          // We should really use a new type of modifier that chains modifiers
+          modifier: upgradeModifier(modifier.modifier)[0],
+        },
+      ];
+    default:
+      return [modifier];
+  }
+}
+
+/**
+ * Given a list of targets, recursively descends all targets and applies `func`
+ * to every primitive target.
+ *
+ * @param targets The targets to extract from
+ * @returns A list of primitive targets
+ */
+function transformPartialPrimitiveTargets(
+  targets: PartialTargetDescriptorV5[],
+  func: (
+    target: PartialPrimitiveTargetDescriptorV5,
+  ) => PartialPrimitiveTargetDescriptor,
+) {
+  return targets.map((target) =>
+    transformPartialPrimitiveTargetsHelper(target, func),
+  );
+}
+
+function transformPartialPrimitiveTargetsHelper(
+  target: PartialTargetDescriptorV5,
+  func: (
+    target: PartialPrimitiveTargetDescriptorV5,
+  ) => PartialPrimitiveTargetDescriptor,
+): PartialTargetDescriptor {
+  switch (target.type) {
+    case "primitive":
+      return func(target);
+    case "implicit":
+      return target;
+    case "list":
+      return {
+        ...target,
+        elements: target.elements.map(
+          (element) =>
+            transformPartialPrimitiveTargetsHelper(element, func) as
+              | PartialPrimitiveTargetDescriptor
+              | PartialRangeTargetDescriptor,
+        ),
+      };
+    case "range":
+      return {
+        ...target,
+        anchor: transformPartialPrimitiveTargetsHelper(target.anchor, func) as
+          | PartialPrimitiveTargetDescriptor
+          | ImplicitTargetDescriptor,
+        active: func(target.active),
+      };
+  }
+}