text: import * as vscode from 'vscode';
  import { VimState } from '../state/vimState';
  import { TokenType, Token } from './token';
  
  type LineRefOperation = TokenType.Plus | TokenType.Minus;
  
  /**
   * Represents a range of lines, as expressed on the command line.
   *
   * http://vimdoc.sourceforge.net/htmldoc/cmdline.html#cmdline-ranges
   */
  export class LineRange {
    left: Token[];
    separator: Token | undefined;
    right: Token[];
  
    constructor() {
      this.left = [];
      this.right = [];
    }
  
    public addToken(tok: Token): void {
      if (tok.type === TokenType.Comma) {
        this.separator = tok;
        return;
      }
  
      if (!this.separator) {
        if (this.left.length > 0) {
          switch (tok.type) {
            case TokenType.Offset:
            case TokenType.Plus:
            case TokenType.Minus:
              break;
            default:
              throw Error('Trailing characters');
          }
        }
        this.left.push(tok);
      } else {
        if (this.right.length > 0) {
          switch (tok.type) {
            case TokenType.Offset:
            case TokenType.Plus:
            case TokenType.Minus:
              break;
            default:
              throw Error('Trailing characters');
          }
        }
        this.right.push(tok);
      }
    }
  
    get isEmpty(): boolean {
      return this.left.length === 0 && this.right.length === 0 && !this.separator;
    }
  
    public toString(): string {
      return this.left.toString() + (this.separator?.content ?? '') + this.right.toString();
    }
  
    /**
     * Resolves the line range to concrete line numbers
     *
     * @param vimState
     * @returns Inclusive line number range [start, end]. Will always be in order.
     */
    public resolve(vimState: VimState): [number, number] {
      if (this.left.length > 0 && this.left[0].type === TokenType.Percent) {
        return [0, vimState.editor.document.lineCount - 1];
      }
  
      const start = LineRange.resolveLineRef(this.left, vimState) ?? vimState.cursorStopPosition.line;
      const end = LineRange.resolveLineRef(this.right, vimState) ?? start;
      return end < start ? [end, start] : [start, end];
    }
  
    private static resolveLineRef(toks: Token[], vimState: VimState): number | undefined {
      if (toks.length === 0) {
        return undefined;
      }
  
      let currentLineNum: number;
      let currentOperation: LineRefOperation | undefined;
  
      const firstToken = toks[0];
      // handle first-token special cases (e.g. %, inital line number is "." by default)
      switch (firstToken.type) {
        case TokenType.Percent:
          return vimState.editor.document.lineCount - 1;
        case TokenType.Dollar:
          currentLineNum = vimState.editor.document.lineCount - 1;
          break;
        case TokenType.Plus:
        case TokenType.Minus:
        case TokenType.Dot:
          currentLineNum = vimState.editor.selection.active.line;
          // undocumented: if the first token is plus or minus, vim seems to behave as though there was a "."
          currentOperation = firstToken.type === TokenType.Dot ? undefined : firstToken.type;
          break;
        case TokenType.LineNumber:
          currentLineNum = Number.parseInt(firstToken.content, 10) - 1; // user sees 1-based - everything else is 0-based
          break;
        case TokenType.SelectionFirstLine:
          currentLineNum = Math.min.apply(
            null,
            vimState.editor.selections.map((selection) =>
              selection.start.isBeforeOrEqual(selection.end)
                ? selection.start.line
                : selection.end.line
            )
          );
          break;
        case TokenType.SelectionLastLine:
          currentLineNum = Math.max.apply(
            null,
            vimState.editor.selections.map((selection) =>
              selection.start.isAfter(selection.end) ? selection.start.line : selection.end.line
            )
          );
          break;
        case TokenType.Mark:
          currentLineNum = vimState.historyTracker.getMark(firstToken.content)!.position.line;
          break;
        default:
          throw new Error('Not Implemented');
      }
  
      // now handle subsequent tokens, offsetting the current candidate line number
      for (let tokenIndex = 1; tokenIndex < toks.length; ++tokenIndex) {
        let currentToken = toks[tokenIndex];
  
        switch (currentOperation) {
          case TokenType.Plus:
            switch (currentToken.type) {
              case TokenType.Minus:
              case TokenType.Plus:
                // undocumented: when there's two operators in a row, vim behaves as though there's a "1" between them
                currentLineNum += 1;
                currentOperation = currentToken.type;
                break;
              case TokenType.Offset:
                currentLineNum += Number.parseInt(currentToken.content, 10);
                currentOperation = undefined;
                break;
              default:
                throw Error('Trailing characters');
            }
            break;
          case TokenType.Minus:
            switch (currentToken.type) {
              case TokenType.Minus:
              case TokenType.Plus:
                // undocumented: when there's two operators in a row, vim behaves as though there's a "1" between them
                currentLineNum -= 1;
                currentOperation = currentToken.type;
                break;
              case TokenType.Offset:
                currentLineNum -= Number.parseInt(currentToken.content, 10);
                currentOperation = undefined;
                break;
              default:
                throw Error('Trailing characters');
            }
            break;
          case undefined:
            switch (currentToken.type) {
              case TokenType.Minus:
              case TokenType.Plus:
                currentOperation = currentToken.type;
                break;
              default:
                throw Error('Trailing characters');
            }
            break;
        }
      }
  
      // undocumented: when there's a trailing operation in the tank without an RHS, vim uses "1"
      switch (currentOperation) {
        case TokenType.Plus:
          currentLineNum += 1;
          break;
        case TokenType.Minus:
          currentLineNum -= 1;
          break;
      }
  
      // finally, make sure current position is in bounds :)
      currentLineNum = Math.max(0, currentLineNum);
      currentLineNum = Math.min(vimState.editor.document.lineCount - 1, currentLineNum);
      return currentLineNum;
    }
  }
  
  export class CommandLine {
    range: LineRange;
    command: CommandBase | undefined;
  
    constructor() {
      this.range = new LineRange();
    }
  
    get isEmpty(): boolean {
      return this.range.isEmpty && !this.command;
    }
  
    toString(): string {
      return ':' + this.range.toString() + ' ' + (this.command?.toString() ?? '');
    }
  
    async execute(vimState: VimState): Promise<void> {
      if (this.command) {
        if (this.range.isEmpty) {
          await this.command.execute(vimState);
        } else {
          await this.command.executeWithRange(vimState, this.range);
        }
      } else {
        const [_, end] = this.range.resolve(vimState);
        vimState.cursorStartPosition = vimState.cursorStopPosition = vimState.cursorStopPosition
          .withLine(end)
          .obeyStartOfLine();
      }
    }
  }
  
  export interface ICommandArgs {
    bang?: boolean;
    range?: LineRange;
  }
  
  export abstract class CommandBase {
    protected get activeTextEditor() {
      return vscode.window.activeTextEditor;
    }
  
    get arguments(): ICommandArgs {
      return this._arguments;
    }
    protected _arguments: ICommandArgs;
  
    public neovimCapable(): boolean {
      return false;
    }
  
    abstract execute(vimState: VimState): Promise<void>;
  
    async executeWithRange(vimState: VimState, range: LineRange): Promise<void> {
      // By default, ignore the given range.
      await this.execute(vimState);
    }
  }
  