Skip to content

Commit b867897

Browse files
feat: Implement TypeScript Language Server Protocol (LSP) integration
- Added TypeScript LSP event listeners for document lifecycle events (didOpen, didChange, didClose). - Implemented language features such as completion, hover, definition, references, and signature help. - Created TypeScript LSP service to manage server lifecycle and communication with the LSP. - Developed Monaco LSP provider to integrate LSP features with the Monaco Editor. - Established TypeScript LSP client for renderer process communication via IPC. - Added diagnostics handling and document management for LSP integration.
1 parent be71962 commit b867897

24 files changed

+1659
-1846
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"tailwind-merge": "^3.3.1",
161161
"tailwindcss-animate": "^1.0.7",
162162
"tippy.js": "^6.3.7",
163+
"typescript-language-server": "^4.3.4",
163164
"vaul": "^1.1.2",
164165
"xterm": "^5.3.0",
165166
"zod": "3",

src/config/monaco-languages.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as monaco from 'monaco-editor';
2-
import { typescriptProjectService } from '@/services/typescript-project';
3-
;
2+
import { typescriptLSPClient, registerLSPProviders } from '@/services/typescript-lsp';
43

54
// Enhanced language configurations for better syntax highlighting
65
export const enhanceMonacoLanguages = () => {
@@ -16,22 +15,25 @@ export const enhanceMonacoLanguages = () => {
1615
trailingCommas: 'error',
1716
});
1817

19-
// Set default TypeScript/JavaScript configuration (will be overridden by project-specific settings)
20-
setDefaultTypeScriptConfiguration();
21-
22-
// Enable type checking and error reporting for TypeScript
18+
// Disable Monaco's built-in TypeScript service - we'll use LSP instead
2319
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
24-
noSemanticValidation: false,
25-
noSyntaxValidation: false,
26-
noSuggestionDiagnostics: false,
20+
noSemanticValidation: true, // Disable - LSP will handle this
21+
noSyntaxValidation: false, // Keep basic syntax validation as fallback
22+
noSuggestionDiagnostics: true, // Disable - LSP will handle this
2723
});
2824

2925
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
30-
noSemanticValidation: false,
31-
noSyntaxValidation: false,
32-
noSuggestionDiagnostics: false,
26+
noSemanticValidation: true, // Disable - LSP will handle this
27+
noSyntaxValidation: false, // Keep basic syntax validation as fallback
28+
noSuggestionDiagnostics: true, // Disable - LSP will handle this
3329
});
3430

