From a8e5aec58906c94c15d8df1de5e06614380ba70d Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Fri, 27 Dec 2024 16:02:44 +0800 Subject: [PATCH 1/2] add codefix for `triple-quote-indent` warning ,and meanwhile add test case for it --- .../triple-quote-indent.codefix.ts | 152 ++++++++++++++++++ packages/compiler/src/core/parser.ts | 15 +- .../triple-quote-indent.codefix.test.ts | 63 ++++++++ 3 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/src/core/compiler-code-fixes/triple-quote-indent.codefix.ts create mode 100644 packages/compiler/test/core/compiler-code-fixes/triple-quote-indent.codefix.test.ts diff --git a/packages/compiler/src/core/compiler-code-fixes/triple-quote-indent.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/triple-quote-indent.codefix.ts new file mode 100644 index 0000000000..df1c1897d5 --- /dev/null +++ b/packages/compiler/src/core/compiler-code-fixes/triple-quote-indent.codefix.ts @@ -0,0 +1,152 @@ +import { isLineBreak, isWhiteSpaceSingleLine } from "../charcode.js"; +import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; +import type { CodeFixEdit, DiagnosticTarget, SourceLocation } from "../types.js"; + +export function createTripleQuoteIndentCodeFix(diagnosticTarget: DiagnosticTarget) { + return defineCodeFix({ + id: "triple-quote-indent", + label: "Format triple-quote-indent", + fix: (context) => { + const result: CodeFixEdit[] = []; + + const location = getSourceLocation(diagnosticTarget); + const { startPos: startPosArr, indent } = findStartPositionAndIndent(location); + startPosArr.map((pos) => { + const updatedLocation = { ...location, pos }; + result.push(context.prependText(updatedLocation, indent)); + }); + + return result; + }, + }); +} + +function isNoNewlineStartTripleQuote(start: number, end: number, input: string): boolean { + while (start < end && isWhiteSpaceSingleLine(input.charCodeAt(start))) { + start++; + } + return !isLineBreak(input.charCodeAt(start)); +} + +function isNoNewlineEndTripleQuote(start: number, end: number, input: string): boolean { + while (end > start && isWhiteSpaceSingleLine(input.charCodeAt(end - 1))) { + end--; + } + return !isLineBreak(input.charCodeAt(end - 1)); +} + +function getSpaceNumbBetweenStartPosAndVal(start: number, end: number, input: string): number { + while (start < end && isWhiteSpaceSingleLine(input.charCodeAt(start))) { + start++; + } + if (isLineBreak(input.charCodeAt(start))) { + start += 2; + } + + let spaceNumb = 0; + while (start < end && isWhiteSpaceSingleLine(input.charCodeAt(start))) { + spaceNumb++; + start++; + } + return spaceNumb; +} + +function getSpaceNumbBetweenEnterAndEndPos(start: number, end: number, input: string): number { + let spaceNumb = 0; + while (end > start && isWhiteSpaceSingleLine(input.charCodeAt(end - 1))) { + spaceNumb++; + end--; + } + return spaceNumb; +} + +function findStartPositionAndIndent(location: SourceLocation): { + startPos: number[]; + indent: string; +} { + const text = location.file.text; + const splitOrIndentStr = "\r\n"; + const startPos = location.pos; + const endPos = location.end; + const offSet = 3; // The length of `"""` + + const noNewlineStart = isNoNewlineStartTripleQuote(startPos + offSet, endPos, text); + const noNewlineEnd = isNoNewlineEndTripleQuote(startPos, endPos - offSet, text); + if (noNewlineStart && noNewlineEnd) { + // eg. `""" one two """` + return { startPos: [startPos + offSet, endPos - offSet], indent: splitOrIndentStr }; + } else if (noNewlineStart) { + // eg. `""" one two \r\n"""` + const startSpaceNumb = getSpaceNumbBetweenStartPosAndVal(startPos + offSet, endPos, text); + const endSpaceNumb = getSpaceNumbBetweenEnterAndEndPos(startPos, endPos - offSet, text); + + // Only in the case of equals, the `triple-quote-indent` warning will be triggered. + // The `no-new-line-start-triple-quote` warning is triggered when it is greater than + if (startSpaceNumb >= endSpaceNumb) { + return { startPos: [startPos + offSet], indent: splitOrIndentStr }; + } else { + return { + startPos: [startPos + offSet], + indent: splitOrIndentStr + " ".repeat(endSpaceNumb - startSpaceNumb), + }; + } + } else if (noNewlineEnd) { + // eg. `"""\r\n one two """` + const startSpaceNumb = getSpaceNumbBetweenStartPosAndVal(startPos + offSet, endPos, text); + const endSpaceNumb = getSpaceNumbBetweenEnterAndEndPos(startPos, endPos - offSet, text); + if (startSpaceNumb < endSpaceNumb) { + return { + startPos: [endPos - offSet], + indent: splitOrIndentStr + " ".repeat(startSpaceNumb), + }; + } else { + // Detailed description: `no-new-line-start-triple-quote`, `no-new-line-end-triple-quote` + // and `triple-quote-indent` are all warnings about incorrect triple quote values. + // Currently, only `triple-quote-indent` has a quick fix. + // Todo: add codefix for `no-new-line-start-triple-quote` and `no-new-line-end-triple-quote` warning + + // It will only warn that the ending """ is not on a new line and will not trigger the triple quote indent warning. + return { + startPos: [endPos - offSet], + indent: splitOrIndentStr + " ".repeat(startSpaceNumb - endSpaceNumb), + }; + } + } else { + // eg. `"""\r\none\r\n two\r\n """` + const endSpaceNumb = getSpaceNumbBetweenEnterAndEndPos(startPos, endPos - offSet, text); + const arrIndents: number[] = []; + let start = startPos + offSet; + + // Calculate the number of spaces needed to align each line + while (start > 0 && start < endPos) { + const currLineSpaceNumb = getSpaceNumbBetweenStartPosAndVal(start, endPos, text); + arrIndents.push(currLineSpaceNumb); + + // If it is 0, the method indexOf cannot get the next `\r\n` position, so add 1 + start += currLineSpaceNumb === 0 ? 1 : currLineSpaceNumb; + start = text.indexOf(splitOrIndentStr, start); + } + + // Find all the positions of `\r\n` and remove the last one because it is the position of `"""` + const arrStartPos: number[] = []; + start = startPos + offSet; + while (start < endPos) { + start = text.indexOf(splitOrIndentStr, start); + if (start < 0) { + break; + } + start += 2; + if (start < endPos) { + arrStartPos.push(start); + } + } + arrStartPos.pop(); + + //If minSpaceNumb is larger than endSpaceNumb, codefix will not be generated + const minSpaceNumb = Math.min(...arrIndents); + return { + startPos: arrStartPos, + indent: " ".repeat(endSpaceNumb - minSpaceNumb), + }; + } +} diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 1b7116a7a6..85c438bf89 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1,11 +1,9 @@ import { isArray, mutate } from "../utils/misc.js"; import { codePointBefore, isIdentifierContinue, trim } from "./charcode.js"; +import { createTripleQuoteIndentCodeFix } from "./compiler-code-fixes/triple-quote-indent.codefix.js"; import { compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; import { - Token, - TokenDisplay, - TokenFlags, createScanner, isComment, isKeyword, @@ -15,6 +13,9 @@ import { skipContinuousIdentifier, skipTrivia, skipTriviaBackward, + Token, + TokenDisplay, + TokenFlags, } from "./scanner.js"; import { AliasStatementNode, @@ -69,6 +70,7 @@ import { NeverKeywordNode, Node, NodeFlags, + NoTarget, NumericLiteralNode, ObjectLiteralNode, ObjectLiteralPropertyNode, @@ -3427,7 +3429,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (diagnostic.severity === "error") { parseErrorInNextFinishedNode = true; treePrintable = false; + + const code = "triple-quote-indent"; + if (diagnostic.target !== NoTarget && diagnostic.code === code) { + mutate(diagnostic).codefixes ??= []; + mutate(diagnostic.codefixes).push(createTripleQuoteIndentCodeFix(diagnostic.target)); + } } + parseDiagnostics.push(diagnostic); } diff --git a/packages/compiler/test/core/compiler-code-fixes/triple-quote-indent.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/triple-quote-indent.codefix.test.ts new file mode 100644 index 0000000000..1e12e2d8d4 --- /dev/null +++ b/packages/compiler/test/core/compiler-code-fixes/triple-quote-indent.codefix.test.ts @@ -0,0 +1,63 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { createTripleQuoteIndentCodeFix } from "../../../src/core/compiler-code-fixes/triple-quote-indent.codefix.js"; +import { SyntaxKind } from "../../../src/index.js"; +import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; + +describe("CodeFix: triple-quote-indent", () => { + it("case 1: each triple-quote is on a new line", async () => { + await expectCodeFixOnAst( + ` + const a = ┆"""\r\none\r\n two\r\n """; + `, + (node) => { + strictEqual(node.kind, SyntaxKind.StringLiteral); + return createTripleQuoteIndentCodeFix(node); + }, + ).toChangeTo(` + const a = """\r\n one\r\n two\r\n """; + `); + }); + + it("case 2: all triple-quote is on one line", async () => { + await expectCodeFixOnAst( + ` + const a = ┆""" one\r\n two """; + `, + (node) => { + strictEqual(node.kind, SyntaxKind.StringLiteral); + return createTripleQuoteIndentCodeFix(node); + }, + ).toChangeTo(` + const a = """\r\n one\r\n two \r\n"""; + `); + }); + + it("case 3: start triple-quote is not on a new line but end one is", async () => { + await expectCodeFixOnAst( + ` + const a = ┆"""one\r\n two\r\n """; + `, + (node) => { + strictEqual(node.kind, SyntaxKind.StringLiteral); + return createTripleQuoteIndentCodeFix(node); + }, + ).toChangeTo(` + const a = """\r\n one\r\n two\r\n """; + `); + }); + + it("case 4: end triple-quote is not on a new line but start one is", async () => { + await expectCodeFixOnAst( + ` + const a = ┆"""\r\n one\r\n two """; + `, + (node) => { + strictEqual(node.kind, SyntaxKind.StringLiteral); + return createTripleQuoteIndentCodeFix(node); + }, + ).toChangeTo(` + const a = """\r\n one\r\n two \r\n """; + `); + }); +}); From 98811abf9599fd4f45c330ad95b95d0c8354c5e4 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 31 Dec 2024 11:39:00 +0800 Subject: [PATCH 2/2] Initialize localization --- .../typespec-vscode/l10n/bundle.l10n.json | 4 ++ packages/typespec-vscode/package.json | 9 ++- packages/typespec-vscode/package.nls.json | 4 ++ .../src/code-action-provider.ts | 6 +- packages/typespec-vscode/src/extension.ts | 3 +- pnpm-lock.yaml | 64 +++++++++++++++++++ 6 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 packages/typespec-vscode/l10n/bundle.l10n.json create mode 100644 packages/typespec-vscode/package.nls.json diff --git a/packages/typespec-vscode/l10n/bundle.l10n.json b/packages/typespec-vscode/l10n/bundle.l10n.json new file mode 100644 index 0000000000..b6bd2c5970 --- /dev/null +++ b/packages/typespec-vscode/l10n/bundle.l10n.json @@ -0,0 +1,4 @@ +{ + "Launching TypeSpec language service...": "Launching TypeSpec language service...", + "See documentation for \"{0}\"": "See documentation for \"{0}\"" +} diff --git a/packages/typespec-vscode/package.json b/packages/typespec-vscode/package.json index 722b29702e..e729b210f2 100644 --- a/packages/typespec-vscode/package.json +++ b/packages/typespec-vscode/package.json @@ -25,6 +25,7 @@ ], "type": "module", "main": "./dist/src/extension.cjs", + "l10n": "./l10n", "browser": "./dist/src/web/extension.js", "engines": { "vscode": "^1.94.0" @@ -101,12 +102,12 @@ "commands": [ { "command": "typespec.restartServer", - "title": "Restart TypeSpec server", + "title": "%typespec.restartServer.title%", "category": "TypeSpec" }, { "command": "typespec.showOutputChannel", - "title": "Show Output Channel", + "title": "%typespec.showOutputChannel.title%", "category": "TypeSpec" } ], @@ -179,6 +180,7 @@ "@vitest/ui": "^2.1.2", "@vscode/test-web": "^0.0.62", "@vscode/vsce": "~3.1.1", + "@vscode/l10n-dev": "^0.0.18", "c8": "^10.1.2", "mocha": "^10.7.3", "rimraf": "~6.0.1", @@ -186,5 +188,8 @@ "typescript": "~5.6.3", "vitest": "^2.1.5", "vscode-languageclient": "~9.0.1" + }, + "dependencies": { + "@vscode/l10n": "^0.0.10" } } diff --git a/packages/typespec-vscode/package.nls.json b/packages/typespec-vscode/package.nls.json new file mode 100644 index 0000000000..014f2ba9e5 --- /dev/null +++ b/packages/typespec-vscode/package.nls.json @@ -0,0 +1,4 @@ +{ + "typespec.restartServer.title": "Restart TypeSpec server", + "typespec.showOutputChannel.title": "Show Output Channel" +} diff --git a/packages/typespec-vscode/src/code-action-provider.ts b/packages/typespec-vscode/src/code-action-provider.ts index 803242b392..38dddb3e89 100644 --- a/packages/typespec-vscode/src/code-action-provider.ts +++ b/packages/typespec-vscode/src/code-action-provider.ts @@ -56,10 +56,8 @@ export class TypeSpecCodeActionProvider implements vscode.CodeActionProvider { codeActionTitle: string, ): vscode.CodeAction { // 'vscode.CodeActionKind.Empty' does not generate a Code Action menu, You must use 'vscode.CodeActionKind.QuickFix' - const action = new vscode.CodeAction( - `See documentation for "${codeActionTitle}"`, - vscode.CodeActionKind.QuickFix, - ); + const codefixTitle = vscode.l10n.t('See documentation for "{0}"', codeActionTitle); + const action = new vscode.CodeAction(codefixTitle, vscode.CodeActionKind.QuickFix); action.command = { command: OPEN_URL_COMMAND, title: diagnostic.message, diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts index 7fe2a5f468..143c9e268d 100644 --- a/packages/typespec-vscode/src/extension.ts +++ b/packages/typespec-vscode/src/extension.ts @@ -48,9 +48,10 @@ export async function activate(context: ExtensionContext) { }), ); + const tipToolTitle = vscode.l10n.t("Launching TypeSpec language service..."); return await vscode.window.withProgress( { - title: "Launching TypeSpec language service...", + title: tipToolTitle, location: vscode.ProgressLocation.Notification, }, async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07ece0d799..81687b6f98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1866,6 +1866,10 @@ importers: version: link:../typespec-vscode packages/typespec-vscode: + dependencies: + '@vscode/l10n': + specifier: ^0.0.10 + version: 0.0.10 devDependencies: '@rollup/plugin-commonjs': specifier: ~28.0.0 @@ -1897,6 +1901,9 @@ importers: '@vitest/ui': specifier: ^2.1.2 version: 2.1.5(vitest@2.1.5) + '@vscode/l10n-dev': + specifier: ^0.0.18 + version: 0.0.18 '@vscode/test-web': specifier: ^0.0.62 version: 0.0.62 @@ -5795,6 +5802,13 @@ packages: '@vscode/emmet-helper@2.11.0': resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + '@vscode/l10n-dev@0.0.18': + resolution: {integrity: sha512-pEKLMnlg7hlxFrZLqcyJe08olmj6KVs2Rof7MVB5rN0D6NOKPBRtkQ176TuMUmW863EDV5WQUgNzOGa2nHBSSQ==} + hasBin: true + + '@vscode/l10n@0.0.10': + resolution: {integrity: sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ==} + '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} @@ -7021,6 +7035,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-json@1.5.0: + resolution: {integrity: sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==} + engines: {node: '>=4.0.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -7764,6 +7782,10 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stdin@7.0.0: + resolution: {integrity: sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==} + engines: {node: '>=8'} + get-stdin@9.0.0: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} engines: {node: '>=12'} @@ -9817,6 +9839,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pseudo-localization@2.4.0: + resolution: {integrity: sha512-ISYMOKY8+f+PmiXMFw2y6KLY74LBrv/8ml/VjjoVEV2k+MS+OJZz7ydciK5ntJwxPrKQPTU1+oXq9Mx2b0zEzg==} + hasBin: true + psl@1.10.0: resolution: {integrity: sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==} @@ -10907,6 +10933,11 @@ packages: typescript: optional: true + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} @@ -11514,6 +11545,10 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml2js@0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -16510,6 +16545,17 @@ snapshots: vscode-languageserver-types: 3.17.5 vscode-uri: 3.0.8 + '@vscode/l10n-dev@0.0.18': + dependencies: + deepmerge-json: 1.5.0 + glob: 8.1.0 + pseudo-localization: 2.4.0 + typescript: 4.9.5 + xml2js: 0.4.23 + yargs: 17.7.2 + + '@vscode/l10n@0.0.10': {} + '@vscode/l10n@0.0.18': {} '@vscode/test-web@0.0.62': @@ -18038,6 +18084,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-json@1.5.0: {} + deepmerge@4.3.1: {} defaults@1.0.4: @@ -18984,6 +19032,8 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + get-stdin@7.0.0: {} + get-stdin@9.0.0: {} get-stream@6.0.1: {} @@ -21588,6 +21638,13 @@ snapshots: proxy-from-env@1.1.0: {} + pseudo-localization@2.4.0: + dependencies: + flat: 5.0.2 + get-stdin: 7.0.0 + typescript: 4.9.5 + yargs: 17.7.2 + psl@1.10.0: dependencies: punycode: 2.3.1 @@ -22935,6 +22992,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@4.9.5: {} + typescript@5.4.2: {} typescript@5.6.3: {} @@ -23549,6 +23608,11 @@ snapshots: xml-name-validator@4.0.0: {} + xml2js@0.4.23: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + xml2js@0.5.0: dependencies: sax: 1.4.1