Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/features/vim/components/vim-status-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const VimStatusIndicator = () => {
if (visualMode === "line") {
return "VISUAL LINE";
}
if (visualMode === "block") {
return "VISUAL BLOCK";
}
return "VISUAL";
case "command":
return "COMMAND";
Expand All @@ -49,6 +52,7 @@ const VimStatusIndicator = () => {
return "bg-green-500/20 text-green-400 border-green-500/30";
case "VISUAL":
case "VISUAL LINE":
case "VISUAL BLOCK":
return "bg-yellow-500/20 text-yellow-400 border-yellow-500/30";
case "COMMAND":
return "bg-purple-500/20 text-purple-400 border-purple-500/30";
Expand Down
211 changes: 211 additions & 0 deletions src/features/vim/core/core/grammar/ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* AST (Abstract Syntax Tree) types for Vim grammar
*
* Grammar (EBNF):
* Command := [Register] [Count] ( Action | OperatorInvocation | Motion )
* Register := '"' RegisterName
* Count := DigitNonZero { Digit }
* Action := PutAction | CharAction | MiscAction | ModeChangeAction
* OperatorInvocation := Operator ( Operator | [Count] Target )
* Operator := 'd'|'c'|'y'|'<'|'>'|'='|'!'|'g~'|'gu'|'gU'|'gq'|'g@'
* Target := ForcedKind? ( TextObject | Motion )
* ForcedKind := 'v' | 'V' | '<C-V>'
* TextObject := ('a'|'i') ObjectKey
* Motion := SimpleMotion | CharMotion | SearchMotion | MarkMotion | PrefixedMotion
*/

export type Count = number;

/**
* Register reference (e.g., "a, "0, "+, "*)
*/
export interface RegisterRef {
name: string; // a-z, 0-9, ", +, *, _, /
}

/**
* Top-level command structure
*/
export type Command =
| {
kind: "action";
reg?: RegisterRef;
count?: Count;
action: Action;
}
| {
kind: "operator";
reg?: RegisterRef;
countBefore?: Count;
operator: OperatorKey;
doubled?: boolean; // true for dd, yy, cc, >>, etc.
target?: Target; // absent => incomplete command
countAfter?: Count;
}
| {
kind: "motion";
count?: Count;
motion: Motion;
}
| {
kind: "visualOperator";
reg?: RegisterRef;
operator: OperatorKey;
}
| {
kind: "visualTextObject";
reg?: RegisterRef;
mode: "inner" | "around";
object: string;
};

/**
* Actions - standalone commands that don't require a motion
*/
export type Action =
| { type: "put"; which: "p" | "P" }
| { type: "charReplace"; which: "r" | "gr"; char: string }
| { type: "modeChange"; mode: ModeChangeAction }
| { type: "singleChar"; operation: SingleCharOperation }
| { type: "undo" }
| { type: "redo" }
| { type: "repeat" } // dot command
| { type: "misc"; key: string }; // J, ~, etc.

/**
* Mode change actions (i, a, A, I, o, O, s)
*/
export type ModeChangeAction =
| "insert" // i
| "append" // a
| "appendLine" // A
| "insertLineStart" // I
| "openBelow" // o
| "openAbove" // O
| "substitute"; // s

/**
* Single character operations (x, X)
*/
export type SingleCharOperation = "deleteChar" | "deleteCharBefore";

/**
* Operator keys
*/
export type OperatorKey =
| "d" // delete
| "c" // change
| "y" // yank
| "<" // outdent
| ">" // indent
| "=" // format
| "!" // filter
| "g~" // toggle case
| "gu" // lowercase
| "gU" // uppercase
| "gq" // format text
| "g@"; // operator function

/**
* Target for an operator (motion or text object)
*/
export type Target =
| {
type: "motion";
forced?: "char" | "line" | "block"; // v, V, Ctrl-V
motion: Motion;
}
| {
type: "textObject";
forced?: "char" | "line" | "block";
mode: "inner" | "around"; // i or a
object: string; // w, s, p, ), ], }, >, ", ', `, t, b, B, etc.
};

/**
* Motion types
*/
export type Motion =
| SimpleMotion
| CharMotion
| SearchMotion
| SearchRepeatMotion
| MarkMotion
| PrefixedMotion;

/**
* Simple single or multi-character motions
*/
export interface SimpleMotion {
type: "simple";
key: string; // w, W, e, E, b, B, h, j, k, l, 0, ^, $, gg, G, {, }, (, ), g_, %, etc.
}

/**
* Character-finding motions (f, F, t, T)
*/
export interface CharMotion {
type: "char";
key: "f" | "F" | "t" | "T";
char: string;
}

/**
* Search motions (/, ?)
*/
export interface SearchMotion {
type: "search";
dir: "fwd" | "bwd"; // / or ?
pattern: string;
}

/**
* Search repeat motions (n, N, *, #)
*/
export interface SearchRepeatMotion {
type: "searchRepeat";
key: "n" | "N" | "*" | "#";
}

/**
* Mark motions (', `)
*/
export interface MarkMotion {
type: "mark";
style: "'" | "`"; // ' for line, ` for exact position
mark: string; // a-z, A-Z, 0-9, <, >, etc.
}

/**
* Prefixed motions (g, z, [, ] families)
* Examples: g_, gj, gk, zt, zz, zb, ]], [[, ]m, [m
*/
export interface PrefixedMotion {
type: "prefixed";
head: "g" | "z" | "[" | "]";
tail: string;
}

/**
* Parse result types
*/
export interface ParseOk {
status: "complete";
command: Command;
}

export interface ParseIncomplete {
status: "incomplete";
}

export interface ParseNeedsChar {
status: "needsChar";
context?: string; // e.g., "f", "r", "'", for better UX
}

export interface ParseInvalid {
status: "invalid";
reason?: string;
}

export type ParseResult = ParseOk | ParseIncomplete | ParseNeedsChar | ParseInvalid;
128 changes: 128 additions & 0 deletions src/features/vim/core/core/grammar/compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Compatibility layer between old and new Vim parsers
*
* Allows incremental migration by trying the new parser first,
* falling back to the old parser if needed.
*/

import { executeVimCommand as executeOld } from "../command-executor";
import {
getCommandParseStatus as getOldParseStatus,
parseVimCommand as parseOld,
} from "../command-parser";
import type { ParseResult } from "./ast";
import { executeAST } from "./executor";
import { getCommandParseStatus as getNewParseStatus, parse as parseNew } from "./parser";

/**
* Feature flag to enable/disable new parser
*
* Set to true to use new parser, false to use old parser.
* Can be toggled at runtime for testing.
*/
let USE_NEW_PARSER = true; // Default: enabled for production use

/**
* Enable or disable the new parser
*/
export function setUseNewParser(enabled: boolean): void {
USE_NEW_PARSER = enabled;
}

/**
* Check if new parser is enabled
*/
export function isNewParserEnabled(): boolean {
return USE_NEW_PARSER;
}

/**
* Parse command with compatibility fallback
*
* Tries new parser first, falls back to old parser if needed.
*/
export function parseVimCommandCompat(keys: string[]): ParseResult | null {
// If new parser is disabled, use old parser
if (!USE_NEW_PARSER) {
const command = parseOld(keys);
if (!command) return null;

// Convert old command format to new ParseResult format
return {
status: "complete",
command: command as any, // Old command format is different but compatible
};
}

// Use new parser
const result = parseNew(keys);

if (result.status === "complete") {
return result;
}

return null;
}

/**
* Execute command with compatibility fallback
*
* Tries new executor first, falls back to old executor if needed.
*/
export function executeVimCommandCompat(keys: string[]): boolean {
// If new parser is disabled, use old executor
if (!USE_NEW_PARSER) {
return executeOld(keys);
}

// Use new parser and executor
const result = parseNew(keys);

if (result.status === "complete") {
// Use new executor
return executeAST(result.command);
}

return false;
}

/**
* Get command parse status with compatibility
*
* Returns: "complete" | "incomplete" | "invalid" | "needsChar"
*/
export function getCommandParseStatusCompat(
keys: string[],
): "complete" | "incomplete" | "invalid" | "needsChar" {
// If new parser is disabled, use old parser
if (!USE_NEW_PARSER) {
return getOldParseStatus(keys);
}

// Use new parser
return getNewParseStatus(keys);
}

/**
* Check if command is complete (ready to execute)
*/
export function isCommandComplete(keys: string[]): boolean {
const status = getCommandParseStatusCompat(keys);
return status === "complete";
}

/**
* Check if command is waiting for more keys
*/
export function expectsMoreKeys(keys: string[]): boolean {
const status = getCommandParseStatusCompat(keys);
return status === "incomplete" || status === "needsChar";
}

/**
* Check if command is invalid
*/
export function isCommandInvalid(keys: string[]): boolean {
const status = getCommandParseStatusCompat(keys);
return status === "invalid";
}
Loading