Skip to content

Commit 0fc9fcd

Browse files
authored
feat: be able to ignore region from format (#1534)
1 parent b3edbf4 commit 0fc9fcd

File tree

5 files changed

+122
-20
lines changed

5 files changed

+122
-20
lines changed

client/testFixture/formatter/expected.sas

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,13 @@ print('hello')
172172
endinteractive;
173173
/* comment */
174174
run;
175+
176+
proc format library=library;
177+
/* region format-ignore */
178+
invalue evaluation 'O'=4
179+
'S'=3
180+
'E'=2
181+
'C'=1
182+
'N'=0;
183+
/* endregion */
184+
run;

client/testFixture/formatter/unformatted.sas

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,13 @@ i;
165165
print('hello')
166166
endinteractive;
167167
/* comment */
168+
run;
169+
proc format library=library;
170+
/* region format-ignore */
171+
invalue evaluation 'O'=4
172+
'S'=3
173+
'E'=2
174+
'C'=1
175+
'N'=0;
176+
/* endregion */
168177
run;

server/src/sas/formatter/parser.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import { Lexer, Token as RealToken } from "../Lexer";
4-
import type { FoldingBlock } from "../LexerEx";
4+
import { type FoldingBlock, LexerEx } from "../LexerEx";
55
import type { Model } from "../Model";
66
import type { SyntaxProvider } from "../SyntaxProvider";
7+
import { isSamePosition } from "../utils";
78

89
interface FakeToken extends Omit<RealToken, "type"> {
910
type: "raw-data";
@@ -33,6 +34,11 @@ export type SASAST = Program | Region | Statement | Token;
3334
export const isComment = (token: Token) =>
3435
token.type === "comment" || token.type === "macro-comment";
3536

37+
const isAtBlockEnd = (block: FoldingBlock | undefined, token: Token) =>
38+
block &&
39+
block.endLine === token.end.line &&
40+
block.endCol === token.end.column;
41+
3642
const removePrevStatement = (parent: Program | Region) => {
3743
if (parent.children.length <= 1) {
3844
return;
@@ -167,6 +173,58 @@ const preserveProcs = (
167173
return current;
168174
};
169175

176+
const preserveCustomRegion = (
177+
current: number,
178+
statement: Statement,
179+
model: Model,
180+
syntaxProvider: SyntaxProvider,
181+
) => {
182+
const token = statement.children[statement.children.length - 1];
183+
if (current === -1) {
184+
if (isComment(token) && /\*\s*region\s+format-ignore\b/.test(token.text)) {
185+
const block = syntaxProvider.getFoldingBlock(
186+
token.start.line,
187+
token.start.column,
188+
true,
189+
false,
190+
true,
191+
);
192+
if (
193+
block &&
194+
block.type === LexerEx.SEC_TYPE.CUSTOM &&
195+
block.startLine === token.start.line &&
196+
block.startCol === token.start.column
197+
) {
198+
current = 0;
199+
const start = token.start;
200+
const end = { line: block.endLine, column: block.endCol };
201+
statement.children.pop();
202+
statement.children.push({
203+
type: "raw-data",
204+
text: model.getText({ start, end }),
205+
start,
206+
end,
207+
});
208+
}
209+
}
210+
} else if (current === 0) {
211+
statement.children.pop();
212+
if (
213+
statement &&
214+
statement.children.length > 0 &&
215+
isSamePosition(
216+
token.end,
217+
statement.children[statement.children.length - 1].end,
218+
)
219+
) {
220+
current = 1;
221+
}
222+
} else if (current === 1) {
223+
return -1;
224+
}
225+
return current;
226+
};
227+
170228
const preserveQuoting = (
171229
current: number,
172230
statement: Statement,
@@ -222,11 +280,13 @@ export const getParser =
222280
let prevStatement: Statement | undefined = undefined;
223281
let quoting = -1;
224282
let preserveProc = -1;
283+
let preserveCustom = -1;
225284

226285
for (let i = 0; i < tokens.length; i++) {
227286
const node = tokens[i];
228287
let parent = parents.length ? parents[parents.length - 1] : root;
229288

289+
//#region --- Preserve Python/Lua
230290
if (region && region.block) {
231291
preserveProc = preserveProcs(preserveProc, region, node, model);
232292
if (preserveProc === 0 && i === tokens.length - 1) {
@@ -245,8 +305,9 @@ export const getParser =
245305
if (node.type === "embedded-code") {
246306
continue;
247307
}
308+
//#endregion ---
248309

249-
// --- Check for block start: DATA, PROC, %MACRO ---
310+
//#region --- Check for block start: DATA, PROC, %MACRO
250311
if (node.type === "sec-keyword" || node.type === "macro-sec-keyword") {
251312
const block = syntaxProvider.getFoldingBlock(
252313
node.start.line,
@@ -276,9 +337,9 @@ export const getParser =
276337
}
277338
}
278339
}
279-
// --- ---
340+
//#endregion ---
280341

281-
// --- Check for statement start ---
342+
//#region --- Check for statement start
282343
if (!currentStatement) {
283344
currentStatement = {
284345
type: "statement",
@@ -301,16 +362,30 @@ export const getParser =
301362
parent.children.push(currentStatement);
302363
}
303364
}
304-
// --- ---
365+
//#endregion ---
305366

306367
currentStatement.children.push(node);
307368

369+
preserveCustom = preserveCustomRegion(
370+
preserveCustom,
371+
currentStatement,
372+
model,
373+
syntaxProvider,
374+
);
375+
if (preserveCustom >= 0) {
376+
if (preserveCustom === 1) {
377+
prevStatement = currentStatement;
378+
currentStatement = undefined;
379+
}
380+
continue;
381+
}
382+
308383
quoting = preserveQuoting(quoting, currentStatement, model);
309384
if (quoting >= 0) {
310385
continue;
311386
}
312387

313-
// --- Check for statement end ---
388+
//#region --- Check for statement end
314389
if (node.type === "sep" && node.text === ";") {
315390
if (
316391
currentStatement.children[0].type === "cards-data" &&
@@ -353,12 +428,7 @@ export const getParser =
353428
// put `end` out of region children to outdent
354429
parent.children.push(region.children.pop()!);
355430
region = parents.pop();
356-
} else if (
357-
region &&
358-
region.block &&
359-
region.block.endLine === node.end.line &&
360-
region.block.endCol === node.end.column
361-
) {
431+
} else if (region && isAtBlockEnd(region.block, node)) {
362432
// block end
363433
if (/^(run|quit|%mend)\b/i.test(currentStatement.children[0].text)) {
364434
// put `run` out of section children to outdent
@@ -389,12 +459,7 @@ export const getParser =
389459
// standalone comment, treat as a whole statement
390460
prevStatement = currentStatement;
391461
currentStatement = undefined;
392-
if (
393-
region &&
394-
region.block &&
395-
region.block.endLine === node.end.line &&
396-
region.block.endCol === node.end.column
397-
) {
462+
if (isAtBlockEnd(region?.block, node)) {
398463
region = parents.pop();
399464
}
400465
} else if (
@@ -411,7 +476,7 @@ export const getParser =
411476
prevStatement = currentStatement;
412477
currentStatement = undefined;
413478
}
414-
// --- ---
479+
//#endregion ---
415480
}
416481

417482
return root;

website/docs/Features/sasCodeEditing.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ To format your code, open context menu and select `Format Document`.
9494

9595
![formatter](/images/formatter.gif)
9696

97+
:::tip
98+
99+
You can define custom regions as below to exclude code from being formatted.
100+
101+
```sas
102+
proc format library=library;
103+
/* region format-ignore */
104+
invalue evaluation 'O'=4
105+
'S'=3
106+
'E'=2
107+
'C'=1
108+
'N'=0;
109+
/* endregion */
110+
run;
111+
```
112+
113+
:::
114+
97115
## Function Signature Help
98116

99117
Signature help provides information for current parameter as you are writing function calls.

website/docusaurus.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const config: Config = {
9494
prism: {
9595
theme: prismThemes.github,
9696
darkTheme: prismThemes.dracula,
97-
additionalLanguages: ["bash", "json"],
97+
additionalLanguages: ["bash", "json", "sas"],
9898
},
9999
} satisfies Preset.ThemeConfig,
100100

0 commit comments

Comments
 (0)