Skip to content
Merged
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
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ jobs:
- name: Install dependencies
run: bun install

- name: Test with coverage
run: bun test --coverage --coverage-reporter=lcov
- name: Run tests
run: bun run test

- name: Generate coverage reports
run: |
cd packages/lsp && bun test --coverage --coverage-reporter=lcov
cd ../code && bun test --coverage --coverage-reporter=lcov || true
cd ../dora && bun test --coverage --coverage-reporter=lcov || true
continue-on-error: true

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
files: |
./packages/lsp/coverage/lcov.info
./packages/code/coverage/lcov.info
./packages/dora/coverage/lcov.info
fail_ci_if_error: false
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Claude/MCP Client <-> StdioTransport <-> McpServer <-> Providers
| Go | gopls | go.mod, go.work |
| Rust | rust-analyzer | Cargo.toml |
| Kotlin | JetBrains Kotlin LSP (auto-download) | build.gradle.kts, build.gradle, pom.xml |
| Dart | dart language-server (auto-download) | pubspec.yaml, pubspec.lock |

### Built-in Formatters

Expand Down
36 changes: 36 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: auto

component_management:
default_rules:
statuses:
- type: project
target: auto

individual_components:
- component_id: code
name: '@pleaseai/code'
paths:
- packages/code/src/**

- component_id: code-format
name: '@pleaseai/code-format'
paths:
- packages/format/src/**

- component_id: code-lsp
name: '@pleaseai/code-lsp'
paths:
- packages/lsp/src/**

- component_id: dora
name: '@pleaseai/dora'
paths:
- packages/dora/src/**
4 changes: 3 additions & 1 deletion packages/lsp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
],
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "bun test ./src"
"test": "bun test ./test",
"test:unit": "bun test ./test/unit",
"test:integration": "bun test ./test/integration"
},
"dependencies": {
"vscode-jsonrpc": "^8.2.1",
Expand Down
1 change: 1 addition & 0 deletions packages/lsp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export { createLSPClient, type LSPClientInfo } from './client'
// Re-export specific items to avoid conflicts
export { getLanguageId, LANGUAGE_EXTENSIONS } from './language'
export {
DartServer,
DenoServer,
getServerById,
getServersForExtension,
Expand Down
196 changes: 186 additions & 10 deletions packages/lsp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,32 @@ async function downloadAndExtract(url: string, destDir: string): Promise<void> {
}
}

// =============================================================================
// Process Lifecycle Utilities
// =============================================================================

/**
* Attach error and exit event handlers to an LSP process
* Centralizes logging for process lifecycle events
*/
function attachLSPProcessHandlers(
proc: ChildProcessWithoutNullStreams,
serverId: string,
): void {
proc.on('error', (err) => {
console.error(`[${serverId}] LSP process error:`, err)
})

proc.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`[${serverId}] LSP exited with code ${code}`)
}
if (signal) {
console.error(`[${serverId}] LSP killed by signal ${signal}`)
}
})
}

// =============================================================================
// Root Detection Utilities
// =============================================================================
Expand Down Expand Up @@ -651,24 +677,173 @@ export const KotlinServer: LSPServerInfo = {
},
})

// Log spawn errors
proc.on('error', (err) => {
console.error(`[kotlin] Kotlin LSP process error:`, err)
})
attachLSPProcessHandlers(proc, 'kotlin')
return { process: proc }
}
catch (err) {
console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err)
return undefined
}
},
}

// =============================================================================
// Dart Language Server
// =============================================================================

proc.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`[kotlin] Kotlin LSP exited with code ${code}`)
/**
* Dart SDK runtime dependency configuration
* Uses official Dart SDK with built-in language server
*/
const DART_RUNTIME_DEPS = {
version: '3.7.1',
platforms: {
'win-x64': {
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip',
binaryPath: 'dart-sdk/bin/dart.exe',
},
'linux-x64': {
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip',
binaryPath: 'dart-sdk/bin/dart',
},
'linux-arm64': {
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-arm64-release.zip',
binaryPath: 'dart-sdk/bin/dart',
},
'osx-x64': {
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip',
binaryPath: 'dart-sdk/bin/dart',
},
'osx-arm64': {
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip',
binaryPath: 'dart-sdk/bin/dart',
},
} as Record<PlatformId, { url: string, binaryPath: string }>,
}

/**
* Get the Dart LSP resources directory
*/
function getDartResourcesDir(): string {
return path.join(os.homedir(), '.cache', 'dora', 'dart-lsp')
}

