Skip to content

Commit 524b80f

Browse files
authored
Merge pull request #2742 from pbwolf/healer-module
Healer module
2 parents d307848 + 7f1d192 commit 524b80f

File tree

4 files changed

+110
-21
lines changed

4 files changed

+110
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changes to Calva.
55
## [Unreleased]
66

77
- Bump deps.clj.jar to v1.12.0.1517
8+
- Add unit tests for the reformatter's "healer" of incomplete forms
89

910
## [2.0.486] - 2025-02-16
1011

src/calva-fmt/src/format.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isUndefined, cloneDeep } from 'lodash';
99
import { LispTokenCursor } from '../../cursor-doc/token-cursor';
1010
import { formatIndex } from './format-index';
1111
import * as state from '../../state';
12+
import * as healer from './healer';
1213

1314
const FormatDepthDefaults = {
1415
deftype: 2,
@@ -59,27 +60,9 @@ export function formatRangeEdits(
5960
if (!cursor.withinString() && !cursor.withinComment()) {
6061
const eol = _convertEolNumToStringNotation(document.eol);
6162
const originalText = document.getText(originalRange);
62-
const leadingWs = originalText.match(/^\s*/)[0];
63-
const trailingWs = originalText.match(/\s*$/)[0];
64-
const missingTexts = cursorDocUtils.getMissingBrackets(originalText);
65-
const healedText = `${missingTexts.prepend}${originalText.trim()}${missingTexts.append}`;
66-
const formattedHealedText = formatCode(healedText, document.eol);
67-
const leadingEolPos = leadingWs.lastIndexOf(eol);
68-
const startIndent =
69-
leadingEolPos === -1
70-
? originalRange.start.character
71-
: leadingWs.length - leadingEolPos - eol.length;
72-
const formattedText = formattedHealedText
73-
.substring(
74-
missingTexts.prepend.length,
75-
missingTexts.prepend.length + formattedHealedText.length - missingTexts.append.length
76-
)
77-
.split(eol)
78-
.map((line: string, i: number) => (i === 0 ? line : `${' '.repeat(startIndent)}${line}`))
79-
.join(eol);
80-
const newText = `${formattedText.startsWith(leadingWs) ? '' : leadingWs}${formattedText}${
81-
formattedText.endsWith(trailingWs) ? '' : trailingWs
82-
}`;
63+
const healing = healer.bandage(originalText, originalRange.start.character, eol);
64+
const formattedHealedText = formatCode(healing.healedText, document.eol);
65+
const newText = healer.unbandage(healing, formattedHealedText);
8366
return [vscode.TextEdit.replace(originalRange, newText)];
8467
}
8568
}

src/calva-fmt/src/healer.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @module
3+
* Facilitator of code formatting by
4+
* converting original text to 'healed' formattable text,
5+
* and converting formatted 'healed' text to
6+
* an 'unhealed' formatted string fit for use as a substitute for the original
7+
*/
8+
9+
import * as cursorDocUtils from '../../cursor-doc/utilities';
10+
11+
/**
12+
* 'Healed' text suitable for code-formatting
13+
* and details with which a formatted version of it
14+
* can be translated back to unhealed replacment text
15+
*/
16+
type BandageContext = {
17+
healedText: string;
18+
details: {
19+
originalIndent: number;
20+
originalText: string;
21+
eol: string;
22+
missingTexts: cursorDocUtils.MissingTexts;
23+
};
24+
};
25+
26+
/**
27+
* Formattable text (fixed up to compensate for apparently missing list openers or closers)
28+
* and context with which to unbandage a formatted version of the same and recover the exact text
29+
* to substitute back into the document.
30+
* @param originalText Text to be reformatted
31+
* @param originalIndent Character offset where the first line of originalText begins
32+
* @param eol Newline characters that should be used in the code
33+
* @returns healedText - patched-up forms to be given to a code formatter, and other context the unbandage function needs
34+
*/
35+
export function bandage(originalText: string, originalIndent: number, eol: string): BandageContext {
36+
const missingTexts = cursorDocUtils.getMissingBrackets(originalText);
37+
const healedText = `${missingTexts.prepend}${originalText.trim()}${missingTexts.append}`;
38+
return {
39+
healedText,
40+
details: { eol, originalText, originalIndent, missingTexts },
41+
};
42+
}
43+
44+
/**
45+
* Formatted substitute text derived from formattedHealedText by removing bandages and reimposing an indentation.
46+
* @param context Context from bandage
47+
* @param formattedHealedText Formatted version of the healedText
48+
* @returns
49+
*/
50+
export function unbandage(context: BandageContext, formattedHealedText: string) {
51+
const d = context.details;
52+
const leadingWs = d.originalText.match(/^\s*/)[0];
53+
const trailingWs = d.originalText.match(/\s*$/)[0];
54+
const leadingEolPos = leadingWs.lastIndexOf(d.eol);
55+
const startIndent =
56+
leadingEolPos === -1 ? d.originalIndent : leadingWs.length - leadingEolPos - d.eol.length;
57+
const formattedText = formattedHealedText
58+
.substring(
59+
d.missingTexts.prepend.length,
60+
d.missingTexts.prepend.length + formattedHealedText.length - d.missingTexts.append.length
61+
)
62+
.split(d.eol)
63+
.map((line: string, i: number) => (i === 0 ? line : `${' '.repeat(startIndent)}${line}`))
64+
.join(d.eol);
65+
const newText = `${formattedText.startsWith(leadingWs) ? '' : leadingWs}${formattedText}${
66+
formattedText.endsWith(trailingWs) ? '' : trailingWs
67+
}`;
68+
return newText;
69+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as expect from 'expect';
2+
import * as healer from '../../../calva-fmt/src/healer';
3+
import * as model from '../../../cursor-doc/model';
4+
5+
model.initScanner(20000);
6+
7+
describe('calva-fmt', () => {
8+
it('Heals unopened list', () => {
9+
const originalDoc = '(def a[:x :y :z ])';
10+
// \----------/
11+
const originalFrag = originalDoc.substring(6);
12+
const healing = healer.bandage(originalFrag, 6, '\n');
13+
expect(healing.healedText).toEqual('([:x :y :z ])');
14+
const formattedHealed = '([:x :y :z])';
15+
const replacement = healer.unbandage(healing, formattedHealed);
16+
expect(replacement).toEqual('[:x :y :z])');
17+
});
18+
it('Heals unclosed vector', () => {
19+
const originalDoc = '(def a[:x :y :z ]) ';
20+
// \--------/
21+
const originalFrag = originalDoc.substring(6, 16);
22+
const healing = healer.bandage(originalFrag, 6, '\n');
23+
expect(healing.healedText).toEqual('[:x :y :z]');
24+
const formattedHealed = '[:x :y :z]';
25+
const replacement = healer.unbandage(healing, formattedHealed);
26+
expect(replacement).toEqual('[:x :y :z ');
27+
});
28+
it('Indents subsequent lines relative to first-line indent', () => {
29+
const originalFrag = '(def a\n42)';
30+
const healing = healer.bandage(originalFrag, 4, '\n');
31+
expect(healing.healedText).toEqual('(def a\n42)');
32+
const formattedHealed = '(def a\n 42)';
33+
const replacement = healer.unbandage(healing, formattedHealed);
34+
expect(replacement).toEqual('(def a\n 42)');
35+
});
36+
});

0 commit comments

Comments
 (0)