codekingpro/portable-devtools
115k
1import { diffLines } from '../diff/line.js';
2export const INCLUDE_HEADERS = {
3 includeIndex: true,
4 includeUnderline: true,
5 includeFileHeaders: true
6};
7export const FILE_HEADERS_ONLY = {
8 includeIndex: false,
9 includeUnderline: false,
10 includeFileHeaders: true
11};
12export const OMIT_HEADERS = {
13 includeIndex: false,
14 includeUnderline: false,
15 includeFileHeaders: false
16};
17export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
18 let optionsObj;
19 if (!options) {
20 optionsObj = {};
21 }
22 else if (typeof options === 'function') {
23 optionsObj = { callback: options };
24 }
25 else {
26 optionsObj = options;
27 }
28 if (typeof optionsObj.context === 'undefined') {
29 optionsObj.context = 4;
30 }
31 // We copy this into its own variable to placate TypeScript, which thinks
32 // optionsObj.context might be undefined in the callbacks below.
33 const context = optionsObj.context;
34 // @ts-expect-error (runtime check for something that is correctly a static type error)
35 if (optionsObj.newlineIsToken) {
36 throw new Error('newlineIsToken may not be used with patch-generation functions, only with diffing functions');
37 }
38 if (!optionsObj.callback) {
39 return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj));
40 }
41 else {
42 const { callback } = optionsObj;
43 diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => {
44 const patch = diffLinesResultToPatch(diff);
45 // TypeScript is unhappy without the cast because it does not understand that `patch` may
46 // be undefined here only if `callback` is StructuredPatchCallbackAbortable:
47 callback(patch);
48 } }));
49 }
50 function diffLinesResultToPatch(diff) {
51 // STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays
52 // of lines containing trailing newline characters. We'll tidy up later...
53 if (!diff) {
54 return;
55 }
56 diff.push({ value: '', lines: [] }); // Append an empty value to make cleanup easier
57 function contextLines(lines) {
58 return lines.map(function (entry) { return ' ' + entry; });
59 }
60 const hunks = [];
61 let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1;
62 for (let i = 0; i < diff.length; i++) {
63 const current = diff[i], lines = current.lines || splitLines(current.value);
64 current.lines = lines;
65 if (current.added || current.removed) {
66 // If we have previous context, start with that
67 if (!oldRangeStart) {
68 const prev = diff[i - 1];
69 oldRangeStart = oldLine;
70 newRangeStart = newLine;
71 if (prev) {
72 curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
73 oldRangeStart -= curRange.length;
74 newRangeStart -= curRange.length;
75 }
76 }
77 // Output our changes
78 for (const line of lines) {
79 curRange.push((current.added ? '+' : '-') + line);
80 }
81 // Track the updated file position
82 if (current.added) {
83 newLine += lines.length;
84 }
85 else {
86 oldLine += lines.length;
87 }
88 }
89 else {
90 // Identical context lines. Track line changes
91 if (oldRangeStart) {
92 // Close out any changes that have been output (or join overlapping)
93 if (lines.length <= context * 2 && i < diff.length - 2) {
94 // Overlapping
95 for (const line of contextLines(lines)) {
96 curRange.push(line);
97 }
98 }
99 else {
100 // end the range and output
101 const contextSize = Math.min(lines.length, context);
102 for (const line of contextLines(lines.slice(0, contextSize))) {
103 curRange.push(line);
104 }
105 const hunk = {
106 oldStart: oldRangeStart,
107 oldLines: (oldLine - oldRangeStart + contextSize),
108 newStart: newRangeStart,
109 newLines: (newLine - newRangeStart + contextSize),
110 lines: curRange
111 };
112 hunks.push(hunk);
113 oldRangeStart = 0;
114 newRangeStart = 0;
115 curRange = [];
116 }
117 }
118 oldLine += lines.length;
119 newLine += lines.length;
120 }
121 }
122 // Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add
123 // "\ No newline at end of file".
124 for (const hunk of hunks) {
125 for (let i = 0; i < hunk.lines.length; i++) {
126 if (hunk.lines[i].endsWith('\n')) {
127 hunk.lines[i] = hunk.lines[i].slice(0, -1);
128 }
129 else {
130 hunk.lines.splice(i + 1, 0, '\\ No newline at end of file');
131 i++; // Skip the line we just added, then continue iterating
132 }
133 }
134 }
135 return {
136 oldFileName: oldFileName, newFileName: newFileName,
137 oldHeader: oldHeader, newHeader: newHeader,
138 hunks: hunks
139 };
140 }
141}
142/**
143 * creates a unified diff patch.
144 * @param patch either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`)
145 */
146export function formatPatch(patch, headerOptions) {
147 if (!headerOptions) {
148 headerOptions = INCLUDE_HEADERS;
149 }
150 if (Array.isArray(patch)) {
151 if (patch.length > 1 && !headerOptions.includeFileHeaders) {
152 throw new Error('Cannot omit file headers on a multi-file patch. '
153 + '(The result would be unparseable; how would a tool trying to apply '
154 + 'the patch know which changes are to which file?)');
155 }
156 return patch.map(p => formatPatch(p, headerOptions)).join('\n');
157 }
158 const ret = [];
159 if (headerOptions.includeIndex && patch.oldFileName == patch.newFileName) {
160 ret.push('Index: ' + patch.oldFileName);
161 }
162 if (headerOptions.includeUnderline) {
163 ret.push('===================================================================');
164 }
165 if (headerOptions.includeFileHeaders) {
166 ret.push('--- ' + patch.oldFileName + (typeof patch.oldHeader === 'undefined' ? '' : '\t' + patch.oldHeader));
167 ret.push('+++ ' + patch.newFileName + (typeof patch.newHeader === 'undefined' ? '' : '\t' + patch.newHeader));
168 }
169 for (let i = 0; i < patch.hunks.length; i++) {
170 const hunk = patch.hunks[i];
171 // Unified Diff Format quirk: If the chunk size is 0,
172 // the first number is one lower than one would expect.
173 // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
174 if (hunk.oldLines === 0) {
175 hunk.oldStart -= 1;
176 }
177 if (hunk.newLines === 0) {
178 hunk.newStart -= 1;
179 }
180 ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines
181 + ' +' + hunk.newStart + ',' + hunk.newLines
182 + ' @@');
183 for (const line of hunk.lines) {
184 ret.push(line);
185 }
186 }
187 return ret.join('\n') + '\n';
188}
189export function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
190 if (typeof options === 'function') {
191 options = { callback: options };
192 }
193 if (!(options === null || options === void 0 ? void 0 : options.callback)) {
194 const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
195 if (!patchObj) {
196 return;
197 }
198 return formatPatch(patchObj, options === null || options === void 0 ? void 0 : options.headerOptions);
199 }
200 else {
201 const { callback } = options;
202 structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options), { callback: patchObj => {
203 if (!patchObj) {
204 callback(undefined);
205 }
206 else {
207 callback(formatPatch(patchObj, options.headerOptions));
208 }
209 } }));
210 }
211}
212export function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
213 return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);
214}
215/**
216 * Split `text` into an array of lines, including the trailing newline character (where present)
217 */
218function splitLines(text) {
219 const hasTrailingNl = text.endsWith('\n');
220 const result = text.split('\n').map(line => line + '\n');
221 if (hasTrailingNl) {
222 result.pop();
223 }
224 else {
225 result.push(result.pop().slice(0, -1));
226 }
227 return result;
228}
229 