/**
* Setup Dart runtime dependencies
* Downloads Dart SDK if not available and dart is not in PATH
*
* Unlike setupKotlinDependencies which returns { javaHomePath, kotlinLspPath },
* this returns only the binary path because:
* - Dart SDK is self-contained (no separate JRE dependency)
* - No environment variables (like JAVA_HOME) required for execution
*/
async function setupDartDependencies(platformId: PlatformId): Promise<string | undefined> {
const config = DART_RUNTIME_DEPS.platforms[platformId]

if (!config) {
console.warn(`[dart] Unsupported platform: ${platformId}`)
return undefined
}

const resourcesDir = getDartResourcesDir()
const dartPath = path.join(resourcesDir, config.binaryPath)

try {
await fs.access(dartPath)
return dartPath
}
catch (err) {
const isNotFound = err instanceof Error
&& 'code' in err
&& (err as NodeJS.ErrnoException).code === 'ENOENT'

if (!isNotFound) {
console.error(`[dart] Cannot access Dart at ${dartPath}:`, err instanceof Error ? err.message : err)
return undefined
}

// Dart not found, download it
console.warn(`[dart] Downloading Dart SDK ${DART_RUNTIME_DEPS.version} for ${platformId}...`)
try {
await downloadAndExtract(config.url, resourcesDir)

// Make dart executable on Unix platforms
if (!platformId.startsWith('win-')) {
try {
await fs.chmod(dartPath, 0o755)
}
if (signal) {
console.error(`[kotlin] Kotlin LSP killed by signal ${signal}`)
catch (chmodErr) {
console.error(`[dart] Failed to make Dart executable at ${dartPath}:`, chmodErr instanceof Error ? chmodErr.message : chmodErr)
return undefined
}
}
}
catch (downloadErr) {
console.error(`[dart] Failed to download Dart SDK:`, downloadErr)
return undefined
}
}

// Verify Dart exists
try {
await fs.access(dartPath)
return dartPath
}
catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err)
console.error(`[dart] Dart executable not accessible at ${dartPath}: ${errorMsg}`)
return undefined
}
}

/**
* Dart Language Server
* Uses official Dart SDK with system-first fallback and auto-download
*/
export const DartServer: LSPServerInfo = {
id: 'dart',
extensions: ['.dart'],
root: nearestRoot(['pubspec.yaml', 'pubspec.lock']),
async spawn(root) {
// Try system dart first
const systemDart = Bun.which('dart')
if (systemDart) {
try {
const proc = spawn(systemDart, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], {
cwd: root,
})

attachLSPProcessHandlers(proc, 'dart')
return { process: proc }
}
catch (err) {
console.warn(`[dart] Failed to spawn system Dart LSP, trying auto-download:`, err)
}
}

// Fallback to auto-download
const platformId = getPlatformId()
if (!platformId) {
console.warn(`[dart] Unsupported platform: ${process.platform}-${process.arch}`)
return undefined
}

const dartPath = await setupDartDependencies(platformId)
if (!dartPath) {
console.warn(`[dart] Failed to setup Dart SDK. Check previous logs for details.`)
return undefined
}

try {
const proc = spawn(dartPath, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], {
cwd: root,
})

attachLSPProcessHandlers(proc, 'dart')
return { process: proc }
}
catch (err) {
console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err)
console.error(`[dart] Failed to spawn Dart LSP:`, err)
return undefined
}
},
Expand All @@ -685,6 +860,7 @@ export const LSP_SERVERS: LSPServerInfo[] = [
GoplsServer,
RustAnalyzerServer,
KotlinServer,
DartServer,
]

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/lsp/test/fixtures/dart-project/lib/helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// Standalone subtract function
int subtract(int a, int b) {
return a - b;
}

/// Divides two numbers with error handling
double divide(double a, double b) {
if (b == 0) {
throw ArgumentError('Cannot divide by zero');
}
return a / b;
}

/// Formats a number as currency
String formatCurrency(double amount, {String symbol = '\$'}) {
return '$symbol${amount.toStringAsFixed(2)}';
}
56 changes: 56 additions & 0 deletions packages/lsp/test/fixtures/dart-project/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/// Calculator class for basic arithmetic operations
class Calculator {
/// Adds two numbers
int add(int a, int b) {
final result = a + b;
return result;
}

/// Subtracts two numbers
int subtract(int a, int b) {
return a - b;
}

/// Multiplies two numbers
int multiply(int a, int b) {
return a * b;
}
}

/// Helper class with static methods
class MathHelper {
/// Calculates power
static double power(double base, int exponent) {
double result = 1;
for (int i = 0; i < exponent; i++) {
result *= base;
}
return result;
}

/// Calculates absolute value
static double abs(double value) {
return value < 0 ? -value : value;
}
}

/// Main entry point
void main() {
final calc = Calculator();

// Test arithmetic operations
final sum = calc.add(5, 3);
final diff = calc.subtract(10, 4);
final product = calc.multiply(6, 7);

print('Sum: $sum');
print('Difference: $diff');
print('Product: $product');

// Test helper methods
final squared = MathHelper.power(2.0, 3);
final absolute = MathHelper.abs(-42.5);

print('2^3 = $squared');
print('|-42.5| = $absolute');
}
6 changes: 6 additions & 0 deletions packages/lsp/test/fixtures/dart-project/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: test_dart_app
description: Test Dart project for LSP integration tests
version: 1.0.0

environment:
sdk: ^3.0.0
Loading