https://github.com/nicklayb/codemirror-loupe
https://github.com/nicklayb/codemirror-loupe
Last synced: 5 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/nicklayb/codemirror-loupe
- Owner: nicklayb
- License: mit
- Created: 2025-12-18T01:17:03.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-12-20T01:42:49.000Z (6 months ago)
- Last Synced: 2025-12-22T01:07:49.305Z (6 months ago)
- Language: TypeScript
- Size: 45.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# CodeMirror Language Support for Loupe
CodeMirror 6 language extension providing syntax highlighting and language support for the [Loupe query language](https://github.com/nicklayb/loupe).
## What is Loupe?
Loupe is a query language for safe and configurable inspection of Ecto schemas in Elixir applications. It provides a declarative syntax for querying data with support for:
- Quantifiers (ranges, multipliers)
- Complex predicates with boolean logic
- Field variants and path binding
- JSON-like parameters
- Safe, configurable schema inspection
## Features
- Full syntax highlighting for Loupe queries
- Automatic code folding for nested structures
- Comment support (`#` line comments)
- Smart indentation
- Built on CodeMirror 6 and Lezer parser
## Installation
```bash
npm install @nicklayb/codemirror-loupe
```
## Usage
### Basic Setup
```typescript
import { EditorView, basicSetup } from 'codemirror';
import { loupe } from '@nicklayb/codemirror-lang-loupe';
new EditorView({
doc: 'get User where email = "user@example.com"',
extensions: [basicSetup, loupe()],
parent: document.querySelector('#editor')
});
```
### With Custom Theme
```typescript
import { EditorView, basicSetup } from 'codemirror';
import { loupe } from '@nicklayb/codemirror-lang-loupe';
new EditorView({
doc: 'get all Post where status = "published"',
extensions: [
basicSetup,
loupe(),
EditorView.theme({
'&': { fontSize: '16px' },
'.cm-content': { fontFamily: 'Monaco, monospace' }
})
],
parent: document.querySelector('#editor')
});
```
### With Autocompletion (Callback-Based)
Enable smart, dynamic autocompletion with callback functions. This allows you to provide schemas and fields on-demand, supporting nested field navigation:
```typescript
import { EditorView, basicSetup } from 'codemirror';
import { autocompletion } from '@codemirror/autocomplete';
import { loupe, loupeCompletion } from '@nicklayb/codemirror-lang-loupe';
new EditorView({
doc: 'get User where ',
extensions: [
basicSetup,
loupe(),
autocompletion({
override: [loupeCompletion({
// Provide available commands
getCommands: () => [
{ label: 'get', type: 'keyword', info: 'Get records' },
{ label: 'find', type: 'keyword', info: 'Find records' },
{ label: 'fetch', type: 'keyword', info: 'Fetch records' }
],
// Provide available schemas
getSchemas: () => [
{ label: 'User', type: 'type', info: 'User account' },
{ label: 'Post', type: 'type', info: 'Blog posts' }
],
// Provide fields dynamically based on context
getFields: (context) => {
const { schema, fieldPath } = context;
// Root-level fields
if (fieldPath.length === 0) {
if (schema === 'User') {
return [
{ label: 'id', type: 'property', detail: 'integer' },
{ label: 'email', type: 'property', detail: 'string' },
{ label: 'role', type: 'property', detail: 'Role' }
];
}
}
// Nested fields (e.g., after typing "role.")
if (fieldPath[fieldPath.length - 1] === 'role') {
return [
{ label: 'name', type: 'property', detail: 'string' },
{ label: 'level', type: 'property', detail: 'integer' }
];
}
return [];
}
})]
})
],
parent: document.querySelector('#editor')
});
```
**Autocompletion features:**
- **Fully callback-based**: Provide commands, schemas, and fields dynamically at runtime
- **Nested field navigation**: Type dots to navigate nested fields (e.g., `user.role.name`)
- **Context-aware**: The `getFields` callback receives the current schema and field path
- **Async support**: All callbacks can return Promises for fetching data from APIs
- **Command suggestions**: Provided via `getCommands` callback at the beginning
- **Schema suggestions**: Provided via `getSchemas` callback after the command
- **Field suggestions**: Provided via `getFields` callback after `where` and logical operators
- **Operator suggestions**: After field names (=, !=, <, >, <=, >=, in, like)
- **Keyword suggestions**: where, and, or, not, in, like, empty
**Context object passed to `getFields`:**
```typescript
interface LoupeCompletionContext {
schema: string; // Current schema (e.g., "User")
fieldPath: string[]; // Current path (e.g., ["role"] for "role.")
type: 'field' | ...; // Type of completion
}
```
## Loupe Query Syntax Examples
### Basic Query
```loupe
get User where email = "user@example.com"
```
### Query with Quantifier
```loupe
get 10 Post where status = "published" and views > 1k
```
### Query with Range
```loupe
get 5..10 Comment where created_at > "2024-01-01"
```
### Query with Parameters
```loupe
get Article {title: "Hello", draft: false} where author_id = user_id
```
### Complex Query with Nested Conditions
```loupe
get all Transaction where
(amount >= 100 and amount <= 1000) or
status = "pending"
```
### Query with IN Operator
```loupe
get User where role in ["admin", "moderator"]
```
### Query with LIKE Operator
```loupe
get Product where name like "iPhone"
```
### Query with Field Variant
```loupe
get Account where balance:amount >= 10k
```
### Query with Path Binding
```loupe
get User where role.permissions[posts, access] = "write"
```
### Grouped Field OR (`|`)
```loupe
get Post where title | description like "search term"
```
The `|` operator checks if **any** of the grouped fields match the condition.
### Grouped Field AND (`&`)
```loupe
get Product where price & discount > 100
```
The `&` operator checks if **all** of the grouped fields match the condition.
### Query with NOT and EMPTY
```loupe
get Document where not status:empty and published = true
```
## Development
### Prerequisites
- Node.js 20 or later
- npm or pnpm
### Setup with Nix (Recommended)
If you have Nix with flakes enabled:
```bash
nix develop
npm install
npm run build
```
### Manual Setup
```bash
npm install
npm run build
```
### Available Scripts
- `npm run build:parser` - Generate the Lezer parser from grammar
- `npm run build` - Build the library (runs parser generation + rollup)
- `npm run dev` - Run the example app in development mode
### Project Structure
```
loupejs/
├── src/
│ ├── grammar.lezer # Lezer grammar definition
│ ├── highlight.ts # Syntax highlighting rules
│ └── index.ts # Main entry point
├── example/
│ ├── index.html # Example app HTML
│ └── main.js # Example app code
├── dist/ # Build output
├── flake.nix # Nix development environment
├── package.json
├── tsconfig.json
└── rollup.config.js
```
## Running the Example
To see the language support in action:
```bash
npm run dev
```
Then open your browser to the URL shown (typically http://localhost:5173 or http://localhost:5174).
**Important**: You must access the example through the Vite dev server URL. Opening the HTML file directly (file://) won't work because ES modules with bare imports require a development server to resolve properly.
## Building from Source
1. Clone the repository
2. Install dependencies: `npm install`
3. Build the parser: `npm run build:parser`
4. Build the library: `npm run build`
The compiled library will be in the `dist/` directory.
## Language Features
### Supported Tokens
- **Commands**: Any identifier (e.g., `get`, `find`, `fetch`)
- **Keywords**: `where`, `all`, `in`, `like`, `empty`, `and`, `or`, `not`
- **Comparison Operators**: `=`, `!=`, `<`, `>`, `<=`, `>=`
- **Grouped Field Operators**: `|` (OR for fields), `&` (AND for fields)
- **Boolean Operators**: `and`, `or`, `not`
- **Quantifiers**: Numbers, ranges (`1..10`), multipliers (`k`, `m`)
- **Values**: Strings, numbers, booleans, identifiers
- **Structures**: Parameters `{key: value}`, lists `[value1, value2]`
- **Advanced**: Field variants (`:variant`), path binding (`[field1, field2]`)
- **Comments**: Line comments starting with `#`
**Note**: The `|` and `&` operators are for grouping fields (e.g., `field1 | field2 = "value"`), while `and`/`or` are for combining predicates (e.g., `field1 = "a" and field2 = "b"`).
### Syntax Highlighting
The extension provides semantic highlighting for:
- Keywords (purple/blue)
- Operators (red/orange)
- Strings (green)
- Numbers (blue)
- Comments (gray)
- Identifiers and schema names
- Field variants and path bindings
## License
MIT
## Related Projects
- [Loupe](https://github.com/nicklayb/loupe) - The Loupe query language for Elixir
- [CodeMirror 6](https://codemirror.net/) - Extensible code editor
- [Lezer](https://lezer.codemirror.net/) - Incremental parser system
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Author
Nicolas Boisvert
## Acknowledgments
Built with CodeMirror 6 and Lezer parser generator.