1+ // import {appendFileSync} from "node:fs";
2+
13import {
2- AccessorDeclaration ,
4+ Block ,
5+ ClassElement ,
36 ClassLikeDeclaration ,
47 createSourceFile ,
58 EnumDeclaration ,
9+ factory ,
610 FileTextChanges ,
711 forEach ,
812 forEachChild ,
913 formatting ,
1014 hasJSDocNodes ,
1115 InterfaceDeclaration ,
16+ isBlock ,
1217 isClassElement ,
1318 isClassLike ,
1419 isEnumMember ,
20+ isNamedDeclaration ,
1521 isSourceFileJS ,
22+ isTypeElement ,
1623 LanguageServiceHost ,
17- MethodDeclaration ,
24+ NamedDeclaration ,
1825 Node ,
1926 NodeArray ,
2027 Program ,
21- PropertyDeclaration ,
22- PropertyName ,
2328 ScriptTarget ,
2429 SourceFile ,
2530 SourceMapper ,
31+ Statement ,
2632 SyntaxKind ,
2733 sysLog ,
2834 textChanges ,
2935 TextSpan ,
36+ TypeElement ,
3037 UserPreferences ,
3138} from "./_namespaces/ts" ;
3239import { ChangeTracker } from "./textChanges" ;
33-
3440// useful stuff:
3541// textChanges.ChangeTracker - lots of methods for inserting nodes in the right place.
3642// textChanges.ChangeTracker.pushRaw() - to push `updates` before running our analysis.
@@ -55,6 +61,13 @@ export function mapCode(
5561 // TODO: uhhh... do something about file-less mappings. Not supported for now.
5662 return ;
5763 }
64+ // appendFileSync("D:\\copilot-results.txt", `--------------------------------------------\n// ${sourceFile.fileName}\n`, "utf8");
65+ // if (focusLocations) {
66+ // appendFileSync("D:\\copilot-results.txt", `// focusLocations: ${JSON.stringify(focusLocations)}\n`, "utf8")
67+ // }
68+ // for (const content of contents) {
69+ // appendFileSync("D:\\copilot-results.txt", content + "\n\n", "utf8");
70+ // }
5871 const parsed = contents . map ( parse ) ;
5972 sysLog ( `${ parsed } ` ) ;
6073 for ( const nodes of parsed ) {
@@ -65,7 +78,6 @@ export function mapCode(
6578 ) ;
6679 } catch ( e ) {
6780 sysLog ( `mapCode: ${ e } ` ) ;
68- // TODO: Should this be null instead?
6981 return [ ] ;
7082 }
7183}
@@ -110,47 +122,52 @@ function placeNodeGroup(file: SourceFile, changeTracker: ChangeTracker, nodes: N
110122 // using first and last node from `nodes`. Use node types and names to
111123 // match ranges.
112124 if ( isClassElement ( nodes [ 0 ] ) ) {
113- placeClassNodeGroup ( file , changeTracker , nodes , focusLocations ) ;
125+ placeClassNodeGroup ( file , changeTracker , nodes as NodeArray < ClassElement > , focusLocations ) ;
126+ }
127+ else if ( isTypeElement ( nodes [ 0 ] ) ) {
128+ placeClassNodeGroup ( file , changeTracker , nodes as NodeArray < TypeElement > , focusLocations ) ;
114129 }
115130 else if ( isEnumMember ( nodes [ 0 ] ) ) {
116131 placeEnumNodeGroup ( file , changeTracker , nodes , focusLocations ) ;
117132 }
118133 else {
119- placeStatements ( file , changeTracker , nodes , focusLocations ) ;
134+ placeStatements ( file , changeTracker , nodes as NodeArray < Statement > , focusLocations ) ;
120135 }
121136}
122137
123- function placeClassNodeGroup ( file : SourceFile , changeTracker : ChangeTracker , nodes : NodeArray < Node > , focusLocations ?: TextSpan [ ] [ ] ) {
138+ function placeClassNodeGroup ( file : SourceFile , changeTracker : ChangeTracker , nodes : NodeArray < ClassElement > | NodeArray < TypeElement > , focusLocations ?: TextSpan [ ] [ ] ) {
124139 // We have one or more class-ish members.
125140 //
126141 // 1. If focusLocations is null/undefined, find the first class in the
127142 // SourceFile and insert/replace into that class.
128143 // 2. If we have focusLocations, go over each one and:
129144 // a. Find the nearest class or interface declaration that contains the focusLocation.
130145 let classOrInterface : ClassLikeDeclaration | InterfaceDeclaration | undefined ;
131- if ( ! focusLocations ) {
146+ if ( ! focusLocations || ! focusLocations . length ) {
132147 for ( const stmt of file . statements ) {
133148 if ( isClassLike ( stmt ) ) {
134149 classOrInterface = stmt ;
135150 break ;
136151 }
152+ else if ( stmt . kind === SyntaxKind . InterfaceDeclaration ) {
153+ classOrInterface = stmt as InterfaceDeclaration ;
154+ break ;
155+ }
137156 }
138157 }
139158 else {
140159 let node : Node | undefined ;
141160 top: for ( const locationGroup of focusLocations ) {
142161 for ( const location of locationGroup ) {
143- node = getNodeAtPosition ( file , location . start ) ;
144- while ( node ) {
145- if ( isClassLike ( node ) ) {
146- classOrInterface = node ;
147- break top;
148- }
149- else if ( node . kind === SyntaxKind . InterfaceDeclaration ) {
150- classOrInterface = node as InterfaceDeclaration ;
151- break top;
152- }
153- node = node . parent ;
162+ node = findAncestor ( getNodeAtPosition ( file , location . start ) , node => {
163+ return isClassLike ( node ) || node . kind === SyntaxKind . InterfaceDeclaration ;
164+ } ) ;
165+ if ( node && node . kind === SyntaxKind . InterfaceDeclaration ) {
166+ classOrInterface = node as InterfaceDeclaration ;
167+ break top;
168+ } else if ( node ) {
169+ classOrInterface = node as ClassLikeDeclaration ;
170+ break top;
154171 }
155172 }
156173 }
@@ -160,19 +177,44 @@ function placeClassNodeGroup(file: SourceFile, changeTracker: ChangeTracker, nod
160177 throw new Error ( "Failed to find a class or interface to map the given code into." ) ;
161178 }
162179 else {
180+ const classEls : ClassElement [ ] = [ ] ;
181+ const typeEls : TypeElement [ ] = [ ] ;
163182 top: for ( const node of nodes ) {
164- const nodeName = getIdentifier ( node ) ;
165183 for ( const member of classOrInterface . members ) {
166- const memberName = getIdentifier ( member ) ;
167- if ( ( nodeName && memberName && nodeName === memberName ) ||
168- node . kind === SyntaxKind . Constructor && member . kind === SyntaxKind . Constructor ) {
169- // If we have a matching name, do a replace.
184+ if ( matchNode ( node , member , file ) ) {
185+ // If we have corresponding nodes, replace the old one with the new member.
170186 changeTracker . replaceNode ( file , member , node )
171187 continue top;
172188 }
173189 }
174190 // Otherwise, we insert this node at the end of the class/interface.
175- changeTracker . insertNodeAtEndOfScope ( file , classOrInterface , node ) ;
191+ if ( isClassElement ( node ) ) {
192+ classEls . push ( node ) ;
193+ }
194+ else if ( isTypeElement ( node ) ) {
195+ typeEls . push ( node ) ;
196+ }
197+ }
198+ if ( classEls . length || typeEls . length ) {
199+ if ( classOrInterface . members . length === 0 ) {
200+ if ( classEls . length ) {
201+ const newNode = { ...classOrInterface , members : factory . createNodeArray ( classEls ) } ;
202+ changeTracker . replaceNode ( file , classOrInterface , newNode )
203+ }
204+ else {
205+ const newNode = { ...classOrInterface , members : factory . createNodeArray ( typeEls ) } ;
206+ changeTracker . replaceNode ( file , classOrInterface , newNode )
207+ }
208+ }
209+ else if ( classEls . length ) {
210+ changeTracker . insertNodesAfter ( file , classOrInterface . members [ classOrInterface . members . length - 1 ] , classEls ) ;
211+ }
212+ else if ( typeEls . length ) {
213+ changeTracker . insertNodesAfter ( file , classOrInterface . members [ classOrInterface . members . length - 1 ] , typeEls ) ;
214+ }
215+ else {
216+ throw new Error ( "Nothing to append" ) ;
217+ }
176218 }
177219 }
178220}
@@ -181,8 +223,56 @@ function placeEnumNodeGroup(_file: SourceFile, _changeTracker: ChangeTracker, _n
181223 throw new Error ( "Not implemented." ) ;
182224}
183225
184- function placeStatements ( _file : SourceFile , _changeTracker : ChangeTracker , _nodes : NodeArray < Node > , _focusLocations ?: TextSpan [ ] [ ] ) {
185- throw new Error ( "Not implemented." ) ;
226+ function placeStatements ( file : SourceFile , changeTracker : ChangeTracker , nodes : NodeArray < Statement > , focusLocations ?: TextSpan [ ] [ ] ) {
227+ // If there's an empty or null focusLocation, we just shove everything at
228+ // the end of the file and call it a day.
229+ if ( ! focusLocations || ! focusLocations . length ) {
230+ changeTracker . insertNodesAtEndOfFile ( file , nodes , /*blankLineBetween*/ false ) ;
231+ return ;
232+ }
233+
234+ // Otherwise, we'll need to find the right place to insert or replace.
235+
236+ // First, we try and find a nearby frame of reference: are there
237+ // declarations with similar names nearby?
238+ for ( const locationGroup of focusLocations ) {
239+ for ( const location of locationGroup ) {
240+ const scope : Node | undefined = findAncestor ( getNodeAtPosition ( file , location . start ) , node => {
241+ if ( ! isBlock ( node ) ) {
242+ return false ;
243+ }
244+ for ( const stmt of node . statements ) {
245+ for ( const node of nodes ) {
246+ if ( matchNode ( node , stmt , file ) ) {
247+ return true ;
248+ }
249+ }
250+ }
251+ return false ;
252+ } ) ;
253+ if ( scope ) {
254+ // We found a scope that contains a matching node.
255+
256+ // TODO: find range and replaceRangeWithNodes.
257+ return ;
258+ }
259+ }
260+ }
261+
262+ // If we have no frame of reference, we'll just insert at the end of the
263+ // nearest statements scope, or, if we can't find a block, at the top
264+ // level of the source file.
265+ let scopeStatements : NodeArray < Statement > = file . statements ;
266+ top: for ( const locationGroup of focusLocations ) {
267+ for ( const location of locationGroup ) {
268+ const block = findAncestor ( getNodeAtPosition ( file , location . start ) , isBlock ) ;
269+ if ( block ) {
270+ scopeStatements = ( block as Block ) . statements ;
271+ break top;
272+ }
273+ }
274+ }
275+ changeTracker . insertNodesAfter ( file , scopeStatements [ scopeStatements . length - 1 ] , nodes ) ;
186276}
187277
188278function getNodeAtPosition ( sourceFile : SourceFile , position : number ) : Node {
@@ -202,33 +292,44 @@ function getNodeAtPosition(sourceFile: SourceFile, position: number): Node {
202292 }
203293}
204294
205- function getIdentifier ( node : Node ) : string | undefined {
206- let name : PropertyName | undefined ;
207- switch ( node . kind ) {
208- case SyntaxKind . PropertyDeclaration :
209- name = ( node as PropertyDeclaration ) . name ;
210- break ;
211- case SyntaxKind . MethodDeclaration :
212- name = ( node as MethodDeclaration ) . name ;
213- break ;
214- case SyntaxKind . GetAccessor :
215- case SyntaxKind . SetAccessor :
216- name = ( node as AccessorDeclaration ) . name ;
217- break ;
295+ function getIdentifier ( node : NamedDeclaration , file : SourceFile ) : string | undefined {
296+ const name = node . name ;
297+ if ( ! name ) return undefined ;
298+ switch ( name . kind ) {
299+ case SyntaxKind . Identifier :
300+ return name . text ;
301+ case SyntaxKind . PrivateIdentifier :
302+ return name . escapedText as string ;
303+ case SyntaxKind . JsxNamespacedName :
304+ return name . namespace . text + ":" + name . name . text ;
218305 default :
219- return undefined ;
220- }
221- if ( name ) {
222- switch ( name . kind ) {
223- case SyntaxKind . Identifier :
224- return name . text ;
225- case SyntaxKind . PrivateIdentifier :
226- return name . escapedText as string ;
227- case SyntaxKind . StringLiteral :
228- case SyntaxKind . NumericLiteral :
229- case SyntaxKind . ComputedPropertyName :
230- return name . getText ( ) ;
231- }
306+ return name . getText ( file ) ;
232307 }
233308}
234309
310+ function matchNode ( a : Node , b : Node , file : SourceFile ) : boolean {
311+ if ( a . kind !== b . kind ) {
312+ return false ;
313+ }
314+ if ( a . kind === SyntaxKind . Constructor ) {
315+ return a . kind === b . kind ;
316+ }
317+
318+ if ( isNamedDeclaration ( a ) && isNamedDeclaration ( b ) ) {
319+ const aName = getIdentifier ( a , file ) ;
320+ const bName = getIdentifier ( b , file ) ;
321+ return ! ! ( aName && bName && aName === bName ) ;
322+ }
323+
324+ // TODO: Maybe match by other characteristics?
325+ return false ;
326+ }
327+
328+ function findAncestor ( node : Node , f : ( node : Node ) => boolean ) : Node | undefined {
329+ while ( node ) {
330+ if ( f ( node ) ) {
331+ return node ;
332+ }
333+ node = node . parent ;
334+ }
335+ }
0 commit comments