Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format multi line strings #3422

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---

Formatter: Indent or dedent multiline strings to the current indentation
14 changes: 7 additions & 7 deletions docs/getting-started/typespec-for-openapi-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,13 @@ and can contain markdown formatting.

```typespec
@doc("""
Get status info for the service.
The status includes the current version of the service.
The status value may be one of:
- `ok`: the service is operating normally
- `degraded`: the service is operating in a degraded state
- `down`: the service is not operating
""")
Get status info for the service.
The status includes the current version of the service.
The status value may be one of:
- `ok`: the service is operating normally
- `degraded`: the service is operating in a degraded state
- `down`: the service is not operating
""")
@tag("Status")
@route("/status")
@get
Expand Down
8 changes: 4 additions & 4 deletions docs/language-basics/type-literals.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Multi-line string literals are denoted using three double quotes `"""`.

```typespec
alias Str = """
This is a multi line string
- opt 1
- opt 2
""";
This is a multi line string
- opt 1
- opt 2
""";
```

- The opening `"""` must be followed by a new line.
Expand Down
128 changes: 116 additions & 12 deletions packages/compiler/src/formatter/print/printer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { AstPath, Doc, Printer } from "prettier";
import { builders } from "prettier/doc";
import { isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../../core/charcode.js";
import {
CharCode,
isIdentifierContinue,
isIdentifierStart,
utf16CodeUnits,
} from "../../core/charcode.js";
import { compilerAssert } from "../../core/diagnostics.js";
import { Keywords } from "../../core/scanner.js";
import {
Expand Down Expand Up @@ -86,7 +91,19 @@ import { commentHandler } from "./comment-handler.js";
import { needsParens } from "./needs-parens.js";
import { DecorableNode, PrettierChildPrint, TypeSpecPrettierOptions } from "./types.js";
import { util } from "./util.js";
const { align, breakParent, group, hardline, ifBreak, indent, join, line, softline } = builders;
const {
align,
breakParent,
group,
hardline,
ifBreak,
indent,
join,
line,
softline,
literalline,
markAsRoot,
} = builders;

const { isNextLineEmpty } = util as any;

Expand Down Expand Up @@ -688,7 +705,8 @@ function printCallOrDecoratorArgs(
const shouldHug =
node.arguments.length === 1 &&
(node.arguments[0].kind === SyntaxKind.ModelExpression ||
node.arguments[0].kind === SyntaxKind.StringLiteral);
node.arguments[0].kind === SyntaxKind.StringLiteral ||
node.arguments[0].kind === SyntaxKind.StringTemplateExpression);

if (shouldHug) {
return [
Expand Down Expand Up @@ -1636,7 +1654,28 @@ function printStringLiteral(
options: TypeSpecPrettierOptions
): Doc {
const node = path.node;
return getRawText(node, options);
const multiline = isMultiline(node, options);

const raw = getRawText(node, options);
if (multiline) {
const lines = splitLines(raw.slice(3));
const whitespaceIndent = lines[lines.length - 1].length - 3;
const newLines = trimMultilineString(lines, whitespaceIndent);
return [`"""`, indent(markAsRoot(newLines))];
} else {
return raw;
}
}

function isMultiline(
node: StringLiteralNode | StringTemplateExpressionNode,
options: TypeSpecPrettierOptions
) {
return (
options.originalText[node.pos] &&
options.originalText[node.pos + 1] === `"` &&
options.originalText[node.pos + 2] === `"`
);
}

