Skip to content

Commit c0a42d5

Browse files
authored
feat(lsp): add Dart Language Server support (#9)
* feat(lsp): add Dart Language Server support Add Dart LSP support with system-first fallback and auto-download: - Check for system `dart` in PATH first - Auto-download Dart SDK 3.7.1 if not found - Support all major platforms: linux-x64, linux-arm64, osx-x64, osx-arm64, win-x64 - Root detection via pubspec.yaml and pubspec.lock Closes #8 * refactor(lsp): improve Dart LSP implementation - Extract process event handlers to attachLSPProcessHandlers() helper - Add documentation explaining setupDartDependencies vs Kotlin pattern - Add behavioral tests for root detection (pubspec.yaml/pubspec.lock) - Update CLAUDE.md to include Dart in supported servers table * test(lsp): add monorepo and deep nesting tests for DartServer - Add test for nested monorepo package detection (inner pubspec.yaml) - Add test for deep directory structure root detection * refactor(lsp): migrate tests to Bun ecosystem pattern - Move tests from src/__tests__/ to test/ directory - Add test/unit/ for unit tests - Add test/integration/ for integration tests - Add test/fixtures/ for test data - Create Dart project fixture for LSP integration tests - Add DartServer integration tests (serena pattern) - Update package.json with test:unit and test:integration scripts Directory structure follows Bun/Elysia conventions: test/ ├── unit/ # Unit tests ├── integration/ # Integration tests └── fixtures/ # Test data * ci: fix test command for new directory structure - Use `bun run test` which runs turbo for all packages - Generate coverage from packages/lsp separately - Update coverage file path for Codecov * ci: add Codecov components for monorepo packages - Create codecov.yml with component configuration - Define components for each package: code, code-format, code-lsp, dora - Generate coverage reports for all packages with tests - Upload multiple coverage files to Codecov * fix: use single quotes in codecov.yml
1 parent f71d8dd commit c0a42d5

14 files changed

Lines changed: 601 additions & 19 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,22 @@ jobs:
4949
- name: Install dependencies
5050
run: bun install
5151

52-
- name: Test with coverage
53-
run: bun test --coverage --coverage-reporter=lcov
52+
- name: Run tests
53+
run: bun run test
54+
55+
- name: Generate coverage reports
56+
run: |
57+
cd packages/lsp && bun test --coverage --coverage-reporter=lcov
58+
cd ../code && bun test --coverage --coverage-reporter=lcov || true
59+
cd ../dora && bun test --coverage --coverage-reporter=lcov || true
60+
continue-on-error: true
5461

5562
- name: Upload coverage to Codecov
5663
uses: codecov/codecov-action@v5
5764
with:
5865
token: ${{ secrets.CODECOV_TOKEN }}
59-
files: ./coverage/lcov.info
66+
files: |
67+
./packages/lsp/coverage/lcov.info
68+
./packages/code/coverage/lcov.info
69+
./packages/dora/coverage/lcov.info
6070
fail_ci_if_error: false

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Claude/MCP Client <-> StdioTransport <-> McpServer <-> Providers
125125
| Go | gopls | go.mod, go.work |
126126
| Rust | rust-analyzer | Cargo.toml |
127127
| Kotlin | JetBrains Kotlin LSP (auto-download) | build.gradle.kts, build.gradle, pom.xml |
128+
| Dart | dart language-server (auto-download) | pubspec.yaml, pubspec.lock |
128129

129130
### Built-in Formatters
130131

codecov.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 1%
7+
patch:
8+
default:
9+
target: auto
10+
11+
component_management:
12+
default_rules:
13+
statuses:
14+
- type: project
15+
target: auto
16+
17+
individual_components:
18+
- component_id: code
19+
name: '@pleaseai/code'
20+
paths:
21+
- packages/code/src/**
22+
23+
- component_id: code-format
24+
name: '@pleaseai/code-format'
25+
paths:
26+
- packages/format/src/**
27+
28+
- component_id: code-lsp
29+
name: '@pleaseai/code-lsp'
30+
paths:
31+
- packages/lsp/src/**
32+
33+
- component_id: dora
34+
name: '@pleaseai/dora'
35+
paths:
36+
- packages/dora/src/**

packages/lsp/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
],
2626
"scripts": {
2727
"typecheck": "tsc -p tsconfig.json --noEmit",
28-
"test": "bun test ./src"
28+
"test": "bun test ./test",
29+
"test:unit": "bun test ./test/unit",
30+
"test:integration": "bun test ./test/integration"
2931
},
3032
"dependencies": {
3133
"vscode-jsonrpc": "^8.2.1",

packages/lsp/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export { createLSPClient, type LSPClientInfo } from './client'
392392
// Re-export specific items to avoid conflicts
393393
export { getLanguageId, LANGUAGE_EXTENSIONS } from './language'
394394
export {
395+
DartServer,
395396
DenoServer,
396397
getServerById,
397398
getServersForExtension,

packages/lsp/src/server.ts

Lines changed: 186 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,32 @@ async function downloadAndExtract(url: string, destDir: string): Promise<void> {
130130
}
131131
}
132132

133+
// =============================================================================
134+
// Process Lifecycle Utilities
135+
// =============================================================================
136+
137+
/**
138+
* Attach error and exit event handlers to an LSP process
139+
* Centralizes logging for process lifecycle events
140+
*/
141+
function attachLSPProcessHandlers(
142+
proc: ChildProcessWithoutNullStreams,
143+
serverId: string,
144+
): void {
145+
proc.on('error', (err) => {
146+
console.error(`[${serverId}] LSP process error:`, err)
147+
})
148+
149+
proc.on('exit', (code, signal) => {
150+
if (code !== 0 && code !== null) {
151+
console.error(`[${serverId}] LSP exited with code ${code}`)
152+
}
153+
if (signal) {
154+
console.error(`[${serverId}] LSP killed by signal ${signal}`)
155+
}
156+
})
157+
}
158+
133159
// =============================================================================
134160
// Root Detection Utilities
135161
// =============================================================================
@@ -651,24 +677,173 @@ export const KotlinServer: LSPServerInfo = {
651677
},
652678
})
653679

