@@ -8,13 +8,19 @@ import { addIgnoreWordToSettings, writeToSettings } from './config';
88
99let totalIssueCount = 0 ;
1010
11- export const writeChangeToFile = async ( url : URL , issue : Issue , replacer : string ) => {
12- const file = await readFile ( url , 'utf8' ) ;
13-
11+ export const updateFileText = ( issue : Issue , replacer : string , file : string ) => {
1412 // `issue.offset` is the index of the first character of the issue. With this, we slice everything in the file
1513 // before the text, then add the replacer, then add everything after the text.
16- const newFile = file . slice ( 0 , issue . offset ) + replacer + file . slice ( issue . offset + issue . text . length ) ;
17- await writeFile ( url , newFile ) ;
14+ return file . slice ( 0 , issue . offset ) + replacer + file . slice ( issue . offset + issue . text . length ) ;
15+ } ;
16+
17+ export const replaceSingleChange = async ( issue : Issue , issues : Issue [ ] , replacer : string ) => {
18+ previousState . resolvedIssues . push ( issue ) ;
19+ updateFutureIssues ( issue , replacer , issues ) ;
20+
21+ const url = new URL ( issue . uri ! ) ;
22+ const file = await readFile ( url , 'utf8' ) ;
23+ await writeFile ( url , updateFileText ( issue , replacer , file ) ) ;
1824} ;
1925
2026// When we replace a word, we need to update the offset of all future typos in that file, because they would be
@@ -66,15 +72,11 @@ export const updateFutureIssues = (issue: Issue, replacer: string, issues: Issue
6672 }
6773} ;
6874
69- export const fixIssue = async ( issues : Issue [ ] , issue : Issue , replacer : string , url : URL ) => {
70- previousState . resolvedIssues . push ( issue ) ;
71- updateFutureIssues ( issue , replacer , issues ) ;
72- await writeChangeToFile ( url , issue , replacer ) . catch ( ( ) => null ) ;
73- } ;
75+ export const performSideEffects = async ( issues : Issue [ ] , issue : Issue , action : Action , replacer : string ) => {
76+ const filesToWrite = new Map < string , { file : string ; url : URL } > ( ) ;
77+ const updatedIssues = [ ] ;
7478
75- export const performSideEffects = async ( issues : Issue [ ] , issue : Issue , action : Action , replacer : string , url : URL ) => {
76- // The issues array is modified in place, so we need to make a copy of it.
77- for ( const futureIssue of [ ...issues ] ) {
79+ for ( const [ idx , futureIssue ] of issues . entries ( ) ) {
7880 if (
7981 ! futureIssue . uri ||
8082 // If we're skipping the file, we only want to remove issues that share the same URI. Otherwise (action
@@ -83,22 +85,26 @@ export const performSideEffects = async (issues: Issue[], issue: Issue, action:
8385 ? futureIssue . uri !== issue . uri
8486 : futureIssue . text !== issue . text
8587 ) {
88+ updatedIssues . push ( futureIssue ) ;
8689 continue ;
8790 }
8891
89- issues . splice ( issues . indexOf ( futureIssue ) , 1 ) ;
92+ if ( action === Action . ReplaceAll ) {
93+ const url = new URL ( futureIssue . uri ! ) ;
94+ const file = filesToWrite . get ( futureIssue . uri ! ) ?. file ?? ( await readFile ( url , 'utf8' ) ) ;
95+ filesToWrite . set ( futureIssue . uri ! , { file : updateFileText ( futureIssue , replacer , file ) , url } ) ;
9096
91- // If we're ignoring the issue, we don't need to do any more work.
92- if ( action !== Action . ReplaceAll ) {
93- continue ;
97+ previousState . resolvedIssues . push ( futureIssue ) ;
98+ updateFutureIssues ( futureIssue , replacer , issues . slice ( idx + 1 ) ) ;
9499 }
95-
96- // Otherwise, we update the offsets of future issues and write the change to file, just as we would if it
97- // was a normal Replace action.
98- // TODO: If multiple of these issues are in the same file, we should store the file contents in memory and
99- // only write it once at the end.
100- await fixIssue ( issues , futureIssue , replacer , futureIssue . uri === issue . uri ? url : new URL ( futureIssue . uri ! ) ) ;
101100 }
101+
102+ // `issues.splice(0, issues.length, ...)` is used to replace the entire array in place. This is faster than calling
103+ // issues.splice individually, especially when there are many issues.
104+ issues . splice ( 0 , issues . length , ...updatedIssues ) ;
105+
106+ // Only write to a file once, instead of once per issue.
107+ await Promise . allSettled ( [ ...filesToWrite . values ( ) ] . map ( ( { url, file } ) => writeFile ( url , file ) ) ) ;
102108} ;
103109
104110export const restoreState = async ( issues : Issue [ ] ) => {
@@ -109,12 +115,21 @@ export const restoreState = async (issues: Issue[]) => {
109115 }
110116
111117 if ( replacer ) {
112- for ( const resolvedIssue of resolvedIssues ) {
113- // Replace the new replacer with the old text instead of the other way around.
114- const newReplacer = resolvedIssue . text ;
115- resolvedIssue . text = replacer ;
116- await writeChangeToFile ( new URL ( resolvedIssue . uri ! ) , resolvedIssue , newReplacer ) . catch ( ( ) => null ) ;
118+ // Abridged version of performSideEffects, but without updating future issues, because we're restoring the state
119+ // right after this.
120+ const filesToWrite = new Map < string , { file : string ; url : URL } > ( ) ;
121+ for ( const resolvedIssue of resolvedIssues . reverse ( ) ) {
122+ const url = new URL ( resolvedIssue . uri ! ) ;
123+ const file = filesToWrite . get ( resolvedIssue . uri ! ) ?. file ?? ( await readFile ( url , 'utf8' ) ) ;
124+
125+ // The issue text is now the replacer and it should be replaced with the original text to effectively revert
126+ // the change.
127+ const newFile = updateFileText ( { ...resolvedIssue , text : replacer } , resolvedIssue . text , file ) ;
128+
129+ filesToWrite . set ( resolvedIssue . uri ! , { file : newFile , url } ) ;
117130 }
131+
132+ await Promise . allSettled ( [ ...filesToWrite . values ( ) ] . map ( ( { url, file } ) => writeFile ( url , file ) ) ) ;
118133 }
119134
120135 // Edit the entire issues array in place, so that the reference is the same.
@@ -127,8 +142,7 @@ export const handleIssue = async (issues: Issue[], issue: Issue) => {
127142 return ;
128143 }
129144
130- const url = new URL ( issue . uri ) ;
131- const [ action , replacer ] = await determineAction ( url , issue , issues , totalIssueCount ) ;
145+ const [ action , replacer ] = await determineAction ( issue , issues , totalIssueCount ) ;
132146
133147 previousState . action = action ;
134148
@@ -155,13 +169,18 @@ export const handleIssue = async (issues: Issue[], issue: Issue) => {
155169 config : undefined
156170 } as Omit < typeof previousState , 'action' > ) ;
157171
158- if ( action === Action . Replace || action === Action . ReplaceAll ) {
159- await fixIssue ( issues , issue , replacer , url ) ;
160- }
172+ if ( action === Action . Replace ) {
173+ await replaceSingleChange ( issue , issues , replacer ) . catch ( ( ) => null ) ;
174+ } else if ( action === Action . ReplaceAll || action === Action . IgnoreAll || action === Action . SkipFile ) {
175+ // The above actions all have side effects (as in, they require modification of future issues).
176+
177+ // Add the current issue back to the list of issues so it can be used in the performSideEffects loop to prevent
178+ // duplicated code. It will be removed from the list in the loop.
179+ if ( action === Action . ReplaceAll ) {
180+ issues . unshift ( issue ) ;
181+ }
161182
162- // The following actions all have side effects (as in, they require modification of future issues).
163- if ( action === Action . ReplaceAll || action === Action . IgnoreAll || action === Action . SkipFile ) {
164- await performSideEffects ( issues , issue , action , replacer , url ) ;
183+ await performSideEffects ( issues , issue , action , replacer ) ;
165184
166185 if ( action === Action . IgnoreAll ) {
167186 await addIgnoreWordToSettings ( issue . text ) . catch ( ( ) => null ) ;
0 commit comments