forked from liuup/claude-code-analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseIdeSelection.ts
More file actions
150 lines (136 loc) · 4.25 KB
/
useIdeSelection.ts
File metadata and controls
150 lines (136 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { useEffect, useRef } from 'react'
import { logError } from 'src/utils/log.js'
import { z } from 'zod/v4'
import type {
ConnectedMCPServer,
MCPServerConnection,
} from '../services/mcp/types.js'
import { getConnectedIdeClient } from '../utils/ide.js'
import { lazySchema } from '../utils/lazySchema.js'
export type SelectionPoint = {
line: number
character: number
}
export type SelectionData = {
selection: {
start: SelectionPoint
end: SelectionPoint
} | null
text?: string
filePath?: string
}
export type IDESelection = {
lineCount: number
lineStart?: number
text?: string
filePath?: string
}
// Define the selection changed notification schema
const SelectionChangedSchema = lazySchema(() =>
z.object({
method: z.literal('selection_changed'),
params: z.object({
selection: z
.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
})
.nullable()
.optional(),
text: z.string().optional(),
filePath: z.string().optional(),
}),
}),
)
/**
* A hook that tracks IDE text selection information by directly registering
* with MCP client notification handlers
*/
export function useIdeSelection(
mcpClients: MCPServerConnection[],
onSelect: (selection: IDESelection) => void,
): void {
const handlersRegistered = useRef(false)
const currentIDERef = useRef<ConnectedMCPServer | null>(null)
useEffect(() => {
// Find the IDE client from the MCP clients list
const ideClient = getConnectedIdeClient(mcpClients)
// If the IDE client changed, we need to re-register handlers.
// Normalize undefined to null so the initial ref value (null) matches
// "no IDE found" (undefined), avoiding spurious resets on every MCP update.
if (currentIDERef.current !== (ideClient ?? null)) {
handlersRegistered.current = false
currentIDERef.current = ideClient || null
// Reset the selection when the IDE client changes.
onSelect({
lineCount: 0,
lineStart: undefined,
text: undefined,
filePath: undefined,
})
}
// Skip if we've already registered handlers for the current IDE or if there's no IDE client
if (handlersRegistered.current || !ideClient) {
return
}
// Handler function for selection changes
const selectionChangeHandler = (data: SelectionData) => {
if (data.selection?.start && data.selection?.end) {
const { start, end } = data.selection
let lineCount = end.line - start.line + 1
// If on the first character of the line, do not count the line
// as being selected.
if (end.character === 0) {
lineCount--
}
const selection = {
lineCount,
lineStart: start.line,
text: data.text,
filePath: data.filePath,
}
onSelect(selection)
}
}
// Register notification handler for selection_changed events
ideClient.client.setNotificationHandler(
SelectionChangedSchema(),
notification => {
if (currentIDERef.current !== ideClient) {
return
}
try {
// Get the selection data from the notification params
const selectionData = notification.params
// Process selection data - validate it has required properties
if (
selectionData.selection &&
selectionData.selection.start &&
selectionData.selection.end
) {
// Handle selection changes
selectionChangeHandler(selectionData as SelectionData)
} else if (selectionData.text !== undefined) {
// Handle empty selection (when text is empty string)
selectionChangeHandler({
selection: null,
text: selectionData.text,
filePath: selectionData.filePath,
})
}
} catch (error) {
logError(error as Error)
}
},
)
// Mark that we've registered handlers
handlersRegistered.current = true
// No cleanup needed as MCP clients manage their own lifecycle
}, [mcpClients, onSelect])
}