Code Editing¶
The plugin provides a rich set of editing features that make writing ReScript code more productive.
Code Folding¶
Native
Collapse and expand blocks to focus on the code that matters.
Foldable Elements¶
Module declarations —
module Name = { ... }Let bindings — Multi-line
letwith{ ... }Type definitions — Multi-line
typedeclarationsBlock comments —
/* ... */Custom regions —
//#region…//#endregion
Custom Regions¶
Use //#region and //#endregion markers to create custom foldable sections:
//#region Helper functions
let add = (a, b) => a + b
let multiply = (a, b) => a * b
//#endregion
Code folding lets you hide implementation details and focus on the declarations that matter, making large files navigable without splitting them into smaller ones.
Code Formatting¶
Formatting and format checks use concurrent input/output with a total 10-second command timeout and a 4 MiB limit per output stream. Cancellation stops the owned process and observed child processes. Timeout or output overflow leaves the document unchanged and reports the failure reason.
LSP Required
Press Ctrl+Alt+L (Cmd+Option+L on macOS) to format the current file using the rescript format CLI.
How It Works¶
The plugin pipes the current document content to rescript format --stdin .<ext> via stdin and replaces the document text with the formatted output. Both .res and .resi files are supported. Formatting runs asynchronously via IntelliJ’s AsyncDocumentFormattingService, so the editor remains responsive during the operation.
Requirements¶
The ReScript compiler (rescript) must be installed in your project’s node_modules. The plugin searches for the CLI starting from the file’s directory up to the project root. If the CLI is not found, a notification is displayed.
Error Handling¶
CLI not found — A notification indicates that
rescriptwas not found innode_modulesTimeout — If formatting takes longer than 10 seconds, the process is terminated and an error notification is shown
Formatter errors — If the formatter exits with a non-zero code (e.g., syntax errors in the file), the stderr output is displayed as a notification
Format on Save¶
To automatically format ReScript files when saving, enable Reformat code in Settings > Tools > Actions on Save.
See also
Code Analysis — Format Check can highlight unformatted files and offer a quick-fix to format them.
One shortcut formats your entire file to match the official ReScript style, eliminating manual whitespace adjustments and ensuring consistent code style across your team.
Brace Matching¶
Native
The editor highlights matching brace pairs:
{and}[and](and)
Brace matching helps you instantly see where a block opens and closes, preventing mismatched delimiters in deeply nested code.
Smart Quotes¶
Native
When you type ", ', or `, the closing quote is automatically inserted.
Auto-paired quotes save keystrokes and prevent common syntax errors from unmatched string delimiters.
Smart Enter¶
Native
Press Shift+Enter to intelligently complete the current statement and start a new line:
Closes unclosed parentheses and braces
Adds missing
switchbody bracesMoves the cursor to the right position
Smart Enter intelligently closes open constructs and positions your cursor, so you can keep typing the next statement without manually balancing delimiters.
Statement Mover¶
Native
Move top-level declarations up or down:
Alt+Shift+Up— Move declaration upAlt+Shift+Down— Move declaration down
This moves the entire declaration (including decorators) as a unit.
Reorder declarations without cut-and-paste — the mover handles the entire declaration block including decorators, so you never accidentally leave behind an annotation or break a multi-line definition.
Intention Actions¶
Native
Press Alt+Enter on an expression to see available intentions:
Intention |
Description |
|---|---|
Wrap with |
Wrap expression in |
Wrap with |
Wrap expression in |
Wrap with |
Wrap expression in |
Add |
Add |
Generate doc comment |
Insert a |
Convert pipe to function call |
Convert |
Convert function call to pipe |
Convert |
Publish to interface |
Add a declaration from |
Unpublish from interface |
Remove a declaration from the |
Insert labeled arguments |
Insert all labeled arguments for a function call |
Merge switch cases |
Merge switch cases with identical bodies into |
Flatten nested switch |
Collapse a |
Case split |
Expand a pattern variable into all constructor cases |
Convert to labeled arguments |
Convert positional arguments to labeled arguments |
Remove unnecessary parentheses |
Remove parentheses around proven scalar initializers |
Remove redundant qualifier |
Remove unnecessary module path qualifiers |
Expand open into qualified references |
Rewrite members introduced by a project-local |
Convert filter+map to filterMap |
Convert |
Add type annotation |
Add explicit type annotation to a |
Insert inferred type annotations |
Batch-insert inferred type annotations into every un-annotated top-level |
Add |
Append |
Add |
Add underscore prefix to suppress unused variable warnings |
Remove redundant braces |
Remove unnecessary |
Fix identifier case |
Correct identifier casing (e.g., lowercase for values, uppercase for modules) |
Expand destructuring |
Expand |
Convert call to uncurried form |
Rewrite |
Extract module to file |
Move a top-level |
Rename variant constructor (project-wide, no LSP) |
Rename a variant constructor across every |
Intention actions turn common code transformations into one-click operations — instead of manually restructuring code, press Alt+Enter and let the IDE handle the mechanical changes while you focus on the logic.
See also
Code Analysis provides inspections that detect issues automatically, with quick fixes for one-click resolution.
Wrap with Some(…)¶
Wraps a selected expression in Some(), useful when you need to convert a plain value into an option type.
Select the expression, press Alt+Enter, and choose Wrap with Some(…).
Select name, then:
let name = "Alice"
let greeting = getGreeting(name)
let name = "Alice"
let greeting = getGreeting(Some(name))
Wrap with Ok(…)¶
Wraps a selected expression in Ok(), useful when returning a success value from a function that uses the result type.
Select value, then:
let value = computeResult()
let response = processResponse(value)
let value = computeResult()
let response = processResponse(Ok(value))
Wrap with Error(…)¶
Wraps a selected expression in Error(), useful when returning an error value from a function that uses the result type.
Select message on the second line, then:
let message = "Something went wrong"
let result = message
let message = "Something went wrong"
let result = Error(message)
Add @genType¶
Adds the @genType annotation above a let, type, or module declaration. This annotation tells the ReScript compiler to generate TypeScript type definitions for the declaration, enabling seamless interop with TypeScript code.
The intention is only available on declarations that do not already have a @genType annotation.
let greet = (name: string) => `Hello, ${name}!`
@genType
let greet = (name: string) => `Hello, ${name}!`
This also works on type and module declarations:
type user = {
name: string,
age: int,
}
@genType
type user = {
name: string,
age: int,
}
Generate doc comment¶
Inserts a /** ... */ documentation comment stub above the current declaration. For function declarations, @param tags are automatically generated for each parameter.
The intention is available on let, type, module, external, and exception declarations that do not already have a doc comment.
let add = (~a: int, ~b: int) => a + b
/**
*
* @param a
* @param b
*/
let add = (~a: int, ~b: int) => a + b
Pipe ⇔ Function Call Conversion¶
Convert between pipe-first syntax and regular function call syntax. Place the caret on a pipe expression or function call and press Alt+Enter.
Pipe to function call:
// Before
arr->Array.map(x => x + 1)
// After
Array.map(arr, x => x + 1)
Function call to pipe:
// Before
Array.map(arr, x => x + 1)
// After
arr->Array.map(x => x + 1)
Unit calls are preserved: getItems()->Array.length becomes Array.length(getItems()), and converting back restores getItems()->Array.length. Nested arguments, strings, comments, arrays/records and multiline expressions retain their source text. One undo restores the original expression.
These intentions handle -> and qualified function calls with positional arguments. They are not offered inside strings/comments or for |>, labeled/optional arguments, placeholders, JSX/templates, incomplete delimiters or ambiguous operator boundaries. Parenthesize complex expressions before conversion; unsupported input is left unchanged.
Conversions also work inside lambda bodies with plain positional parameters, such as items => items->Array.length. Typed or destructured lambda parameters and blocks with statements are currently unsupported. Argument shapes are checked recursively; balanced but unknown input such as Array.map(arr, a b) is not converted.
Publish/Unpublish Interface¶
Control which declarations are exposed in the .resi interface file directly from the .res implementation.
Publish to interface: Place the caret on a let, type, module, or external declaration in a .res file and press Alt+Enter, then choose Publish to interface. The declaration signature is appended to the corresponding .resi file.
Unpublish from interface: Place the caret on a declaration that exists in the .resi file and press Alt+Enter, then choose Unpublish from interface. The matching declaration is removed from the .resi file.
Note
These intentions require a corresponding .resi file to exist. Use Create Interface File to generate one first if needed.
Insert Labeled Arguments¶
When calling a function with labeled arguments, place the caret inside the function call parentheses and press Alt+Enter, then choose Insert labeled arguments. All labeled arguments are inserted as named parameters.
makeUser()
makeUser(~name, ~age, ~role)
Merge Switch Cases¶
When multiple switch cases have identical bodies, place the caret on one of them and press Alt+Enter, then choose Merge switch cases. The cases are combined into a single arm with multiple patterns.
switch status {
| Active => "valid"
| Pending => "valid"
| Inactive => "invalid"
}
switch status {
| Active | Pending => "valid"
| Inactive => "invalid"
}
Flatten Nested Switch¶
When a switch arm binds a value and its body is solely another switch over that same binding, the two levels can be collapsed into one. Place the caret on the outer arm and press Alt+Enter, then choose Flatten nested switch. Each inner pattern is folded into the binding position of the outer pattern, so | Some(y) => switch y { ... } produces one flattened arm per inner case.
switch outer {
| Some(y) =>
switch y {
| Some(z) => use(z)
| None => fallback
}
| None => empty
}
switch outer {
| Some(Some(z)) => use(z)
| Some(None) => fallback
| None => empty
}
The intention is purely syntactic and works without the language server. It is offered only when the outer arm has exactly one binding, the body is nothing but the inner switch over that binding, and neither switch uses when guards or inner or-patterns.
Case Split¶
Expand a pattern match variable into all possible constructor cases. Place the caret on a variable pattern in a switch arm and press Alt+Enter, then choose Case split.
switch option {
| x => handle(x)
}
switch option {
| Some(value) => handle(Some(value))
| None => handle(None)
}
Add Missing Switch Arms¶
Fill in the variant constructors that are not yet covered by an existing switch. Place the caret on the switch keyword (or anywhere inside the scrutinee) of a partial switch and press Alt+Enter, then choose Add missing switch arms. The intention queries LSP hover for the scrutinee’s type, computes the set difference between the variant’s constructors and the arms already present, and inserts a skeleton arm for each missing constructor just before the closing }. The intention does not appear when the switch already contains a wildcard _ arm or a bare lowercase binding pattern, because such arms make the switch exhaustive. Or-patterns (| Foo | Bar => ...) count both constructors as covered. In nested switches the innermost one containing the caret is targeted.
When the LSP hover returns just a bare type name (color, status) instead of an inline | Red | Blue body — which is what the ReScript LSP normally does for user-defined variants — the intention falls back to a project-wide stub-index lookup. The RescriptNameIndex is queried for a type <name> = ... declaration matching the hovered identifier, the right-hand side is re-parsed with the standard type declaration parser, and the resulting constructors feed back into the missing-arms set difference. The fallback runs silently and only when needed, so the intention now succeeds for the common case of “the variant is defined elsewhere in the project”.
let describe = (x: option<int>) =>
switch x {
| Some(v) => Belt.Int.toString(v)
}
let describe = (x: option<int>) =>
switch x {
| Some(v) => Belt.Int.toString(v)
| None => todo
}
Convert to Labeled Arguments¶
Convert positional function arguments to labeled arguments. Place the caret on a function call and press Alt+Enter.
makeUser("Alice", 30, "admin")
makeUser(~name="Alice", ~age=30, ~role="admin")
Remove Unnecessary Parentheses¶
Remove parentheses around a scalar literal or single identifier that forms the complete initializer of a simple let binding. Place the caret on the enclosing parentheses and press Alt+Enter. Strings and comments are preserved, and the edit can be undone once.
let x = (42)
let x = 42
Calls, tuples, unit values, lambda parameters, patterns, annotations, nested groups and operator expressions (including comparisons and pipes) are refused. Files containing type/module/external declarations, annotations, templates or JSX are outside the supported subset. The action leaves the code unchanged whenever it cannot confirm that the parentheses are redundant.
Remove Redundant Qualifier¶
Remove a qualifier only when a preceding open and a directly declared local module prove that the unqualified value names the same binding. Place the caret on the module identifier and press Alt+Enter. The module must have unique scalar literal exports, and the file must use the supported top-level declaration/value syntax.
module A = { let value = 1 }
open A
let result = A.value
module A = { let value = 1 }
open A
let result = value
External modules, aliases/re-exports, later or competing opens, include, shadowed names, nested scopes, type annotations, and unsupported syntax are refused. Strings and comments do not create opens or references. Intervening comments are retained, one undo restores the source, and a document change after availability invalidates the prepared edit.
Expand Open into Qualified References¶
The inverse of Remove Redundant Qualifier: rewrite the members introduced by an open M back into explicit M.name references and delete the now-redundant open statement. Place the caret on the open M line and press Alt+Enter.
This works purely on syntax and does not require the language server. It is available only when M resolves to a source file (M.res / M.resi) inside your project — the members to qualify are taken from that file’s top-level declarations. Library modules such as Belt (resolved from node_modules) are out of scope, so the intention is not offered for them. References that are already qualified, and members that are shadowed by a later local binding, are left untouched.
open M
let x = foo(bar)
let x = M.foo(M.bar)
Convert filter+map to filterMap¶
Convert a ->Array.filter(f)->Array.map(g) chain into a single ->Array.filterMap(...) call. Place the caret on the filter or map call and press Alt+Enter.
items
->Array.filter(x => x > 0)
->Array.map(x => x * 2)
items->Array.filterMap(x => {
if x > 0 {
Some(x * 2)
} else {
None
}
})
Add Type Annotation¶
Add an explicit type annotation to a let binding using type information from the LSP hover. Place the caret on a let binding and press Alt+Enter.
let name = "Alice"
let name: string = "Alice"
Insert Inferred Type Annotations¶
Annotate every un-annotated top-level let in the current file in one pass, rather than invoking Add type annotation binding by binding. Place the caret anywhere in the file and press Alt+Enter, then choose Insert inferred type annotations. The IDE resolves each inferred type from the LSP hover and applies all insertions in a single undoable step.
The same action is also available from the Type Coverage Heat Map tool window: select a low-coverage file in the table and click the Insert Inferred Types toolbar button to annotate it without opening it first.
let greeting = "hello"
let count = 42
let double = x => x * 2
let greeting: string = "hello"
let count: int = 42
let double: int => int = x => x * 2
Bindings that already carry an annotation are left untouched. Types the language server cannot express as a stable annotation — weak type variables such as '_a, or hover text that still contains Markdown residue — are skipped, and the result balloon reports how many bindings were annotated and how many were skipped. The action requires a running language server.
Add ->ignore¶
Append ->ignore to an expression whose return value is unused, suppressing the compiler warning about discarded values. Place the caret on the expression and press Alt+Enter.
Js.log("debug message")
Array.push(items, newItem)
Js.log("debug message")->ignore
Array.push(items, newItem)->ignore
Add _ Prefix¶
Add an underscore prefix to a variable name to indicate it is intentionally unused. This suppresses the compiler’s unused variable warning. Place the caret on an unused variable and press Alt+Enter.
let result = someComputation()
let _result = someComputation()
Remove Redundant Braces¶
Remove unnecessary { } around a single expression. Place the caret on the braces and press Alt+Enter.
let greet = (name) => {
"Hello, " ++ name
}
let greet = (name) => "Hello, " ++ name
Fix Identifier Case¶
Correct identifier casing to follow ReScript conventions: values and functions should start with a lowercase letter, modules and variant constructors should start with an uppercase letter. Place the caret on a misnamed identifier and press Alt+Enter.
let MyValue = 42
let myValue = 42
Expand Destructuring¶
Expand a destructured let binding into individual let bindings for each field. Place the caret on a destructuring pattern and press Alt+Enter.
let {name, age, email} = user
let name = user.name
let age = user.age
let email = user.email
Convert Call to Uncurried Form¶
Rewrites a curried call site to the uncurried form f(. x) when the target function is defined with the legacy uncurried syntax let f = (. x) => .... Provided as a native fallback for the LSP applyUncurried quick fix on ReScript v10 / v11 codebases.
Place the caret on the call’s identifier and press Alt+Enter, then choose Convert call to uncurried form.
let add = (. x, y) => x + y
let result = add(1, 2)
let add = (. x, y) => x + y
let result = add(. 1, 2)
The intention only inspects the first matching definition found in the stub index. The zero-argument form f() is rewritten to f(.).
Extract Module to File¶
Moves a top-level module M = { ... } declaration out of the current file and into a sibling M.res file. Provided as a native fallback for the LSP extractLocalModuleToFile code action when the language server’s WorkspaceEdit.CreateFile operation is not honoured by the IDE.
Place the caret on the module declaration’s name and press Alt+Enter, then choose Extract module to file.
module Inner = {
let value = 42
let double = x => x * 2
}
let total = Inner.double(Inner.value)
Foo.res:
let total = Inner.double(Inner.value)
Inner.res (newly created):
let value = 42
let double = x => x * 2
The intention does not rewrite references to the extracted module elsewhere in the source file. When such references are detected, a warning balloon appears so you can adjust the call sites manually (e.g., add open Inner or update qualified paths). The intention is hidden when a sibling Inner.res file already exists.
Surround With¶
Native
Select code and press Ctrl+Alt+T to surround it with:
ifexpressionswitchexpressiontry/catchblockBlock scope
{ ... }JSX element
<div>...</div>JSX fragment
<>...</>
if expression¶
Wraps the selected code inside an if block. The cursor is placed on the condition placeholder so you can immediately type the condition.
Select Js.log("hello"), then:
Js.log("hello")
if (condition) {
Js.log("hello")
}
switch expression¶
Wraps the selected code inside a switch expression with a default _ arm. The cursor is placed on the expr placeholder so you can type the expression to match on.
Select defaultHandler(), then:
defaultHandler()
switch expr {
| _ => defaultHandler()
}
try / catch block¶
Wraps the selected code in a try / catch block. The cursor is placed on the () placeholder in the catch arm so you can define the error handling logic.
Select parseJson(input), then:
parseJson(input)
try {
parseJson(input)
} catch {
| exn => ()
}
Block scope { … }¶
Wraps the selected code in a block scope. This is useful for limiting variable scope or grouping expressions.
Select the two lines, then:
let temp = calculate()
process(temp)
{
let temp = calculate()
process(temp)
}
JSX element …¶
Wraps the selected markup in a <div> element. The cursor selects the opening tag name div so you can immediately rename it to the desired element.
Select <Greeting name />, then:
<Greeting name />
<div>
<Greeting name />
</div>
JSX fragment <>…</>¶
Wraps the selected markup in a JSX fragment <>...</>, which groups sibling elements without introducing an extra DOM node.
Select the two lines, then:
<Header />
<Body />
<>
<Header />
<Body />
</>
Surround With wraps selected code in a control structure with one shortcut, saving you from manually typing the surrounding syntax and re-indenting the enclosed code.
See also
Code Completion offers postfix templates (.switch, .pipe) and live templates for similar code generation patterns.
Import Optimization¶
Native
Press Ctrl+Alt+O to optimize imports in the current file.
How It Works¶
Import optimization removes an adjacent repeated open only when its target is a proven local module in the same scope. Currently, the supported modules contain literal let bindings only and export no modules. Comments and whitespace between the opens are allowed.
Reopening a module after another open or declaration can change reference resolution, so it is preserved. Unknown module targets, aliases and nested module exports are also preserved. The inspection and quick fix use the same proof, and the quick fix rechecks the current source before editing.
Configuration¶
Automatic removal based only on an unused-open warning is suspended because the warning may belong to an older source version. The stored Remove unused opens preference is retained and its control is disabled; it currently does not authorize deletion without a current semantic proof. Unknown cases leave the source unchanged.
Example¶
module A = {let value = 1}
open A
open A
Console.log(value)
module A = {let value = 1}
open A
Console.log(value)
Rename¶
LSP Required
Press Shift+F6 to rename a symbol across the project. The Language Server handles finding all references and updating them.
Rename refactoring updates every reference across the entire project in one operation, eliminating the risk of missed or inconsistent renames that manual find-and-replace would leave behind.
Extract Variable¶
Native
Press Ctrl+Alt+V (Cmd+Alt+V on macOS) to extract the selected expression into a let binding.
The handler detects the selected expression within a let declaration body, creates a new let binding above the current statement, and replaces the original expression with a reference to the new variable.
Select a + b, then:
let calculate = (a, b) => {
let result = a + b * 2
result
}
let calculate = (a, b) => {
let sum = a + b
let result = sum * 2
result
}
Extract Variable lets you name intermediate expressions for clarity, making complex expressions self-documenting without manually restructuring the surrounding code.
Extract Function¶
Native
Press Ctrl+Alt+M (Cmd+Alt+M on macOS) to extract the selected code into a new function.
The handler resolves references to supported lexical bindings and extracts the selected expression or statement range into a new let function immediately before the selected statement in the same block. Only bindings declared outside the selection become parameters. Strings, comments, qualified members and record labels do not become parameters. The declaration and call site are edited in one undoable command, and the generated name avoids existing identifiers. Top-level helpers are private; selecting an entire exported declaration is refused.
Select a + b, then:
let calculate = (a, b) => {
let result = a + b
result * 2
}
let calculate = (a, b) => {
let extractedFunction = (a, b) => {
a + b
}
let result = extractedFunction(a, b)
result * 2
}
Simple let bindings and block-body lambda parameters are supported. Local bindings within the selection stay local to the extracted function; extraction is refused if those bindings are used outside the selection. Rebinding, destructuring, lambda values inside the selection, unknown field values, bare module members, constructors, partial applications, opens, modules, templates, raw JavaScript, shorthand labels, potentially generalized bindings (including function call results) and unresolved names are conservatively rejected. Record literals, array/index expressions, placeholders, return-type annotations and selection across statement continuations are also refused because the required type or insertion context is not proven. Complete project-wide semantic resolution is not yet available. Field reads and updates are limited to .contents on a verified ref initializer. Calls and ref updates remain in the extracted body and execute once at the original call site.
Inline Constant¶
Native
Press Ctrl+Alt+N (Cmd+Alt+N on macOS) on a single-line let declaration to inline a literal-only constant expression.
The handler validates lexical scope and replaces only identifier references, preserving strings, comments and qualified fields. Every replacement is parenthesized to preserve expression precedence. Local declarations are removed in the same undoable command; top-level declarations remain available to other modules.
Calls, variable-dependent expressions, division, multiline declarations, comments on the declaration and type annotations are unavailable. Files containing rebinding, unresolved patterns, templates, raw JavaScript, eval, annotations, opens or modules are conservatively unavailable until semantic reference resolution is supported. Parameters that shadow the selected name, record shorthand and labelled arguments are rejected when their role cannot be established.
let result = {
let sum = 1 + 2
sum * 3
}
let result = {
(1 + 2) * 3
}
The result remains 9, and one undo restores both the declaration and every edited reference.
Introduce Constant¶
Native
Extract a literal value into a module-level constant. Select a string, number, or other literal and use Refactor > Introduce Constant.
The handler extracts the selected literal into a new let binding at the top of the file (after any open statements) and replaces the original literal with a reference to the new constant.
Introduce constant for "Hello, World!":
let greeting = "Hello, World!"
let farewell = "Goodbye, World!"
let helloWorld = "Hello, World!"
let greeting = helloWorld
let farewell = "Goodbye, World!"
Introduce Constant extracts magic values into named bindings at the module level, making their intent explicit and enabling reuse across the file.
Change Signature¶
Native
Press Ctrl+F6 to modify a function’s parameters — reorder, rename, add, or remove — and update call sites within the file.
The handler parses the function declaration at the caret, supports both labeled (~name: type) and positional parameters, and applies changes to the declaration and all matching call sites.
Reorder parameters:
let make = (~name: string, ~age: int) => { name, age }
let user = make(~name="Alice", ~age=30)
let make = (~age: int, ~name: string) => { name, age }
let user = make(~age=30, ~name="Alice")
Change Signature lets you restructure a function’s parameter list and automatically updates all call sites, making API evolution safe and mechanical rather than error-prone manual editing.
React Component Extraction¶
Native
Extract selected JSX into a new React component. Select a JSX expression, then use Refactor > Extract React Component.
The handler creates a new @react.component module with the selected JSX as its body. Props used within the selection are detected and added as labeled parameters to the new component’s make function.
Select <div className="card">...</div>, then:
@react.component
let make = (~items) => {
<div className="container">
<div className="card">
{items->Array.map(item => <span key={item}> {React.string(item)} </span>)->React.array}
</div>
</div>
}
module Card = {
@react.component
let make = (~items) => {
<div className="card">
{items->Array.map(item => <span key={item}> {React.string(item)} </span>)->React.array}
</div>
}
}
@react.component
let make = (~items) => {
<div className="container">
<Card items />
</div>
}
Extracting a React component from JSX is a common refactoring in React development — this automation detects the required props and creates a properly structured module, saving you from manually copying code and threading props.
Paste as JSON.t¶
Native
Use Edit > Paste as JSON.t to convert JSON from your clipboard into a ReScript JSON.t value.
Instead of manually wrapping each JSON value in ReScript constructors, paste the raw JSON and get a correctly typed JSON.t expression instantly.
Code Generation¶
Native
Press Cmd+N (or Alt+Insert) to open the Generate menu:
Generate Switch Arms — Auto-generate match arms for a variant type
Generate Module Type — Generate a module type skeleton from a module implementation
Generate Make Function — Generate a constructor function from a record type
Generate JSON Encoder/Decoder — Generate JSON encoder and decoder functions from a type
Generate Record Value — Generate a record value with default values for all fields
Generate Switch Arms¶
When your caret is inside a variant type declaration, this action generates a switch expression with one arm per constructor. This helps you write exhaustive pattern matches without manually listing every variant.
Place your caret inside the type declaration and press Cmd+N (or Alt+Insert), then choose Switch Arms.
type shape =
| Circle(float)
| Rectangle(float, float)
| Triangle(float, float, float)
| Point
type shape =
| Circle(float)
| Rectangle(float, float)
| Triangle(float, float, float)
| Point
switch value {
| Circle(_) => todo
| Rectangle(_) => todo
| Triangle(_) => todo
| Point => todo
}
Constructors that carry a payload (e.g., Circle(float)) get a _ wildcard in the generated arm, while constructors without a payload (e.g., Point) match directly. Replace todo with your actual logic for each arm.
Generate Module Type¶
When your caret is inside a module declaration, this action generates a module type signature skeleton and inserts it above the module. The generated signature includes all let, type, external, and nested module declarations found in the module body.
Place your caret inside the module declaration and press Cmd+N (or Alt+Insert), then choose Module Type.
module StringUtils = {
type config = {verbose: bool}
let capitalize = (s: string) => {
// ...
}
let truncate = (s: string, maxLen: int) => {
// ...
}
module Internal = {
let helper = () => ()
}
}
module type StringUtilsType = {
type config
let capitalize: 'a
let truncate: 'a
module Internal: {}
}
module StringUtils = {
type config = {verbose: bool}
let capitalize = (s: string) => {
// ...
}
let truncate = (s: string, maxLen: int) => {
// ...
}
module Internal = {
let helper = () => ()
}
}
The generated type uses 'a as a placeholder for value types – replace these with the actual type signatures. Type declarations appear without their definition body so you can specify the exposed type shape. Nested modules are listed with an empty {} signature for you to fill in.
Generate Make Function¶
When your caret is inside a record type declaration, this action generates a make constructor function with labeled arguments for each record field.
Place your caret inside the type declaration and press Cmd+N (or Alt+Insert), then choose Make Function.
type user = {
name: string,
age: int,
email: string,
}
type user = {
name: string,
age: int,
email: string,
}
let make = (~name, ~age, ~email) => {
name,
age,
email,
}
Optional fields (e.g., email?: string) are generated as optional labeled arguments (~email=?).
Generate JSON Encoder/Decoder¶
When your caret is inside a record or variant type declaration, this action generates JSON encoder and decoder functions using @rescript/core’s JSON module (no external dependencies required).
Place your caret inside the type declaration and press Cmd+N (or Alt+Insert), then choose JSON Encoder/Decoder.
Record type example:
type user = {name: string, age: int, email: option<string>}
Generates an encoder that converts each field to the appropriate JSON.t constructor (String, Number, Boolean, Null, Object, Array) and a decoder that pattern-matches the JSON structure back into the record type.
Variant type example (simple enum):
type color = Red | Green | Blue
Generates a String-based encoder/decoder where each constructor maps to its name as a JSON string.
Variant type example (tagged union):
type status = Success(string) | Error(int)
Generates a tagged union encoder/decoder using Object with a "tag" field and positional payload fields ("_0", "_1", …).
Supported types: string, int, float, bool, option<T>, array<T>, and arbitrary nesting (e.g., option<array<string>>). Unrecognized types generate a /* TODO */ placeholder for you to fill in.
Naming convention: Functions are named encode + capitalized type name and decode + capitalized type name (e.g., encodeUser / decodeUser). For a type named t, the functions are simply encode / decode.
Generate Record Value¶
When your caret is inside a record type declaration, this action generates a record value with default values for all fields. This is useful for quickly creating an initial value or test fixture.
Place your caret inside the type declaration and press Cmd+N (or Alt+Insert), then choose Record Value.
type user = {
name: string,
age: int,
active: bool,
email: option<string>,
}
type user = {
name: string,
age: int,
active: bool,
email: option<string>,
}
let value: user = {
name: "",
age: 0,
active: false,
email: None,
}
Default values are inferred from field types: "" for string, 0 for int, 0.0 for float, false for bool, None for option<T>, and [] for array<T>.
Generate Module Type Implementation¶
When your caret is inside a module type declaration, this action generates a module that implements the module type with stub functions for each signature entry.
Place your caret inside the module type declaration and press Cmd+N (or Alt+Insert), then choose Module Type Implementation.
module type Printable = {
type t
let toString: t => string
let print: t => unit
}
module type Printable = {
type t
let toString: t => string
let print: t => unit
}
module PrintableImpl: Printable = {
type t
let toString = (_) => todo
let print = (_) => todo
}
Replace todo placeholders with actual implementations. The generated module is constrained to the module type, so the compiler will catch any missing or incorrect signatures.
Code generation turns type definitions into working code — switch arms, constructors, JSON codecs, and module skeletons are created automatically from your types, eliminating tedious and error-prone boilerplate writing.
Strip Trailing Spaces¶
Native
The plugin protects whitespace inside string literals when IntelliJ removes trailing spaces on save. Without this, intentional whitespace at the end of a string line would be stripped.
Protected token types:
STRING_VALUE— regular string contentJS_STRING_OPEN/JS_STRING_CLOSE— JavaScript template string boundaries
Configure trailing space behavior in Settings > Editor > General > On Save.
This protection ensures that intentional whitespace inside string literals is never silently removed by the editor’s trailing space cleanup, preventing subtle bugs in string-heavy code.
Editor Floating Toolbar¶
Native
A floating toolbar appears when editing ReScript files, providing quick access to commonly used actions:
Format — Format the current file (
Cmd+Option+L)Open Compiled JS — Open the compiled JavaScript output
Create Interface — Generate a
.resiinterface file
The toolbar auto-shows when you start editing a ReScript file and auto-hides when not in use.
The floating toolbar puts the most common ReScript-specific actions — format, view compiled JS, create interface — one click away, without memorizing keyboard shortcuts.
Spellchecking¶
Native
The plugin supports IntelliJ’s built-in spellchecker for:
Comments (line and block)
String literals
Identifiers (camelCase splitting)
Built-in spellchecking catches typos in comments, strings, and identifiers as you type, helping you maintain professional code quality without a separate review step.
Backspace Handler¶
Native
When you delete an opening JSX tag with backspace, the matching closing tag is automatically removed.
Paired tag deletion prevents orphaned closing tags — when you backspace through an opening JSX tag, the closing tag disappears automatically, keeping your markup balanced.
Move Element Left/Right¶
Native
Use Alt+Shift+Cmd+Left / Alt+Shift+Cmd+Right to swap comma-separated elements. Works with function arguments, array items, record fields, and tuple elements.
Reorder arguments, array items, or record fields with a keyboard shortcut instead of manually cutting and pasting, keeping commas and formatting correct automatically.
Code Block Selection¶
Native
Use Ctrl+Shift+[ and Ctrl+Shift+] to navigate to the start and end of the enclosing code block (brace-delimited).
Quickly jump to block boundaries when working inside large function bodies or deeply nested structures, without scrolling or searching for the matching brace.
Split/Join List¶
Native
Toggle comma-separated lists between single-line and multi-line format. Available via Edit > Split/Join List.
Switching between compact and expanded list formats is a single action, so you can adjust code density for readability without manually adding or removing line breaks and trailing commas.
Copy/Paste Escaping¶
Native
When pasting text into a string literal, special characters (backslash, quotes, newlines, tabs) are automatically escaped.
Automatic escaping means you can paste raw text into a string literal and trust that special characters are handled correctly, avoiding hard-to-spot syntax errors from unescaped quotes or backslashes.
Unwrap/Remove¶
Native
Press Ctrl+Shift+Delete (Cmd+Shift+Delete on macOS) to remove a surrounding wrapper and extract the inner expression.
A popup lists all applicable unwrap options at the cursor position. Select one to unwrap.
Supported Wrappers¶
Wrapper |
Before |
After |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Example:
let greeting = Some("Hello, world!")
let greeting = "Hello, world!"
Unwrap/Remove is the inverse of Surround With — it strips away a wrapper like Some(), if, or try in one action, simplifying expressions without manually deleting delimiters and re-indenting.
JSX Auto-Close Tag¶
Native
When you type > to close a JSX opening tag, the corresponding closing tag is automatically inserted and the cursor is positioned between the tags.
Type <div>:
<div>
<div></div>
This works for:
HTML elements:
<div>,<span>,<input>React components:
<MyComponent>Module-qualified components:
<Module.Component>
The auto-close does not trigger for self-closing tags (e.g., <br />), inside comments, or inside string literals.
Auto-closing tags saves keystrokes and prevents mismatched JSX tags — type the opening tag and the closing tag appears automatically, keeping your markup balanced as you write.
Enter Handler (Comment Continuation)¶
Native
When you press Enter inside a comment, the next line is automatically prefixed with the appropriate comment continuation characters.
Documentation Comments¶
Inside a /** ... */ block, pressing Enter inserts * at the start of the new line:
/** Some documentation|
After pressing Enter:
/** Some documentation
* |
Line Comments¶
Inside a // comment, pressing Enter inserts // at the start of the new line:
// This is a comment|
After pressing Enter:
// This is a comment
// |
Doc comments (///) are also continued:
/// A doc comment|
After pressing Enter:
/// A doc comment
/// |
Comment continuation keeps your multi-line comments properly formatted without manually typing prefixes on each new line, maintaining a clean comment style effortlessly.
Smart Join Lines¶
Native
Press Ctrl+Shift+J (Cmd+Shift+J on macOS) to join the current line with the next line using ReScript-aware logic.
Pipe Chain Join¶
When a line ends with -> or the next line starts with ->, the lines are joined without adding a space:
value->
Array.map(x => x + 1)
value->Array.map(x => x + 1)
Let Binding Join¶
When a line ends with =, the lines are joined with a single space after =:
let result =
computeValue()
let result = computeValue()
Arrow Function Join¶
When a line ends with =>, the lines are joined with a single space:
let fn = (x) =>
x + 1
let fn = (x) => x + 1
For patterns not recognized above, the standard IDE join behavior is used.
Smart join understands ReScript syntax — pipe chains join without spaces, let bindings preserve their =, and arrow functions stay readable, producing cleaner results than generic line joining.
Rename Variant Constructor (no LSP)¶
Native
The standard Shift+F6 rename routes through textDocument/rename on the language server, which gives the most accurate cross-file rewrite. When the language server isn’t running — node not on PATH, @rescript/language-server not installed, network-restricted environments — that path is unavailable. This Alt+Enter intention provides a token-shape fallback that works everywhere.
Trigger: place the caret on a UIDENT used as a variant constructor (an arm pattern like | Foo(_) =>, a type declaration arm | Foo, or a constructor invocation Foo(x)), press Alt+Enter, and pick Rename variant constructor (project-wide, no LSP).
How It Works¶
RescriptConstructorOccurrenceClassifier decides what each UIDENT in the project represents — CONSTRUCTOR, PATTERN, MODULE_QUALIFIED_TAIL, or OTHER — by looking at the previous and next non-trivia tokens:
Surrounding tokens |
Verdict |
|---|---|
|
PATTERN (switch arm or |
|
MODULE_QUALIFIED_TAIL (only the |
|
CONSTRUCTOR (constructor invocation) |
|
CONSTRUCTOR (zero-arity in expression position) |
|
OTHER (skipped) |
Tokens inside string literals or comments |
OTHER (lexer handles this) |
RescriptConstructorOccurrenceFinder then runs PsiSearchHelper.processElementsWithWord over .res and .resi files in the project, classifies each hit, and keeps the CONSTRUCTOR / PATTERN / MODULE_QUALIFIED_TAIL ones. Up to 500 occurrences are collected; beyond that the intention surfaces a “use Shift+F6 (LSP rename) or narrow the search” error so you don’t accidentally produce an enormous diff.
The actual rewrite happens in a single WriteCommandAction, so the entire project-wide change Undoes as one step.
Limitations¶
The classifier is purely syntactic. Two different variants with the same constructor name (e.g.
type a = | Fooandtype b = | Foo) are renamed together — useShift+F6(LSP rename) when you need disambiguation by type.Polymorphic variants (
#foo) are not in scope for this intention.Module-qualified call sites are renamed by their tail only; the leading
Module.segment is never touched.
JSX Paired Tag Rename¶
Native
When you edit a JSX tag name, the change is mirrored live onto its matching tag on the other side of the element, so the opening and closing names never drift apart. Typing or deleting characters in <div> keeps the trailing </div> in sync as you go, and the whole synchronized edit Undoes in a single step.
Example:
// Caret right after "div" in the opening tag; type "x"
<div></div>
// Result — both names update together
<divx></divx>
The mirror only fires while the two names still match (the side you are editing matched the other side before the keystroke), so it never overwrites an intentionally different closing name. It does nothing for self-closing tags (<br />) or fragments (<>...</>), which have no second name to keep in sync.
Synchronized renaming removes the manual step of fixing the closing tag after changing the opening one — rename in place and the counterpart follows, with a single Undo to back it all out.
Word Selection (Extend/Shrink)¶
Native
Use Ctrl+W (Alt+Up on macOS) to extend the selection and Ctrl+Shift+W (Alt+Down on macOS) to shrink it, with ReScript-aware boundaries.
Selection Boundaries¶
The word selector recognizes the following ReScript-specific boundaries for extend/shrink selection:
Context |
Selection Expands To |
|---|---|
Inside a string literal |
String content (without quotes) → Entire string (with quotes) |
Inside parentheses |
Content inside parens → Including parentheses |
Inside block comment |
Comment content → Entire comment including delimiters |
Inside line comment |
Comment text → Entire comment line |
This provides more natural selection behavior than the default word selector when working with ReScript code structures.
ReScript-aware selection boundaries make it easy to select exactly the content you need — string contents without quotes, block comment text without delimiters — using the standard extend/shrink shortcuts.
Paste as JSX¶
Native
When pasting HTML content into a ReScript file, it is automatically converted to JSX syntax:
HTML attributes (
class,for,onclick) are renamed to JSX equivalents (className,htmlFor,onClick)Void elements (
<br>,<img>,<input>) are self-closed (<br />)Inline
stylestrings are converted to ReScript style objectsBoolean attributes (e.g.,
disabled,checked) are preserved as JSX boolean propsdata-*andaria-*attributes are preserved as-is
Paste as JSX converts HTML to ReScript JSX automatically — instead of manually renaming class to className and self-closing void elements, the conversion happens on paste.
Paste as ReScript¶
Native
This conversion is triggered automatically when you paste JavaScript or TypeScript code into a .res file using the standard paste shortcut (Cmd+V / Ctrl+V). No separate menu action is needed — the plugin detects JavaScript/TypeScript patterns in the clipboard and converts them on the fly.
When pasting JavaScript or TypeScript code into a ReScript file, it is automatically converted to ReScript syntax:
JavaScript conversions¶
const/let/vardeclarations are converted toletbindingsArrow functions
(x) => { return x }are simplified to(x) => x===/!==are converted to==/!=console.log(x)is converted toJs.log(x)null/undefinedare converted toNoneTemplate literals remain as-is (ReScript uses the same syntax)
TypeScript type stripping¶
Variable type annotations (
const x: string = ...) are removedFunction parameter type annotations (
(a: number, b: string)) are removedReturn type annotations (
): boolean {) are removedType assertions (
value as string) are removedinterfaceandenumdeclarations are commented outexport type/export interface/export enumare commented out
JSX/TSX pattern conversion¶
Conditional rendering
{condition && <expr>}is converted to{condition ? <expr> : React.null}Array methods (
.map(),.filter(),.forEach()) are converted to pipe-first style (->Array.map())JSX spread
{...props}gets a warning comment (not supported in ReScript JSX)
const greeting = (name: string): string => {
console.log("Hello, " + name);
return name.toUpperCase();
}
let greeting = (name) => {
Js.log("Hello, " ++ name)
name->String.toUpperCase
}
Paste as ReScript bridges the gap between JavaScript/TypeScript and ReScript — copy code from documentation, Stack Overflow, or existing JS/TS files and paste it directly into a .res file with automatic syntax conversion.
Note that React JSX/TSX code (containing className=, {expression} braces, or camelCase event handlers like onClick=) is handled by the JS/TS→ReScript converter rather than the HTML→JSX converter, ensuring correct treatment of JSX patterns.
Comment Toggle¶
Native
Ctrl+/— Toggle line comment (//)Ctrl+Shift+/— Toggle block comment (/* */)Quickly toggle comments on and off for debugging or temporarily disabling code, without manually typing or removing comment delimiters.