654-
// Log spawn errors
655-
proc.on('error', (err) => {
656-
console.error(`[kotlin] Kotlin LSP process error:`, err)
657-
})
680+
attachLSPProcessHandlers(proc, 'kotlin')
681+
return { process: proc }
682+
}
683+
catch (err) {
684+
console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err)
685+
return undefined
686+
}
687+
},
688+
}
689+
690+
// =============================================================================
691+
// Dart Language Server
692+
// =============================================================================
658693

659-
proc.on('exit', (code, signal) => {
660-
if (code !== 0 && code !== null) {
661-
console.error(`[kotlin] Kotlin LSP exited with code ${code}`)
694+
/**
695+
* Dart SDK runtime dependency configuration
696+
* Uses official Dart SDK with built-in language server
697+
*/
698+
const DART_RUNTIME_DEPS = {
699+
version: '3.7.1',
700+
platforms: {
701+
'win-x64': {
702+
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip',
703+
binaryPath: 'dart-sdk/bin/dart.exe',
704+
},
705+
'linux-x64': {
706+
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip',
707+
binaryPath: 'dart-sdk/bin/dart',
708+
},
709+
'linux-arm64': {
710+
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-arm64-release.zip',
711+
binaryPath: 'dart-sdk/bin/dart',
712+
},
713+
'osx-x64': {
714+
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip',
715+
binaryPath: 'dart-sdk/bin/dart',
716+
},
717+
'osx-arm64': {
718+
url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip',
719+
binaryPath: 'dart-sdk/bin/dart',
720+
},
721+
} as Record<PlatformId, { url: string, binaryPath: string }>,
722+
}
723+
724+
/**
725+
* Get the Dart LSP resources directory
726+
*/
727+
function getDartResourcesDir(): string {
728+
return path.join(os.homedir(), '.cache', 'dora', 'dart-lsp')
729+
}
730+
731+
/**
732+
* Setup Dart runtime dependencies
733+
* Downloads Dart SDK if not available and dart is not in PATH
734+
*
735+
* Unlike setupKotlinDependencies which returns { javaHomePath, kotlinLspPath },
736+
* this returns only the binary path because:
737+
* - Dart SDK is self-contained (no separate JRE dependency)
738+
* - No environment variables (like JAVA_HOME) required for execution
739+
*/
740+
async function setupDartDependencies(platformId: PlatformId): Promise<string | undefined> {
741+
const config = DART_RUNTIME_DEPS.platforms[platformId]
742+
743+
if (!config) {
744+
console.warn(`[dart] Unsupported platform: ${platformId}`)
745+
return undefined
746+
}
747+
748+
const resourcesDir = getDartResourcesDir()
749+
const dartPath = path.join(resourcesDir, config.binaryPath)
750+
751+
try {
752+
await fs.access(dartPath)
753+
return dartPath
754+
}
755+
catch (err) {
756+
const isNotFound = err instanceof Error
757+
&& 'code' in err
758+
&& (err as NodeJS.ErrnoException).code === 'ENOENT'
759+
760+
if (!isNotFound) {
761+
console.error(`[dart] Cannot access Dart at ${dartPath}:`, err instanceof Error ? err.message : err)
762+
return undefined
763+
}
764+
765+
// Dart not found, download it
766+
console.warn(`[dart] Downloading Dart SDK ${DART_RUNTIME_DEPS.version} for ${platformId}...`)
767+
try {
768+
await downloadAndExtract(config.url, resourcesDir)
769+
770+
// Make dart executable on Unix platforms
771+
if (!platformId.startsWith('win-')) {
772+
try {
773+
await fs.chmod(dartPath, 0o755)
662774
}
663-
if (signal) {
664-
console.error(`[kotlin] Kotlin LSP killed by signal ${signal}`)
775+
catch (chmodErr) {
776+
console.error(`[dart] Failed to make Dart executable at ${dartPath}:`, chmodErr instanceof Error ? chmodErr.message : chmodErr)
777+
return undefined
665778
}
779+
}
780+
}
781+
catch (downloadErr) {
782+
console.error(`[dart] Failed to download Dart SDK:`, downloadErr)
783+
return undefined
784+
}
785+
}
786+
787+
// Verify Dart exists
788+
try {
789+
await fs.access(dartPath)
790+
return dartPath
791+
}
792+
catch (err) {
793+
const errorMsg = err instanceof Error ? err.message : String(err)
794+
console.error(`[dart] Dart executable not accessible at ${dartPath}: ${errorMsg}`)
795+
return undefined
796+
}
797+
}
798+
799+
/**
800+
* Dart Language Server
801+
* Uses official Dart SDK with system-first fallback and auto-download
802+
*/
803+
export const DartServer: LSPServerInfo = {
804+
id: 'dart',
805+
extensions: ['.dart'],
806+
root: nearestRoot(['pubspec.yaml', 'pubspec.lock']),
807+
async spawn(root) {
808+
// Try system dart first
809+
const systemDart = Bun.which('dart')
810+
if (systemDart) {
811+
try {
812+
const proc = spawn(systemDart, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], {
813+
cwd: root,
814+
})
815+
816+
attachLSPProcessHandlers(proc, 'dart')
817+
return { process: proc }
818+
}
819+
catch (err) {
820+
console.warn(`[dart] Failed to spawn system Dart LSP, trying auto-download:`, err)
821+
}
822+
}
823+
824+
// Fallback to auto-download
825+
const platformId = getPlatformId()
826+
if (!platformId) {
827+
console.warn(`[dart] Unsupported platform: ${process.platform}-${process.arch}`)
828+
return undefined
829+
}
830+
831+
const dartPath = await setupDartDependencies(platformId)
832+
if (!dartPath) {
833+
console.warn(`[dart] Failed to setup Dart SDK. Check previous logs for details.`)
834+
return undefined
835+
}
836+
837+
try {
838+
const proc = spawn(dartPath, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], {
839+
cwd: root,
666840
})
667841

842+
attachLSPProcessHandlers(proc, 'dart')
668843
return { process: proc }
669844
}
670845
catch (err) {
671-
console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err)
846+
console.error(`[dart] Failed to spawn Dart LSP:`, err)
672847
return undefined
673848
}
674849
},
@@ -685,6 +860,7 @@ export const LSP_SERVERS: LSPServerInfo[] = [
685860
GoplsServer,
686861
RustAnalyzerServer,
687862
KotlinServer,
863+
DartServer,
688864
]
689865

690866
/**
File renamed without changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// Standalone subtract function
2+
int subtract(int a, int b) {
3+
return a - b;
4+
}
5+
6+
/// Divides two numbers with error handling
7+
double divide(double a, double b) {
8+
if (b == 0) {
9+
throw ArgumentError('Cannot divide by zero');
10+
}
11+
return a / b;
12+
}
13+
14+
/// Formats a number as currency
15+
String formatCurrency(double amount, {String symbol = '\$'}) {
16+
return '$symbol${amount.toStringAsFixed(2)}';
17+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/// Calculator class for basic arithmetic operations
2+
class Calculator {
3+
/// Adds two numbers
4+
int add(int a, int b) {
5+
final result = a + b;
6+
return result;
7+
}
8+
9+
/// Subtracts two numbers
10+
int subtract(int a, int b) {
11+
return a - b;
12+
}
13+
14+
/// Multiplies two numbers
15+
int multiply(int a, int b) {
16+
return a * b;
17+
}
18+
}
19+
20+
/// Helper class with static methods
21+
class MathHelper {
22+
/// Calculates power
23+
static double power(double base, int exponent) {
24+
double result = 1;
25+
for (int i = 0; i < exponent; i++) {
26+
result *= base;
27+
}
28+
return result;
29+
}
30+
31+
/// Calculates absolute value
32+
static double abs(double value) {
33+
return value < 0 ? -value : value;
34+
}
35+
}
36+
37+
/// Main entry point
38+
void main() {
39+
final calc = Calculator();
40+
41+
// Test arithmetic operations
42+
final sum = calc.add(5, 3);
43+
final diff = calc.subtract(10, 4);
44+
final product = calc.multiply(6, 7);
45+
46+
print('Sum: $sum');
47+
print('Difference: $diff');
48+
print('Product: $product');
49+
50+
// Test helper methods
51+
final squared = MathHelper.power(2.0, 3);
52+
final absolute = MathHelper.abs(-42.5);
53+
54+
print('2^3 = $squared');
55+
print('|-42.5| = $absolute');
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: test_dart_app
2+
description: Test Dart project for LSP integration tests
3+
version: 1.0.0
4+
5+
environment:
6+
sdk: ^3.0.0

0 commit comments

Comments
 (0)