function printNumberLiteral(
Expand Down Expand Up @@ -1870,14 +1909,79 @@ export function printStringTemplateExpression(
print: PrettierChildPrint
) {
const node = path.node;
const content = [
getRawText(node.head, options),
path.map((span: AstPath<StringTemplateSpanNode>) => {
const expression = span.call(print, "expression");
return [expression, getRawText(span.node.literal, options)];
}, "spans"),
];
return content;
const multiline = isMultiline(node, options);
const rawHead = getRawText(node.head, options);
if (multiline) {
const lastSpan = node.spans[node.spans.length - 1];
const lastLines = splitLines(getRawText(lastSpan.literal, options));
const whitespaceIndent = lastLines[lastLines.length - 1].length - 3;
const content = [
trimMultilineString(splitLines(rawHead.slice(3)), whitespaceIndent),
path.map((span: AstPath<StringTemplateSpanNode>) => {
const expression = span.call(print, "expression");
const spanRawText = getRawText(span.node.literal, options);
const spanLines = splitLines(spanRawText);
return [
expression,
spanLines[0],
literalline,
trimMultilineString(spanLines.slice(1), whitespaceIndent),
];
}, "spans"),
];

return [`"""`, indent(markAsRoot([content]))];
} else {
const content = [
rawHead,
path.map((span: AstPath<StringTemplateSpanNode>) => {
const expression = span.call(print, "expression");
return [expression, getRawText(span.node.literal, options)];
}, "spans"),
];
return content;
}
}

function splitLines(text: string): string[] {
const lines = [];
let start = 0;
let pos = 0;

while (pos < text.length) {
const ch = text.charCodeAt(pos);
switch (ch) {
case CharCode.CarriageReturn:
if (text.charCodeAt(pos + 1) === CharCode.LineFeed) {
lines.push(text.slice(start, pos));
start = pos;
pos++;
} else {
lines.push(text.slice(start, pos));
start = pos;
}
break;
case CharCode.LineFeed:
lines.push(text.slice(start, pos));
start = pos;
break;
}
pos++;
}

lines.push(text.slice(start));
return lines;
}

function trimMultilineString(lines: string[], whitespaceIndent: number): Doc[] {
const newLines = [];
for (let i = 0; i < lines.length; i++) {
newLines.push(lines[i].slice(whitespaceIndent));
if (i < lines.length - 1) {
newLines.push(literalline);
}
}
return newLines;
}

function printItemList<T extends Node>(
Expand Down
72 changes: 57 additions & 15 deletions packages/compiler/test/formatter/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1807,7 +1807,7 @@ namespace Foo {
});
});

describe("string literals", () => {
describe("single line string literals", () => {
it("format single line string literal", async () => {
await assertFormat({
code: `
Expand Down Expand Up @@ -1835,28 +1835,70 @@ model Foo {}
`,
});
});
});

it("format multi line string literal", async () => {
describe("multi line string literals", () => {
it("keeps trailing whitespaces", async () => {
await assertFormat({
code: `
@doc( """

this is a doc.
that
span
multiple lines.
3 whitespaces

and blank line above
"""
)
model Foo {}
`,
expected: `
@doc("""
3 whitespaces

this is a doc.
that
and blank line above
""")
model Foo {}
`,
});
});

it("keeps indent relative to closing quotes", async () => {
await assertFormat({
code: `
@doc( """
this is a doc.
that
span
multiple lines.
""")
"""
)
model Foo {}
`,
expected: `
@doc("""
this is a doc.
that
span
multiple lines.
""")
model Foo {}
`,
});
});

it("keeps escaped charaters", async () => {
await assertFormat({
code: `
@doc( """
with \\n
and \\t
"""
)
model Foo {}
`,
expected: `
@doc("""
with \\n
and \\t
""")
model Foo {}
`,
});
Expand Down Expand Up @@ -2847,11 +2889,11 @@ alias T = "foo \${{
await assertFormat({
code: `
alias T = """
This \${ "one" } goes over
multiple
\${ "two" }
lines
""";`,
This \${ "one" } goes over
multiple
\${ "two" }
lines
""";`,
expected: `
alias T = """
This \${"one"} goes over
Expand Down
6 changes: 3 additions & 3 deletions packages/samples/specs/grpc-kiosk-example/kiosk.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ model Kiosk {
}

@doc("""
Describes a digital sign.
Signs can include text, images, or both.
""")
Describes a digital sign.
Signs can include text, images, or both.
""")
model Sign {
@doc("unique id")
id?: int32; // Output only.
Expand Down
12 changes: 6 additions & 6 deletions packages/samples/specs/grpc-kiosk-example/types.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ model Timestamp {
}

@doc("""
An object that represents a latitude/longitude pair. This is expressed as a
pair of doubles to represent degrees latitude and degrees longitude. Unless
specified otherwise, this must conform to the
<a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84
standard</a>. Values must be within normalized ranges.
""")
An object that represents a latitude/longitude pair. This is expressed as a
pair of doubles to represent degrees latitude and degrees longitude. Unless
specified otherwise, this must conform to the
<a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84
standard</a>. Values must be within normalized ranges.
""")
model LatLng {
// The latitude in degrees. It must be in the range [-90.0, +90.0].
latitude: float64;
Expand Down
Loading
Loading