Skip to content

Commit cc1a50c

Browse files
Merge pull request #113 from anithanarayanswamy/feature/lsp-slash-command
Feature: Add /lsp command to list connected LSP servers
2 parents 1c06176 + bac13e0 commit cc1a50c

File tree

4 files changed

+404
-0
lines changed

4 files changed

+404
-0
lines changed

source/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from '@/commands/clear';
44
export * from '@/commands/model';
55
export * from '@/commands/provider';
66
export * from '@/commands/mcp';
7+
export * from '@/commands/lsp';
78
export * from '@/commands/custom-commands';
89
export * from '@/commands/init';
910
export * from '@/commands/theme';
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import test from 'ava';
2+
import React from 'react';
3+
import {render} from 'ink-testing-library';
4+
import {LSP, lspCommand} from './lsp';
5+
import {getLSPManager, type LSPInitResult} from '../lsp/lsp-manager';
6+
import {ThemeContext} from '../hooks/useTheme';
7+
import {themes} from '../config/themes';
8+
9+
console.log(`\nlsp-command.spec.tsx`);
10+
11+
// Mock ThemeProvider for testing
12+
const MockThemeProvider = ({children}: {children: React.ReactNode}) => {
13+
const mockTheme = {
14+
currentTheme: 'tokyo-night' as const,
15+
colors: themes['tokyo-night'].colors,
16+
setCurrentTheme: () => {},
17+
};
18+
return (
19+
<ThemeContext.Provider value={mockTheme}>{children}</ThemeContext.Provider>
20+
);
21+
};
22+
23+
// Mock LSP manager status
24+
const mockLSPStatus = {
25+
initialized: true,
26+
servers: [
27+
{
28+
name: 'typescript-language-server',
29+
ready: true,
30+
languages: ['ts', 'js', 'tsx', 'jsx'],
31+
},
32+
{
33+
name: 'gopls',
34+
ready: true,
35+
languages: ['go'],
36+
},
37+
{
38+
name: 'rust-analyzer',
39+
ready: false,
40+
languages: ['rs'],
41+
},
42+
],
43+
};
44+
45+
// ============================================================================
46+
// Tests for LSP Command Display
47+
// ============================================================================
48+
49+
test('LSP command: shows no servers when none connected', t => {
50+
const emptyStatus = {
51+
initialized: false,
52+
servers: [],
53+
};
54+
55+
const {lastFrame} = render(
56+
<MockThemeProvider>
57+
<LSP status={emptyStatus} />
58+
</MockThemeProvider>,
59+
);
60+
61+
const output = lastFrame();
62+
t.truthy(output);
63+
t.regex(output!, /No LSP servers connected/);
64+
t.regex(output!, /agents\.config\.json/);
65+
t.regex(output!, /LSP servers will auto-discover based on your project files/);
66+
});
67+
68+
test('LSP command: displays server status correctly', t => {
69+
const {lastFrame} = render(
70+
<MockThemeProvider>
71+
<LSP status={mockLSPStatus} />
72+
</MockThemeProvider>,
73+
);
74+
75+
const output = lastFrame();
76+
t.truthy(output);
77+
78+
// Should show connected servers count
79+
t.regex(output!, /Connected LSP Servers \(3\):/);
80+
81+
// Should show server names
82+
t.regex(output!, /typescript-language-server/);
83+
t.regex(output!, /gopls/);
84+
t.regex(output!, /rust-analyzer/);
85+
86+
// Should show status icons
87+
t.regex(output!, /🟢/); // Ready servers
88+
t.regex(output!, /🔴/); // Initializing server
89+
90+
// Should show status text
91+
t.regex(output!, /\(Ready\)/);
92+
t.regex(output!, /\(Initializing\)/);
93+
});
94+
95+
test('LSP command: displays associated languages correctly', t => {
96+
const {lastFrame} = render(
97+
<MockThemeProvider>
98+
<LSP status={mockLSPStatus} />
99+
</MockThemeProvider>,
100+
);
101+
102+
const output = lastFrame();
103+
t.truthy(output);
104+
105+
// Should show languages for each server
106+
t.regex(output!, /Languages: ts, js, tsx, jsx/);
107+
t.regex(output!, /Languages: go/);
108+
t.regex(output!, /Languages: rs/);
109+
});
110+
111+
test('LSP command: handles single language correctly', t => {
112+
const singleLangStatus = {
113+
initialized: true,
114+
servers: [
115+
{
116+
name: 'single-lang-server',
117+
ready: true,
118+
languages: ['python'],
119+
},
120+
],
121+
};
122+
123+
const {lastFrame} = render(
124+
<MockThemeProvider>
125+
<LSP status={singleLangStatus} />
126+
</MockThemeProvider>,
127+
);
128+
129+
const output = lastFrame();
130+
t.truthy(output);
131+
t.regex(output!, /Languages: python/);
132+
});
133+
134+
test('LSP command: handles multiple languages correctly', t => {
135+
const multiLangStatus = {
136+
initialized: true,
137+
servers: [
138+
{
139+
name: 'multi-lang-server',
140+
ready: true,
141+
languages: ['js', 'ts', 'jsx', 'tsx', 'vue'],
142+
},
143+
],
144+
};
145+
146+
const {lastFrame} = render(
147+
<MockThemeProvider>
148+
<LSP status={multiLangStatus} />
149+
</MockThemeProvider>,
150+
);
151+
152+
const output = lastFrame();
153+
t.truthy(output);
154+
t.regex(output!, /Languages: js, ts, jsx, tsx, vue/);
155+
});
156+
157+
test('LSP command: shows correct status icons', t => {
158+
const testCases = [
159+
{ready: true, expectedIcon: '🟢'},
160+
{ready: false, expectedIcon: '🔴'},
161+
];
162+
163+
for (const testCase of testCases) {
164+
const status = {
165+
initialized: true,
166+
servers: [
167+
{
168+
name: 'test-server',
169+
ready: testCase.ready,
170+
languages: ['test'],
171+
},
172+
],
173+
};
174+
175+
const {lastFrame} = render(
176+
<MockThemeProvider>
177+
<LSP status={status} />
178+
</MockThemeProvider>,
179+
);
180+
181+
const output = lastFrame();
182+
t.truthy(output);
183+
t.regex(
184+
output!,
185+
new RegExp(testCase.expectedIcon),
186+
`Should show ${testCase.expectedIcon} for ready=${testCase.ready}`,
187+
);
188+
}
189+
});
190+
191+
test('LSP command: shows correct status text', t => {
192+
const testCases = [
193+
{ready: true, expectedText: 'Ready'},
194+
{ready: false, expectedText: 'Initializing'},
195+
];
196+
197+
for (const testCase of testCases) {
198+
const status = {
199+
initialized: true,
200+
servers: [
201+
{
202+
name: 'test-server',
203+
ready: testCase.ready,
204+
languages: ['test'],
205+
},
206+
],
207+
};
208+
209+
const {lastFrame} = render(
210+
<MockThemeProvider>
211+
<LSP status={status} />
212+
</MockThemeProvider>,
213+
);
214+
215+
const output = lastFrame();
216+
t.truthy(output);
217+
t.regex(
218+
output!,
219+
new RegExp(`\\(${testCase.expectedText}\\)`),
220+
`Should show (${testCase.expectedText}) for ready=${testCase.ready}`,
221+
);
222+
}
223+
});
224+
225+
// ============================================================================
226+
// Command Handler Tests
227+
// ============================================================================
228+
229+
test('lspCommand has correct name', t => {
230+
t.is(lspCommand.name, 'lsp');
231+
});
232+
233+
test('lspCommand has description', t => {
234+
t.truthy(lspCommand.description);
235+
t.is(typeof lspCommand.description, 'string');
236+
t.true(lspCommand.description.length > 0);
237+
});
238+
239+
test('lspCommand handler is a function', t => {
240+
t.is(typeof lspCommand.handler, 'function');
241+
});
242+
243+
test('lspCommand handler returns valid React element', async t => {
244+
// Mock the LSP manager to return our test status
245+
const originalGetLSPManager = getLSPManager;
246+
const mockLSPManager = {
247+
getStatus: () => mockLSPStatus,
248+
};
249+
// Since we can't directly mock the singleton function, we'll just call the handler
250+
// which will use the actual LSP manager (but it will return an element regardless)
251+
const mockMessages: any[] = [];
252+
const mockMetadata: any = {
253+
provider: 'test',
254+
model: 'test',
255+
tokens: 0,
256+
getMessageTokens: () => 0,
257+
};
258+
259+
const result = await lspCommand.handler([], mockMessages, mockMetadata);
260+
261+
t.truthy(result);
262+
t.true(React.isValidElement(result));
263+
});
264+
265+
test('lspCommand handler returns React element when no servers connected', async t => {
266+
const mockMessages: any[] = [];
267+
const mockMetadata: any = {
268+
provider: 'test',
269+
model: 'test',
270+
tokens: 0,
271+
getMessageTokens: () => 0,
272+
};
273+
274+
const result = await lspCommand.handler([], mockMessages, mockMetadata);
275+
276+
t.truthy(result);
277+
t.true(React.isValidElement(result));
278+
});

0 commit comments

Comments
 (0)