31+
// Set basic TypeScript configuration (LSP will override with project-specific settings)
32+
setDefaultTypeScriptConfiguration();
33+
34+
// Register LSP providers for advanced TypeScript features
35+
registerLSPProviders();
36+
3537
// CSS configuration with vendor prefixes support
3638
monaco.languages.css.cssDefaults.setOptions({
3739
validate: true,
@@ -146,14 +148,34 @@ const setDefaultTypeScriptConfiguration = () => {
146148
};
147149

148150
/**
149-
* Initialize TypeScript project integration for the current project
151+
* Initialize TypeScript LSP for the current project
150152
*/
151153
export const initializeTypeScriptProject = async (projectPath: string) => {
152154
try {
153-
await typescriptProjectService.initializeProject(projectPath);
154-
console.log('TypeScript project integration initialized');
155+
// Wait for the app to be fully ready before initializing LSP
156+
// This helps avoid the initialization timing issues
157+
let retries = 10;
158+
while (retries > 0 && !window.electronAPI?.typescriptLSP) {
159+
console.log('Waiting for TypeScript LSP API to be available...');
160+
await new Promise(resolve => setTimeout(resolve, 500));
161+
retries--;
162+
}
163+
164+
if (!window.electronAPI?.typescriptLSP) {
165+
console.warn('TypeScript LSP API not available after waiting');
166+
return false;
167+
}
168+
169+
const success = await typescriptLSPClient.initialize(projectPath);
170+
if (success) {
171+
console.log('TypeScript LSP integration initialized for project:', projectPath);
172+
} else {
173+
console.error('Failed to initialize TypeScript LSP for project:', projectPath);
174+
}
175+
return success;
155176
} catch (error) {
156-
console.error('Error initializing TypeScript project:', error);
177+
console.error('Error initializing TypeScript LSP:', error);
178+
return false;
157179
}
158180
};
159181

src/helpers/ipc/context-exposer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { exposeTerminalContext } from "./terminal/terminal-context";
88
import { exposeShellContext } from "./shell/shell-context";
99
import { exposeAgentContext } from "./agents/agent-context";
1010
import { exposeIndexContext } from "./index/index-context";
11+
import { exposeTypescriptLSPContext } from "./typescript-lsp/typescript-lsp-context";
1112

1213
export default function exposeContexts() {
1314
exposeWindowContext();
@@ -20,4 +21,5 @@ export default function exposeContexts() {
2021
exposeShellContext();
2122
exposeAgentContext();
2223
exposeIndexContext();
24+
exposeTypescriptLSPContext();
2325
}

src/helpers/ipc/listeners-register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { addTerminalEventListeners } from "./terminal/terminal-listeners";
99
import { addAgentEventListeners } from "./agents/agent-listeners";
1010
import { addIndexEventListeners } from "./index/index-listeners";
1111
import registerShellListeners from "./shell/shell-listeners";
12+
import { addTypescriptLSPEventListeners } from "./typescript-lsp/typescript-lsp-listeners";
1213

1314
export default function registerListeners(mainWindow: BrowserWindow) {
1415
addWindowEventListeners(mainWindow);
@@ -20,5 +21,6 @@ export default function registerListeners(mainWindow: BrowserWindow) {
2021
addTerminalEventListeners(mainWindow);
2122
addAgentEventListeners(mainWindow);
2223
addIndexEventListeners();
24+
addTypescriptLSPEventListeners(mainWindow);
2325
registerShellListeners();
2426
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { contextBridge, ipcRenderer } from 'electron';
2+
3+
export function exposeTypescriptLSPContext() {
4+
try {
5+
// Get existing electronAPI or create new one
6+
const existingAPI = (globalThis as any).electronAPI || {};
7+
8+
contextBridge.exposeInMainWorld('electronAPI', {
9+
...existingAPI,
10+
typescriptLSP: {
11+
initialize: (projectPath: string) =>
12+
ipcRenderer.invoke('typescript-lsp:initialize', projectPath),
13+
14+
didOpen: (params: {
15+
uri: string;
16+
languageId: string;
17+
version: number;
18+
text: string;
19+
}) => ipcRenderer.invoke('typescript-lsp:didOpen', params),
20+
21+
didChange: (params: {
22+
uri: string;
23+
version: number;
24+
changes: any[];
25+
}) => ipcRenderer.invoke('typescript-lsp:didChange', params),
26+
27+
didClose: (uri: string) =>
28+
ipcRenderer.invoke('typescript-lsp:didClose', uri),
29+
30+
completion: (params: {
31+
uri: string;
32+
position: { line: number; character: number };
33+
}) => ipcRenderer.invoke('typescript-lsp:completion', params),
34+
35+
hover: (params: {
36+
uri: string;
37+
position: { line: number; character: number };
38+
}) => ipcRenderer.invoke('typescript-lsp:hover', params),
39+
40+
definition: (params: {
41+
uri: string;
42+
position: { line: number; character: number };
43+
}) => ipcRenderer.invoke('typescript-lsp:definition', params),
44+
45+
references: (params: {
46+
uri: string;
47+
position: { line: number; character: number };
48+
}) => ipcRenderer.invoke('typescript-lsp:references', params),
49+
50+
signatureHelp: (params: {
51+
uri: string;
52+
position: { line: number; character: number };
53+
}) => ipcRenderer.invoke('typescript-lsp:signatureHelp', params),
54+
55+
status: () => ipcRenderer.invoke('typescript-lsp:status'),
56+
},
57+
58+
onTypescriptLSPNotification: (callback: (notification: any) => void) => {
59+
ipcRenderer.on('typescript-lsp:notification', (event, notification) => {
60+
callback(notification);
61+
});
62+
},
63+
});
64+
} catch (error) {
65+
console.error('Failed to expose TypeScript LSP context:', error);
66+
}
67+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { ipcMain, BrowserWindow } from 'electron';
2+
import { typescriptLSPService } from './typescript-lsp-service';
3+
4+
function getErrorMessage(error: unknown): string {
5+
if (error instanceof Error) {
6+
return error.message;
7+
}
8+
return String(error);
9+
}
10+
11+
export function addTypescriptLSPEventListeners(mainWindow: BrowserWindow) {
12+
// Initialize TypeScript LSP for a project
13+
ipcMain.handle('typescript-lsp:initialize', async (event, projectPath: string) => {
14+
try {
15+
console.log('IPC: Initializing TypeScript LSP for project:', projectPath);
16+
await typescriptLSPService.initialize(projectPath);
17+
console.log('IPC: TypeScript LSP initialized successfully');
18+
return { success: true };
19+
} catch (error: unknown) {
20+
console.error('IPC: Failed to initialize TypeScript LSP:', error);
21+
return { success: false, error: getErrorMessage(error) };
22+
}
23+
});
24+
25+
// Document lifecycle events
26+
ipcMain.handle('typescript-lsp:didOpen', async (event, params: {
27+
uri: string;
28+
languageId: string;
29+
version: number;
30+
text: string;
31+
}) => {
32+
try {
33+
await typescriptLSPService.didOpenTextDocument(
34+
params.uri,
35+
params.languageId,
36+
params.version,
37+
params.text
38+
);
39+
return { success: true };
40+
} catch (error: unknown) {
41+
console.error('Failed to send didOpen to TypeScript LSP:', error);
42+
return { success: false, error: getErrorMessage(error) };
43+
}
44+
});
45+
46+
ipcMain.handle('typescript-lsp:didChange', async (event, params: {
47+
uri: string;
48+
version: number;
49+
changes: any[];
50+
}) => {
51+
try {
52+
await typescriptLSPService.didChangeTextDocument(
53+
params.uri,
54+
params.version,
55+
params.changes
56+
);
57+
return { success: true };
58+
} catch (error: unknown) {
59+
console.error('Failed to send didChange to TypeScript LSP:', error);
60+
return { success: false, error: getErrorMessage(error) };
61+
}
62+
});
63+
64+
ipcMain.handle('typescript-lsp:didClose', async (event, uri: string) => {
65+
try {
66+
await typescriptLSPService.didCloseTextDocument(uri);
67+
return { success: true };
68+
} catch (error: unknown) {
69+
console.error('Failed to send didClose to TypeScript LSP:', error);
70+
return { success: false, error: getErrorMessage(error) };
71+
}
72+
});
73+
74+
// Language features
75+
ipcMain.handle('typescript-lsp:completion', async (event, params: {
76+
uri: string;
77+
position: { line: number; character: number };
78+
}) => {
79+
try {
80+
const result = await typescriptLSPService.getCompletions(params.uri, params.position);
81+
return { success: true, result };
82+
} catch (error: unknown) {
83+
console.error('Failed to get completions from TypeScript LSP:', error);
84+
return { success: false, error: getErrorMessage(error) };
85+
}
86+
});
87+
88+
ipcMain.handle('typescript-lsp:hover', async (event, params: {
89+
uri: string;
90+
position: { line: number; character: number };
91+
}) => {
92+
try {
93+
const result = await typescriptLSPService.getHover(params.uri, params.position);
94+
return { success: true, result };
95+
} catch (error: unknown) {
96+
console.error('Failed to get hover from TypeScript LSP:', error);
97+
return { success: false, error: getErrorMessage(error) };
98+
}
99+
});
100+
101+
ipcMain.handle('typescript-lsp:definition', async (event, params: {
102+
uri: string;
103+
position: { line: number; character: number };
104+
}) => {
105+
try {
106+
const result = await typescriptLSPService.getDefinition(params.uri, params.position);
107+
return { success: true, result };
108+
} catch (error: unknown) {
109+
console.error('Failed to get definition from TypeScript LSP:', error);
110+
return { success: false, error: getErrorMessage(error) };
111+
}
112+
});
113+
114+
ipcMain.handle('typescript-lsp:references', async (event, params: {
115+
uri: string;
116+
position: { line: number; character: number };
117+
}) => {
118+
try {
119+
const result = await typescriptLSPService.getReferences(params.uri, params.position);
120+
return { success: true, result };
121+
} catch (error: unknown) {
122+
console.error('Failed to get references from TypeScript LSP:', error);
123+
return { success: false, error: getErrorMessage(error) };
124+
}
125+
});
126+
127+
ipcMain.handle('typescript-lsp:signatureHelp', async (event, params: {
128+
uri: string;
129+
position: { line: number; character: number };
130+
}) => {
131+
try {
132+
const result = await typescriptLSPService.getSignatureHelp(params.uri, params.position);
133+
return { success: true, result };
134+
} catch (error: unknown) {
135+
console.error('Failed to get signature help from TypeScript LSP:', error);
136+
return { success: false, error: getErrorMessage(error) };
137+
}
138+
});
139+
140+
// ...existing code...
141+
142+
// Server status
143+
ipcMain.handle('typescript-lsp:status', async () => {
144+
return {
145+
isRunning: typescriptLSPService.isServerRunning(),
146+
};
147+
});
148+
149+
// Listen for LSP notifications and forward them to renderer
150+
typescriptLSPService.on('notification', (notification) => {
151+
mainWindow.webContents.send('typescript-lsp:notification', notification);
152+
});
153+
154+
// Cleanup on app exit
155+
const cleanup = () => {
156+
typescriptLSPService.stopServer();
157+
};
158+
159+
process.on('exit', cleanup);
160+
process.on('SIGINT', cleanup);
161+
process.on('SIGTERM', cleanup);
162+
}

0 commit comments

Comments
 (0)