This document describes RangeLink's architecture, design principles, and implementation strategies for both the current TypeScript monorepo and future multi-language expansion.
- Current Architecture
- Design Principles
- Core Library Design
- Extension Layer Design
- Multi-Language Vision
- Related Documentation
For details on the monorepo structure and package organization, see packages/README.md.
┌─────────────────────────────────┐
│ rangelink-vscode-extension │
│ │
│ ┌─────────────────────────────┐ │
│ │ Commands Layer │ │ ← User interactions
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────────▼──────────────────┐ │
│ │ Configuration Layer │ │ ← VSCode settings
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────────▼──────────────────┐ │
│ │ Adapter Layer │ │ ← VSCode → Core types
│ └──────────┬──────────────────┘ │
└────────────┼────────────────────┘
│ depends on
┌────────────▼────────────────────┐
│ rangelink-core-ts │
│ │
│ ┌─────────────────────────────┐ │
│ │ Formatting (Link Builder) │ │ ← Generate links
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Selection Analysis │ │ ← Rectangular detection
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Validation │ │ ← Config validation
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Domain Models │ │ ← Types, enums
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
Principle: Core library has zero platform dependencies.
Rationale: Enables reuse across editors, tools, and languages.
Implementation:
- Core library defines its own
Selectioninterface - No imports from
vscodenamespace - Extension provides adapter layer:
vscode.Selection→core.Selection
Example:
// ❌ BAD: Core library coupled to VSCode
import { Selection } from 'vscode';
export function formatLink(selection: Selection): string {
// Tightly coupled to VSCode types
}
// ✅ GOOD: Core library platform-agnostic
export interface Selection {
readonly startLine: number;
readonly startChar: number;
readonly endLine: number;
readonly endChar: number;
}
export function formatLink(selection: Selection): string {
// Works with any editor's selection type
}Principle: Core library has zero runtime dependencies.
Rationale: Minimize bundle size, maximize portability, reduce security surface.
Current dependencies:
- Core: None (only
typescriptas devDependency) - Extension: Only
rangelink-core-ts(and VSCode engine)
Benefits:
- 📦 Small bundle size (~50KB core library)
- 🔒 No supply chain vulnerabilities
- 🚀 Fast installation
- 🔄 Easy to port to other languages
Principle: 100% branch coverage target with comprehensive test suites.
Coverage targets:
- Core library: 100% branch coverage
- Extension: 90%+ coverage
Test organization:
tests/
unit/ # Unit tests (isolated)
integration/ # Integration tests (multi-component)
fixtures/ # Test data and helpers
Test categories:
- ✅ Happy path (standard use cases)
- ✅ Edge cases (empty, boundary values)
- ✅ Error conditions (validation failures)
- ✅ Custom configurations (all delimiter combinations)
- ✅ BYOD parsing (portable links)
Principle: Separate errors from informational messages.
Two-tier system:
-
RangeLinkErrorCodes (Error Handling):
- Error codes WITHOUT prefixes (no
ERR_, noWARN_) - Values are descriptive strings (same as keys):
SELECTION_EMPTY = 'SELECTION_EMPTY' - Used with
RangeLinkErrorfor structured exception handling - Includes: code, message, functionName, details, cause
- Purpose: Programmatic error handling
- Principle: If defined here, it's an error. Warning is a logging level.
- Follows
SharedErrorCodespattern for immediate log clarity
- Error codes WITHOUT prefixes (no
-
RangeLinkMessageCode (Informational Logging):
- Contains ONLY
MSG_xxxxcodes - Used for informational logging and i18n
- Purpose: Status updates, successful operations
- Stable identifiers for message templates
- Contains ONLY
Key principle: Logging level is independent of error type. You can catch an error and log it at any level (INFO, WARN, ERROR) based on context.
Benefits:
- 🎯 Clear separation: errors are errors, messages are messages
- 🔒 Type-safe error structures with rich context
- 🌍 i18n-ready (message codes map to translations)
- 🔄 Flexible logging (error level independent of error type)
- 🧪 Testable (custom Jest matchers for error validation)
See ERROR-HANDLING.md for complete error handling specification.
Principle: Micro-iterations (1-2 hours) with clear scope and "done when" criteria.
Workflow:
- Define iteration scope (what IS and IS NOT included)
- Estimate time (1-2 hours)
- Implement with tests
- Commit with descriptive message
- Move to next iteration
Benefits:
- 🎯 Prevents scope creep
- 📈 Natural progress tracking
- 🔄 Easy to pause/resume
- 📝 Clean git history
See Open Issues for planned features.
Location: packages/rangelink-core-ts/src/types/
Key types:
// Selection.ts - Platform-agnostic selection
export interface Selection {
readonly startLine: number; // 1-indexed
readonly startChar: number; // 0-indexed
readonly endLine: number; // 1-indexed
readonly endChar: number; // 0-indexed
}
// RangeLinkConfig.ts - Delimiter configuration
export interface RangeLinkConfig {
readonly delimiterLine: string; // Default: "L"
readonly delimiterPosition: string; // Default: "C"
readonly delimiterHash: string; // Default: "#"
readonly delimiterRange: string; // Default: "-"
}
// HashMode.ts - Selection mode
export enum HashMode {
Normal = 'Normal', // Single hash: #
RectangularMode = 'RectangularMode', // Double hash: ##
}
// RangeLinkMessageCode.ts - Structured logging codes
export enum RangeLinkMessageCode {
CONFIG_LOADED = 'MSG_1001',
CONFIG_ERR_DELIMITER_EMPTY = 'ERR_1002',
// ... more codes
}Location: packages/rangelink-core-ts/src/selection/
Purpose: Determine if selections form a rectangular selection.
Algorithm:
export function isRectangularSelection(selections: ReadonlyArray<Selection>): boolean {
// Need at least 2 selections
if (selections.length < 2) return false;
// All selections must have:
// 1. Same start column
// 2. Same end column
// 3. Consecutive line numbers (no gaps)
const firstSelection = selections[0];
const expectedStartChar = firstSelection.startChar;
const expectedEndChar = firstSelection.endChar;
for (let i = 0; i < selections.length; i++) {
const selection = selections[i];
// Check column consistency
if (selection.startChar !== expectedStartChar || selection.endChar !== expectedEndChar) {
return false;
}
// Check line consecutiveness (except first)
if (i > 0) {
const previousSelection = selections[i - 1];
if (selection.startLine !== previousSelection.startLine + 1) {
return false;
}
}
}
return true;
}Test coverage:
- ✅ Single selection (false)
- ✅ Two selections, same columns, consecutive lines (true)
- ✅ Multiple selections, same columns, consecutive lines (true)
- ✅ Different start columns (false)
- ✅ Different end columns (false)
- ✅ Non-consecutive lines (false)
- ✅ Empty selections array (false)
Location: packages/rangelink-core-ts/src/formatting/
Purpose: Generate RangeLink strings from selections and configuration.
Key function:
export function formatLink(
path: string,
selections: ReadonlyArray<Selection>,
config: RangeLinkConfig,
options: { portable?: boolean; absolutePath?: boolean } = {},
): string {
// 1. Determine hash mode (normal vs rectangular)
const hashMode = isRectangularSelection(selections) ? HashMode.RectangularMode : HashMode.Normal;
// 2. Normalize selection (use first if multiple, non-rectangular)
const selection = normalizeSelection(selections, hashMode);
// 3. Format range part
const rangePart = formatRange(selection, config);
// 4. Add hash prefix (single or double)
const hashPrefix =
hashMode === HashMode.RectangularMode
? config.delimiterHash.repeat(2) // ##
: config.delimiterHash; // #
// 5. Optionally add BYOD metadata
const byodSuffix = options.portable ? formatBYODMetadata(config, selection) : '';
return `${path}${hashPrefix}${rangePart}${byodSuffix}`;
}Link format examples:
- Single line:
path#L42 - Multi-line:
path#L10-L20 - With columns:
path#L10C5-L20C10 - Rectangular:
path##L10C5-L20C10 - Portable:
path#L10C5-L20C10~#~L~-~C~
See LINK-FORMATS.md for complete format specification.
Location: packages/rangelink-core-ts/src/validation/
Purpose: Validate delimiter configuration and provide error codes.
Validation rules:
- ✅ Not empty (min 1 character)
- ✅ No digits (0-9)
- ✅ No whitespace (spaces, tabs, newlines)
- ✅ No reserved characters (
~,|,/,\,:,,,@) - ✅ Unique (case-insensitive)
- ✅ No substring conflicts (case-insensitive)
- ✅ Hash single character (for local config only)
Error codes:
ERR_1002- DELIMITER_EMPTYERR_1003- DELIMITER_DIGITSERR_1004- DELIMITER_WHITESPACEERR_1005- DELIMITER_RESERVEDERR_1006- DELIMITER_NOT_UNIQUEERR_1007- DELIMITER_SUBSTRING_CONFLICTERR_1008- HASH_NOT_SINGLE_CHAR
See ERROR-HANDLING.md for complete validation specification.
Location: packages/rangelink-core-ts/src/byod/ (future)
Purpose: Generate and parse portable links with embedded delimiter metadata.
Format:
path#L10C5-L20C10~#~L~-~C~
└─┬──┘ Metadata
└── Separators: ~ (fixed)
Generation:
export function formatBYODMetadata(config: RangeLinkConfig, selection: Selection): string {
const hasColumns = selection.startChar !== undefined && selection.endChar !== undefined;
if (hasColumns) {
// 4-field format: ~hash~line~range~position~
return `~${config.delimiterHash}~${config.delimiterLine}~${config.delimiterRange}~${config.delimiterPosition}~`;
} else {
// 3-field format: ~hash~line~range~
return `~${config.delimiterHash}~${config.delimiterLine}~${config.delimiterRange}~`;
}
}Parsing: (Phase 2 roadmap item)
- Detect
~separator - Extract metadata fields
- Validate embedded delimiters
- Parse link using embedded delimiters (ignore local config)
- Handle missing position delimiter (recovery)
- Handle rectangular mode (double hash detection)
See BYOD.md for complete BYOD specification.
Problem: VSCode types are not platform-agnostic.
Solution: Adapter layer converts VSCode types to core types.
Implementation:
// extension.ts
import * as vscode from 'vscode';
import { formatLink, Selection as CoreSelection } from 'rangelink-core-ts';
function adaptSelection(vscodeSelection: vscode.Selection): CoreSelection {
return {
startLine: vscodeSelection.start.line + 1, // VSCode is 0-indexed
startChar: vscodeSelection.start.character,
endLine: vscodeSelection.end.line + 1,
endChar: vscodeSelection.end.character,
};
}
export function copyLinkCommand(editor: vscode.TextEditor, config: RangeLinkConfig): void {
const selections = editor.selections.map(adaptSelection);
const path = vscode.workspace.asRelativePath(editor.document.uri);
const link = formatLink(path, selections, config);
vscode.env.clipboard.writeText(link);
vscode.window.showInformationMessage('Link copied to clipboard!');
}Benefits:
- ✅ Core library remains platform-agnostic
- ✅ Easy to add adapters for other editors
- ✅ Clear separation of concerns
Implementation:
// config/loadConfig.ts
import * as vscode from 'vscode';
import { RangeLinkConfig, validateConfig } from 'rangelink-core-ts';
export function loadConfig(): RangeLinkConfig {
const vscodeConfig = vscode.workspace.getConfiguration('rangelink');
const config: RangeLinkConfig = {
delimiterLine: vscodeConfig.get('delimiterLine', 'L'),
delimiterPosition: vscodeConfig.get('delimiterPosition', 'C'),
delimiterHash: vscodeConfig.get('delimiterHash', '#'),
delimiterRange: vscodeConfig.get('delimiterRange', '-'),
};
// Validate using core library
const validationResult = validateConfig(config);
if (!validationResult.isValid) {
// Log errors to output channel
validationResult.errors.forEach((error) => {
outputChannel.appendLine(`[ERROR] [${error.code}] ${error.message}`);
});
// Fall back to defaults
return getDefaultConfig();
}
return config;
}Implementation:
// extension.ts
export function activate(context: vscode.ExtensionContext): void {
const outputChannel = vscode.window.createOutputChannel('RangeLink');
// Register commands
const commands = [
{
id: 'rangelink.copyLinkToSelectionWithRelativePath',
handler: createCopyLinkCommand({ portable: false, absolute: false }),
},
{
id: 'rangelink.copyPortableLinkToSelectionWithRelativePath',
handler: createCopyLinkCommand({ portable: true, absolute: false }),
},
// ... more commands
];
commands.forEach((command) => {
const disposable = vscode.commands.registerCommand(command.id, command.handler);
context.subscriptions.push(disposable);
});
}When RangeLink expands to support multiple languages (TypeScript, Java, C/C++, Rust, Go) and multiple editors (VSCode, Neovim, IntelliJ, Xcode), how do we:
- Ensure feature parity across all implementations?
- Enforce consistency when adding new features?
- Scale development without duplicating effort?
- Maintain quality across all implementations?
Core principles:
- Single Source of Truth - Specification defines behavior, not implementations
- Contract-Driven - All implementations pass same contract tests
- Language-Agnostic Contracts - Test cases defined in JSON, executable in any language
- CI Enforcement - Feature parity is not optional; CI fails if implementations diverge
rangeLink/
spec/ # Specification hub
schema/
range-link.schema.json # Data structure definitions
selection.schema.json
config.schema.json
contracts/ # Behavioral contracts
build-link/
single-line.json # Test cases for single-line links
multi-line.json
rectangular-mode.json
portable.json
parse-link/
all-formats.json
error-handling.json
validation/
delimiter-validation.json
docs/
specification.md # Human-readable spec
packages/
rangelink-core-ts/ # TypeScript implementation
src/ ... tests/contracts/ # Runs spec/contracts/**/*.json
rangelink-core-java/ # Java implementation
src/ ... tests/contracts/ # Runs same contracts
rangelink-core-rust/ # Rust implementation
src/ ... tests/contracts/ # Runs same contracts
rangelink-vscode-extension/ # VSCode (uses TypeScript core)
rangelink-neovim/ # Neovim (uses Rust core or FFI)
rangelink-intellij/ # IntelliJ (uses Java core)
Specification (JSON):
// spec/contracts/build-link/rectangular-mode.json
{
"name": "rectangular_mode_selection",
"testCases": [
{
"name": "simple_rectangular_selection",
"input": {
"path": "src/file.ts",
"selections": [
{ "line": 10, "startChar": 5, "endChar": 10 },
{ "line": 11, "startChar": 5, "endChar": 10 },
{ "line": 12, "startChar": 5, "endChar": 10 }
],
"config": {
"delimiterLine": "L",
"delimiterPosition": "C",
"delimiterHash": "#",
"delimiterRange": "-"
}
},
"expected": {
"link": "src/file.ts##L10C6-L12C11",
"isRectangularMode": true
}
}
]
}TypeScript implementation:
// packages/rangelink-core-ts/tests/contracts/test-build-link.ts
describe('Contract: Rectangular Mode', () => {
const contract = loadContract('build-link/rectangular-mode.json');
contract.testCases.forEach((testCase) => {
it(testCase.name, () => {
const result = formatLink(
testCase.input.path,
testCase.input.selections,
testCase.input.config,
);
expect(result).toBe(testCase.expected.link);
});
});
});Java implementation:
// packages/rangelink-core-java/src/test/java/ContractTests.java
@ParameterizedTest
@JsonSource("../../../../spec/contracts/build-link/rectangular-mode.json")
public void testRectangularMode(Contract contract) {
for (TestCase testCase : contract.getTestCases()) {
String result = RangeLinkBuilder.formatLink(
testCase.getInput().getPath(),
testCase.getInput().getSelections(),
testCase.getInput().getConfig()
);
assertEquals(testCase.getExpected().getLink(), result);
}
}Both implementations must pass the same contract tests. CI enforces this.
# .github/workflows/validate-parity.yml
name: Validate Feature Parity
on: [push, pull_request]
jobs:
validate-parity:
strategy:
matrix:
implementation:
- rangelink-core-ts
- rangelink-core-java
- rangelink-core-rust
steps:
- run: |
cd packages/${{ matrix.implementation }}
npm run test:contracts # or mvn test, cargo test
- run: ./tools/validate-parity.sh
# Fails if ANY implementation fails ANY contractAdding a new feature:
-
Define in specification:
- Add JSON Schema for new data structures
- Add contract tests for new behavior
- Update human-readable spec docs
-
All implementations must pass:
- Update TypeScript core → run contract tests
- Update Java core → run contract tests
- Update Rust core → run contract tests
- CI fails if any implementation doesn't pass
-
Extensions selectively expose:
- VSCode extension exposes features supported by VSCode API
- Neovim plugin exposes features supported by Neovim API
- Core library supports everything, extensions pick what they need
- ✅ Feature parity guaranteed - CI enforces it
- ✅ Faster development - Write spec once, implement in multiple languages
- ✅ Better testing - Shared contracts = comprehensive coverage
- ✅ Clear documentation - Spec serves as living documentation
- ✅ Platform flexibility - Extensions expose what their platform supports
- ✅ Scalability - Easy to add new languages/IDEs
See architecture-multi-language.md for complete multi-language specification.
- LINK-FORMATS.md - Link format specifications and parsing rules
- ERROR-HANDLING.md - Error codes and validation rules
- BYOD.md - Portable links specification
- LOGGING.md - Structured logging approach
Version: 0.1.0 Last Updated: 2025-01-31 Status: Current architecture for RangeLink v0.1.0