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:
Timothee Guerin 2024-06-11 13:06:12 -07:00 коммит произвёл GitHub
Родитель 7912e8c9c7
Коммит dd61517865
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 275 добавлений и 121 удалений

Просмотреть файл

@ -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,12 +1835,35 @@ 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
and blank line above
""")
model Foo {}
`,
});
});
it("keeps indent relative to closing quotes", async () => {
await assertFormat({
code: `
@doc( """
this is a doc.
that
span
@ -1851,12 +1874,31 @@ model Foo {}
`,
expected: `
@doc("""
this is a doc.
that
span
multiple lines.
""")
model Foo {}
`,
});
});
this is a doc.
that
span
multiple lines.
""")
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:
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/*`
- 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/*`
""")
- 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.