Format multi line strings (#3422)
fix #1016 Format multi lines string ``` @doc( """ multi line do ${"abc"} """ ) @doc(""" def """) model Foo {} alias T = """ abc def ${"abc"} ghi """; ``` formats to ``` @doc(""" multi line do ${"abc"} """) @doc(""" def """) model Foo {} alias T = """ abc def ${"abc"} ghi """; ```
This commit is contained in:
Родитель
7912e8c9c7
Коммит
dd61517865
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -689,7 +706,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 [
|
||||
|
@ -1637,7 +1655,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(
|
||||
|
@ -1871,14 +1910,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>(
|
||||
|
|
|
@ -1807,7 +1807,7 @@ namespace Foo {
|
|||
});
|
||||
});
|
||||
|
||||
describe("string literals", () => {
|
||||
describe("single line string literals", () => {
|
||||
it("format single line string literal", async () => {
|
||||
await assertFormat({
|
||||
code: `
|
||||
|
@ -1835,14 +1835,37 @@ model Foo {}
|
|||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("format multi line string literal", async () => {
|
||||
describe("multi line string literals", () => {
|
||||
it("keeps trailing whitespaces", async () => {
|
||||
await assertFormat({
|
||||
code: `
|
||||
@doc( """
|
||||
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.
|
||||
"""
|
||||
|
@ -1851,12 +1874,31 @@ model Foo {}
|
|||
`,
|
||||
expected: `
|
||||
@doc("""
|
||||
|
||||
this is a doc.
|
||||
that
|
||||
span
|
||||
multiple lines.
|
||||
""")
|
||||
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 {}
|
||||
`,
|
||||
});
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,16 +14,16 @@ using TypeSpec.Http;
|
|||
namespace GrpcLibrarySample;
|
||||
|
||||
@doc("""
|
||||
This API represents a simple digital library. It lets you manage Shelf
|
||||
resources and Book resources in the library. It defines the following
|
||||
resource model:
|
||||
|
||||
- The API has a collection of [Shelf][google.example.library.v1.Shelf]
|
||||
resources, named `shelves/*`
|
||||
|
||||
- Each Shelf has a collection of [Book][google.example.library.v1.Book]
|
||||
resources, named `shelves/*/books/*`
|
||||
""")
|
||||
This API represents a simple digital library. It lets you manage Shelf
|
||||
resources and Book resources in the library. It defines the following
|
||||
resource model:
|
||||
|
||||
- The API has a collection of [Shelf][google.example.library.v1.Shelf]
|
||||
resources, named `shelves/*`
|
||||
|
||||
- Each Shelf has a collection of [Book][google.example.library.v1.Book]
|
||||
resources, named `shelves/*/books/*`
|
||||
""")
|
||||
@route("/v1")
|
||||
@tag("LibraryService")
|
||||
namespace LibraryService {
|
||||
|
@ -37,9 +37,9 @@ namespace LibraryService {
|
|||
op getShelf(...GetShelfRequest): Shelf | RpcStatus;
|
||||
|
||||
@doc("""
|
||||
Lists shelves. The order is unspecified but deterministic. Newly created
|
||||
shelves will not necessarily be added to the end of this list.
|
||||
""")
|
||||
Lists shelves. The order is unspecified but deterministic. Newly created
|
||||
shelves will not necessarily be added to the end of this list.
|
||||
""")
|
||||
@route("shelves")
|
||||
op listShelves(...ListRequestBase): ListShelvesResponse | RpcStatus;
|
||||
|
||||
|
@ -49,13 +49,13 @@ shelves will not necessarily be added to the end of this list.
|
|||
op deleteShelf(...DeleteShelfRequest): void | RpcStatus;
|
||||
|
||||
@doc("""
|
||||
Merges two shelves by adding all books from the shelf named
|
||||
`other_shelf_name` to shelf `name`, and deletes
|
||||
`other_shelf_name`. Returns the updated shelf.
|
||||
The book ids of the moved books may not be the same as the original books.
|
||||
Returns NOT_FOUND if either shelf does not exist.
|
||||
This call is a no-op if the specified shelves are the same.
|
||||
""")
|
||||
Merges two shelves by adding all books from the shelf named
|
||||
`other_shelf_name` to shelf `name`, and deletes
|
||||
`other_shelf_name`. Returns the updated shelf.
|
||||
The book ids of the moved books may not be the same as the original books.
|
||||
Returns NOT_FOUND if either shelf does not exist.
|
||||
This call is a no-op if the specified shelves are the same.
|
||||
""")
|
||||
@route("shelves/{name}:merge")
|
||||
@post
|
||||
op mergeShelves(...MergeShelvesRequest): Shelf | RpcStatus;
|
||||
|
@ -70,10 +70,10 @@ This call is a no-op if the specified shelves are the same.
|
|||
op getBook(...GetBookRequest): Book | RpcStatus;
|
||||
|
||||
@doc("""
|
||||
Lists books in a shelf. The order is unspecified but deterministic. Newly
|
||||
created books will not necessarily be added to the end of this list.
|
||||
Returns NOT_FOUND if the shelf does not exist.
|
||||
""")
|
||||
Lists books in a shelf. The order is unspecified but deterministic. Newly
|
||||
created books will not necessarily be added to the end of this list.
|
||||
Returns NOT_FOUND if the shelf does not exist.
|
||||
""")
|
||||
@route("shelves/{name}/books")
|
||||
op listBooks(...ListBooksRequest): ListBooksResponse | RpcStatus;
|
||||
|
||||
|
@ -84,19 +84,19 @@ Returns NOT_FOUND if the shelf does not exist.
|
|||
}
|
||||
|
||||
@doc("""
|
||||
The name of a book.
|
||||
Book names have the form `shelves/{shelf_id}/books/{book_id}`
|
||||
""")
|
||||
The name of a book.
|
||||
Book names have the form `shelves/{shelf_id}/books/{book_id}`
|
||||
""")
|
||||
@pattern("shelves/\\w+/books/\\w+")
|
||||
scalar book_name extends string;
|
||||
|
||||
@doc("A single book in the library.")
|
||||
model Book {
|
||||
@doc("""
|
||||
The resource name of the book.
|
||||
Book names have the form `shelves/{shelf_id}/books/{book_id}`.
|
||||
The name is ignored when creating a book.
|
||||
""")
|
||||
The resource name of the book.
|
||||
Book names have the form `shelves/{shelf_id}/books/{book_id}`.
|
||||
The name is ignored when creating a book.
|
||||
""")
|
||||
name: book_name;
|
||||
|
||||
@doc("The name of the book author.")
|
||||
|
@ -110,19 +110,19 @@ The name is ignored when creating a book.
|
|||
}
|
||||
|
||||
@doc("""
|
||||
The name of a shelf.
|
||||
Shelf names have the form `shelves/{shelf_id}`.
|
||||
""")
|
||||
The name of a shelf.
|
||||
Shelf names have the form `shelves/{shelf_id}`.
|
||||
""")
|
||||
@pattern("shelves/\\w+")
|
||||
scalar shelf_name extends string;
|
||||
|
||||
@doc("A Shelf contains a collection of books with a theme.")
|
||||
model Shelf {
|
||||
@doc("""
|
||||
The resource name of the shelf.
|
||||
Shelf names have the form `shelves/{shelf_id}`.
|
||||
The name is ignored when creating a shelf.
|
||||
""")
|
||||
The resource name of the shelf.
|
||||
Shelf names have the form `shelves/{shelf_id}`.
|
||||
The name is ignored when creating a shelf.
|
||||
""")
|
||||
name: shelf_name;
|
||||
|
||||
@doc("The theme of the shelf")
|
||||
|
@ -159,9 +159,9 @@ model DeleteShelfRequest {
|
|||
}
|
||||
|
||||
@doc("""
|
||||
Describes the shelf being removed (other_shelf_name) and updated
|
||||
(name) in this merge
|
||||
""")
|
||||
Describes the shelf being removed (other_shelf_name) and updated
|
||||
(name) in this merge
|
||||
""")
|
||||
model MergeShelvesRequest {
|
||||
@doc("The name of the shelf we're adding books to.")
|
||||
@path
|
||||
|
@ -229,9 +229,9 @@ model DeleteBookRequest {
|
|||
}
|
||||
|
||||
@doc("""
|
||||
Describes what book to move (name) and what shelf we're moving it
|
||||
to (other_shelf_name).
|
||||
""")
|
||||
Describes what book to move (name) and what shelf we're moving it
|
||||
to (other_shelf_name).
|
||||
""")
|
||||
model MoveBookRequest {
|
||||
@doc("The name of the book to move.")
|
||||
name: book_name;
|
||||
|
@ -246,30 +246,30 @@ model MoveBookRequest {
|
|||
|
||||
model ListRequestBase {
|
||||
@doc("""
|
||||
Requested page size. Server may return fewer shelves than requested.
|
||||
If unspecified, server will pick an appropriate default.
|
||||
""")
|
||||
Requested page size. Server may return fewer shelves than requested.
|
||||
If unspecified, server will pick an appropriate default.
|
||||
""")
|
||||
@query
|
||||
page_size?: int32;
|
||||
|
||||
@doc("""
|
||||
A token identifying a page of results the server should return.
|
||||
Typically, this is the value of
|
||||
[ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token]
|
||||
returned from the previous call to `ListShelves` method.
|
||||
""")
|
||||
A token identifying a page of results the server should return.
|
||||
Typically, this is the value of
|
||||
[ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token]
|
||||
returned from the previous call to `ListShelves` method.
|
||||
""")
|
||||
@query
|
||||
page_token?: string;
|
||||
}
|
||||
|
||||
model ListResponseBase {
|
||||
@doc("""
|
||||
A token to retrieve next page of results.
|
||||
Pass this value in the
|
||||
[ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token]
|
||||
field in the subsequent call to `ListShelves` method to retrieve the next
|
||||
page of results.
|
||||
""")
|
||||
A token to retrieve next page of results.
|
||||
Pass this value in the
|
||||
[ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token]
|
||||
field in the subsequent call to `ListShelves` method to retrieve the next
|
||||
page of results.
|
||||
""")
|
||||
next_page_token?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ alias myConst = "foobar";
|
|||
model Person {
|
||||
simple: "Simple ${123} end";
|
||||
multiline: """
|
||||
Multi
|
||||
${123}
|
||||
${true}
|
||||
line
|
||||
""";
|
||||
Multi
|
||||
${123}
|
||||
${true}
|
||||
line
|
||||
""";
|
||||
ref: "Ref this alias ${myConst} end";
|
||||
template: Template<"custom">;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче