11import { Effect } from 'effect'
22import type { RebuiltText } from '../chunking/rebuild'
3+ import { findScopeAtOffset , getAncestorChain } from '../scope/tree'
34import type {
45 ByteRange ,
56 Chunk ,
67 ChunkContext ,
78 ChunkOptions ,
9+ EntityInfo ,
10+ ExtractedEntity ,
11+ ImportInfo ,
812 ScopeTree ,
913} from '../types'
14+ import { getSiblings , type SiblingOptions } from './siblings'
1015
1116/**
1217 * Error when attaching context fails
@@ -19,88 +24,136 @@ export class ContextError {
1924 ) { }
2025}
2126
22- /**
23- * Attach context information to a chunk
24- *
25- * @param text - The rebuilt text info for the chunk
26- * @param scopeTree - The scope tree for the file
27- * @param options - Chunking options
28- * @param index - The chunk index
29- * @param totalChunks - Total number of chunks
30- * @returns Effect yielding the complete chunk with context
31- *
32- * TODO: Implement context attachment
33- */
34- export const attachContext = (
35- text : RebuiltText ,
36- scopeTree : ScopeTree ,
37- options : ChunkOptions ,
38- index : number ,
39- totalChunks : number ,
40- ) : Effect . Effect < Chunk , ContextError > => {
41- // TODO: Implement context attachment
42- // 1. Find scope for this chunk's byte range
43- // 2. Get entities within the chunk
44- // 3. Get siblings based on options
45- // 4. Get relevant imports
46- const context : ChunkContext = {
47- scope : [ ] ,
48- entities : [ ] ,
49- siblings : [ ] ,
50- imports : [ ] ,
51- }
52-
53- void scopeTree
54- void options
55-
56- return Effect . succeed ( {
57- text : text . text ,
58- byteRange : text . byteRange ,
59- lineRange : text . lineRange ,
60- context,
61- index,
62- totalChunks,
63- } )
64- }
65-
6627/**
6728 * Get scope information for a byte range
6829 *
30+ * Finds the scope containing this range and builds an array of EntityInfo
31+ * from the scope and its ancestors.
32+ *
6933 * @param byteRange - The byte range to get scope for
7034 * @param scopeTree - The scope tree
71- * @returns Scope entity info array
72- *
73- * TODO: Implement scope lookup
35+ * @returns Scope entity info array representing the scope chain
7436 */
7537export const getScopeForRange = (
7638 byteRange : ByteRange ,
7739 scopeTree : ScopeTree ,
7840) : ChunkContext [ 'scope' ] => {
79- // TODO: Implement scope lookup
80- // Find containing scopes and return as EntityInfo[]
81- void byteRange
82- void scopeTree
83- return [ ]
41+ // Find the scope at the start of the range
42+ const scope = findScopeAtOffset ( scopeTree , byteRange . start )
43+
44+ if ( ! scope ) {
45+ return [ ]
46+ }
47+
48+ // Build scope chain: current scope + ancestors
49+ const scopeChain : EntityInfo [ ] = [ ]
50+
51+ // Add current scope
52+ scopeChain . push ( {
53+ name : scope . entity . name ,
54+ type : scope . entity . type ,
55+ signature : scope . entity . signature ,
56+ } )
57+
58+ // Add ancestors (from immediate parent to root)
59+ const ancestors = getAncestorChain ( scope )
60+ for ( const ancestor of ancestors ) {
61+ scopeChain . push ( {
62+ name : ancestor . entity . name ,
63+ type : ancestor . entity . type ,
64+ signature : ancestor . entity . signature ,
65+ } )
66+ }
67+
68+ return scopeChain
8469}
8570
8671/**
8772 * Get entities within a byte range
8873 *
74+ * Finds entities whose byte ranges overlap with the given range.
75+ * Overlap condition: entity.start < range.end && entity.end > range.start
76+ *
8977 * @param byteRange - The byte range to search
9078 * @param scopeTree - The scope tree
9179 * @returns Entity info array for entities in range
92- *
93- * TODO: Implement entity lookup
9480 */
9581export const getEntitiesInRange = (
9682 byteRange : ByteRange ,
9783 scopeTree : ScopeTree ,
9884) : ChunkContext [ 'entities' ] => {
99- // TODO: Implement entity lookup
100- // Find entities whose ranges overlap with byteRange
101- void byteRange
102- void scopeTree
103- return [ ]
85+ const overlappingEntities = scopeTree . allEntities . filter ( ( entity ) => {
86+ // Overlap check: entity.start < range.end && entity.end > range.start
87+ return (
88+ entity . byteRange . start < byteRange . end &&
89+ entity . byteRange . end > byteRange . start
90+ )
91+ } )
92+
93+ // Map to EntityInfo
94+ return overlappingEntities . map ( ( entity ) => ( {
95+ name : entity . name ,
96+ type : entity . type ,
97+ signature : entity . signature ,
98+ } ) )
99+ }
100+
101+ /**
102+ * Parse import source from an import entity
103+ *
104+ * Extracts the source module path from import signatures like:
105+ * - `import { foo } from 'module'`
106+ * - `import foo from 'module'`
107+ * - `import * as foo from 'module'`
108+ *
109+ * @param entity - The import entity
110+ * @returns The import source or empty string if not found
111+ */
112+ const parseImportSource = ( entity : ExtractedEntity ) : string => {
113+ // Try to extract from signature using regex
114+ // Common patterns: from 'source' or from "source"
115+ const fromMatch = entity . signature . match ( / f r o m \s + [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / )
116+ if ( fromMatch ?. [ 1 ] ) {
117+ return fromMatch [ 1 ]
118+ }
119+
120+ // For CommonJS style: require('source')
121+ const requireMatch = entity . signature . match ( / r e q u i r e \s * \( \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / )
122+ if ( requireMatch ?. [ 1 ] ) {
123+ return requireMatch [ 1 ]
124+ }
125+
126+ return ''
127+ }
128+
129+ /**
130+ * Check if an import is a default import
131+ *
132+ * @param entity - The import entity
133+ * @returns Whether this is a default import
134+ */
135+ const isDefaultImport = ( entity : ExtractedEntity ) : boolean => {
136+ // Default import patterns:
137+ // import foo from 'module'
138+ // But NOT: import { foo } from 'module'
139+ // And NOT: import * as foo from 'module'
140+ const signature = entity . signature
141+ return (
142+ / ^ i m p o r t \s + \w + \s + f r o m / . test ( signature ) &&
143+ ! / ^ i m p o r t \s * \{ / . test ( signature ) &&
144+ ! / ^ i m p o r t \s * \* / . test ( signature )
145+ )
146+ }
147+
148+ /**
149+ * Check if an import is a namespace import
150+ *
151+ * @param entity - The import entity
152+ * @returns Whether this is a namespace import
153+ */
154+ const isNamespaceImport = ( entity : ExtractedEntity ) : boolean => {
155+ // Namespace import pattern: import * as foo from 'module'
156+ return / ^ i m p o r t \s * \* \s * a s \s + \w + / . test ( entity . signature )
104157}
105158
106159/**
@@ -110,18 +163,135 @@ export const getEntitiesInRange = (
110163 * @param scopeTree - The scope tree
111164 * @param filterImports - Whether to filter to only used imports
112165 * @returns Import info array
113- *
114- * TODO: Implement import filtering
115166 */
116167export const getRelevantImports = (
117168 entities : ChunkContext [ 'entities' ] ,
118169 scopeTree : ScopeTree ,
119170 filterImports : boolean ,
120171) : ChunkContext [ 'imports' ] => {
121- // TODO: Implement import filtering
122- // If filterImports, only include imports used by chunk entities
123- void entities
124- void scopeTree
125- void filterImports
126- return [ ]
172+ const imports = scopeTree . imports
173+
174+ if ( imports . length === 0 ) {
175+ return [ ]
176+ }
177+
178+ // Map import entity to ImportInfo
179+ const mapToImportInfo = ( entity : ExtractedEntity ) : ImportInfo => ( {
180+ name : entity . name ,
181+ source : parseImportSource ( entity ) ,
182+ isDefault : isDefaultImport ( entity ) || undefined ,
183+ isNamespace : isNamespaceImport ( entity ) || undefined ,
184+ } )
185+
186+ // If not filtering, return all imports
187+ if ( ! filterImports ) {
188+ return imports . map ( mapToImportInfo )
189+ }
190+
191+ // Filter to only imports that are used by entities in the chunk
192+ // Build a set of names that appear in entity signatures and names
193+ const usedNames = new Set < string > ( )
194+ for ( const entity of entities ) {
195+ // Add the entity name
196+ usedNames . add ( entity . name )
197+
198+ // Extract identifiers from signature if available
199+ if ( entity . signature ) {
200+ // Match word characters that could be identifiers
201+ const identifiers = entity . signature . match (
202+ / \b [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * \b / g,
203+ )
204+ if ( identifiers ) {
205+ for ( const id of identifiers ) {
206+ usedNames . add ( id )
207+ }
208+ }
209+ }
210+ }
211+
212+ // Filter imports to those whose names appear in the chunk
213+ const filteredImports = imports . filter ( ( importEntity ) => {
214+ return usedNames . has ( importEntity . name )
215+ } )
216+
217+ return filteredImports . map ( mapToImportInfo )
218+ }
219+
220+ /**
221+ * Attach context information to a chunk
222+ *
223+ * @param text - The rebuilt text info for the chunk
224+ * @param scopeTree - The scope tree for the file
225+ * @param options - Chunking options
226+ * @param index - The chunk index
227+ * @param totalChunks - Total number of chunks
228+ * @returns Effect yielding the complete chunk with context
229+ */
230+ export const attachContext = (
231+ text : RebuiltText ,
232+ scopeTree : ScopeTree ,
233+ options : ChunkOptions ,
234+ index : number ,
235+ totalChunks : number ,
236+ ) : Effect . Effect < Chunk , ContextError > => {
237+ return Effect . try ( {
238+ try : ( ) => {
239+ // Determine context mode
240+ const contextMode = options . contextMode ?? 'full'
241+
242+ // For 'none' mode, return minimal context
243+ if ( contextMode === 'none' ) {
244+ const context : ChunkContext = {
245+ scope : [ ] ,
246+ entities : [ ] ,
247+ siblings : [ ] ,
248+ imports : [ ] ,
249+ }
250+ return {
251+ text : text . text ,
252+ byteRange : text . byteRange ,
253+ lineRange : text . lineRange ,
254+ context,
255+ index,
256+ totalChunks,
257+ }
258+ }
259+
260+ // Get scope for this chunk's byte range
261+ const scope = getScopeForRange ( text . byteRange , scopeTree )
262+
263+ // Get entities within the chunk
264+ const entities = getEntitiesInRange ( text . byteRange , scopeTree )
265+
266+ // Get siblings based on options
267+ const siblingDetail = options . siblingDetail ?? 'signatures'
268+ const siblingOptions : SiblingOptions = {
269+ detail : siblingDetail ,
270+ maxSiblings : contextMode === 'minimal' ? 2 : undefined ,
271+ }
272+ const siblings = getSiblings ( text . byteRange , scopeTree , siblingOptions )
273+
274+ // Get relevant imports
275+ const filterImports = options . filterImports ?? false
276+ const imports = getRelevantImports ( entities , scopeTree , filterImports )
277+
278+ const context : ChunkContext = {
279+ scope,
280+ entities,
281+ siblings,
282+ imports,
283+ }
284+
285+ return {
286+ text : text . text ,
287+ byteRange : text . byteRange ,
288+ lineRange : text . lineRange ,
289+ context,
290+ index,
291+ totalChunks,
292+ }
293+ } ,
294+ catch : ( error : unknown ) =>
295+ new ContextError ( 'Failed to attach context' , error ) ,
296+ } )
127297}
0 commit comments