diff --git a/src/basic-languages/monaco.contribution.ts b/src/basic-languages/monaco.contribution.ts index b6c07a545a..d623774265 100644 --- a/src/basic-languages/monaco.contribution.ts +++ b/src/basic-languages/monaco.contribution.ts @@ -79,6 +79,7 @@ import './systemverilog/systemverilog.contribution'; import './tcl/tcl.contribution'; import './twig/twig.contribution'; import './typescript/typescript.contribution'; +import './typespec/typespec.contribution'; import './vb/vb.contribution'; import './wgsl/wgsl.contribution'; import './xml/xml.contribution'; diff --git a/src/basic-languages/typespec/typespec.contribution.ts b/src/basic-languages/typespec/typespec.contribution.ts new file mode 100644 index 0000000000..5f81e2dd05 --- /dev/null +++ b/src/basic-languages/typespec/typespec.contribution.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerLanguage } from '../_.contribution'; + +declare var AMD: any; +declare var require: any; + +registerLanguage({ + id: 'typespec', + extensions: ['.tsp'], + aliases: ['TypeSpec'], + loader: () => { + if (AMD) { + return new Promise((resolve, reject) => { + require(['vs/basic-languages/typespec/typespec'], resolve, reject); + }); + } else { + return import('./typespec'); + } + } +}); diff --git a/src/basic-languages/typespec/typespec.test.ts b/src/basic-languages/typespec/typespec.test.ts new file mode 100644 index 0000000000..470e8e321d --- /dev/null +++ b/src/basic-languages/typespec/typespec.test.ts @@ -0,0 +1,500 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { testTokenization } from '../test/testRunner'; + +// Those test were auto generated from the test in the https://github.com/microsoft/typespec repo +// to keep in sync you can follow the instruction in https://github.com/microsoft/typespec/blob/main/packages/monarch/README.md + +testTokenization('typespec', [ + [ + { + line: 'import "@typespec/http";', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 6, + type: '' + }, + { + startIndex: 7, + type: 'string.tsp' + }, + { + startIndex: 23, + type: '' + } + ] + } + ], + [ + { + line: 'using TypeSpec.Http', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 14, + type: '' + }, + { + startIndex: 15, + type: 'identifier.tsp' + } + ] + } + ], + [ + { + line: 'namespace Foo {}', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'identifier.tsp' + }, + { + startIndex: 13, + type: '' + } + ] + } + ], + [ + { + line: 'namespace Foo {', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'identifier.tsp' + }, + { + startIndex: 13, + type: '' + } + ] + }, + { + line: ' model Bar {}', + tokens: [ + { + startIndex: 0, + type: '' + }, + { + startIndex: 4, + type: 'keyword.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'identifier.tsp' + }, + { + startIndex: 13, + type: '' + } + ] + }, + { + line: ' }', + tokens: [ + { + startIndex: 0, + type: '' + } + ] + } + ], + [ + { + line: 'model Foo {}', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 9, + type: '' + } + ] + } + ], + [ + { + line: 'model Foo is Bar;', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'keyword.tsp' + }, + { + startIndex: 12, + type: '' + }, + { + startIndex: 13, + type: 'identifier.tsp' + }, + { + startIndex: 16, + type: '' + } + ] + } + ], + [ + { + line: 'model Foo extends Bar;', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'keyword.tsp' + }, + { + startIndex: 17, + type: '' + }, + { + startIndex: 18, + type: 'identifier.tsp' + }, + { + startIndex: 21, + type: '' + } + ] + } + ], + [ + { + line: 'interface Foo {}', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 10, + type: 'identifier.tsp' + }, + { + startIndex: 13, + type: '' + } + ] + } + ], + [ + { + line: 'union Foo {}', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 9, + type: '' + } + ] + } + ], + [ + { + line: 'scalar foo extends string;', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 6, + type: '' + }, + { + startIndex: 7, + type: 'identifier.tsp' + }, + { + startIndex: 10, + type: '' + }, + { + startIndex: 11, + type: 'keyword.tsp' + }, + { + startIndex: 18, + type: '' + }, + { + startIndex: 19, + type: 'identifier.tsp' + }, + { + startIndex: 25, + type: '' + } + ] + } + ], + [ + { + line: 'op test(): void;', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 2, + type: '' + }, + { + startIndex: 3, + type: 'identifier.tsp' + }, + { + startIndex: 7, + type: '' + }, + { + startIndex: 11, + type: 'keyword.tsp' + }, + { + startIndex: 15, + type: '' + } + ] + } + ], + [ + { + line: 'enum Direction { up, down }', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 4, + type: '' + }, + { + startIndex: 5, + type: 'identifier.tsp' + }, + { + startIndex: 14, + type: '' + }, + { + startIndex: 17, + type: 'identifier.tsp' + }, + { + startIndex: 19, + type: '' + }, + { + startIndex: 21, + type: 'identifier.tsp' + }, + { + startIndex: 25, + type: '' + } + ] + } + ], + [ + { + line: 'alias Foo = "a" | "b";', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 9, + type: '' + }, + { + startIndex: 12, + type: 'string.tsp' + }, + { + startIndex: 15, + type: '' + }, + { + startIndex: 18, + type: 'string.tsp' + }, + { + startIndex: 21, + type: '' + } + ] + } + ], + [ + { + line: 'alias T = """', + tokens: [ + { + startIndex: 0, + type: 'keyword.tsp' + }, + { + startIndex: 5, + type: '' + }, + { + startIndex: 6, + type: 'identifier.tsp' + }, + { + startIndex: 7, + type: '' + }, + { + startIndex: 11, + type: 'string.tsp' + } + ] + }, + { + line: ' this', + tokens: [ + { + startIndex: 0, + type: 'string.tsp' + } + ] + }, + { + line: ' is', + tokens: [ + { + startIndex: 0, + type: 'string.tsp' + } + ] + }, + { + line: ' multiline', + tokens: [ + { + startIndex: 0, + type: 'string.tsp' + } + ] + }, + { + line: ' """', + tokens: [ + { + startIndex: 0, + type: 'string.tsp' + } + ] + } + ] +]); diff --git a/src/basic-languages/typespec/typespec.ts b/src/basic-languages/typespec/typespec.ts new file mode 100644 index 0000000000..d069e11ce3 --- /dev/null +++ b/src/basic-languages/typespec/typespec.ts @@ -0,0 +1,130 @@ +import type { languages } from '../../fillers/monaco-editor-core'; + +const bounded = (text: string) => `\\b${text}\\b`; +const notBefore = (regex: string) => `(?!${regex})`; + +const identifierStart = '[_a-zA-Z]'; +const identifierContinue = '[_a-zA-Z0-9]'; +const identifier = bounded(`${identifierStart}${identifierContinue}*`); +const directive = bounded(`[_a-zA-Z-0-9]+`); + +const keywords = [ + 'import', + 'model', + 'scalar', + 'namespace', + 'op', + 'interface', + 'union', + 'using', + 'is', + 'extends', + 'enum', + 'alias', + 'return', + 'void', + 'if', + 'else', + 'projection', + 'dec', + 'extern', + 'fn' +]; +const namedLiterals = ['true', 'false', 'null', 'unknown', 'never']; +const nonCommentWs = `[ \\t\\r\\n]`; +const numericLiteral = `[0-9]+`; + +export const conf: languages.LanguageConfiguration = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: '/**', close: ' */', notIn: ['string'] } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' } + ], + indentationRules: { + decreaseIndentPattern: new RegExp('^((?!.*?/\\*).*\\*/)?\\s*[\\}\\]].*$'), + increaseIndentPattern: new RegExp( + '^((?!//).)*(\\{([^}"\'`/]*|(\\t|[ ])*//.*)|\\([^)"\'`/]*|\\[[^\\]"\'`/]*)$' + ), + // e.g. * ...| or */| or *-----*/| + unIndentedLinePattern: new RegExp( + '^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$' + ) + } +}; + +export const language: languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '.tsp', + brackets: [ + { open: '{', close: '}', token: 'delimiter.curly' }, + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' } + ], + symbols: /[=:;<>]+/, + keywords, + namedLiterals, + escapes: `\\\\(u{[0-9A-Fa-f]+}|n|r|t|\\\\|"|\\\${)`, + tokenizer: { + root: [{ include: '@expression' }, { include: '@whitespace' }], + stringVerbatim: [ + { regex: `(|"|"")[^"]`, action: { token: 'string' } }, + { regex: `"""${notBefore(`"`)}`, action: { token: 'string', next: '@pop' } } + ], + stringLiteral: [ + { regex: `\\\${`, action: { token: 'delimiter.bracket', next: '@bracketCounting' } }, + { regex: `[^\\\\"$]+`, action: { token: 'string' } }, + { regex: '@escapes', action: { token: 'string.escape' } }, + { regex: `\\\\.`, action: { token: 'string.escape.invalid' } }, + { regex: `"`, action: { token: 'string', next: '@pop' } } + ], + bracketCounting: [ + { regex: `{`, action: { token: 'delimiter.bracket', next: '@bracketCounting' } }, + { regex: `}`, action: { token: 'delimiter.bracket', next: '@pop' } }, + { include: '@expression' } + ], + comment: [ + { regex: `[^\\*]+`, action: { token: 'comment' } }, + { regex: `\\*\\/`, action: { token: 'comment', next: '@pop' } }, + { regex: `[\\/*]`, action: { token: 'comment' } } + ], + whitespace: [ + { regex: nonCommentWs }, + { regex: `\\/\\*`, action: { token: 'comment', next: '@comment' } }, + { regex: `\\/\\/.*$`, action: { token: 'comment' } } + ], + expression: [ + { regex: `"""`, action: { token: 'string', next: '@stringVerbatim' } }, + { regex: `"${notBefore(`""`)}`, action: { token: 'string', next: '@stringLiteral' } }, + { regex: numericLiteral, action: { token: 'number' } }, + { + regex: identifier, + action: { + cases: { + '@keywords': { token: 'keyword' }, + '@namedLiterals': { token: 'keyword' }, + '@default': { token: 'identifier' } + } + } + }, + { regex: `@${identifier}`, action: { token: 'tag' } }, + { regex: `#${directive}`, action: { token: 'directive' } } + ] + } +}; diff --git a/website/src/website/data/home-samples/sample.typespec.txt b/website/src/website/data/home-samples/sample.typespec.txt new file mode 100644 index 0000000000..4376773a03 --- /dev/null +++ b/website/src/website/data/home-samples/sample.typespec.txt @@ -0,0 +1,71 @@ +import "@typespec/rest"; +import "@typespec/openapi"; +import "./decorators.js"; + +using TypeSpec.Http; + +@service({ + title: "Pet Store Service", +}) +/** This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. */ +namespace PetStore; + +// Model types +model Pet { + name: string; + tag?: string; + + @minValue(0) + @maxValue(20) + age: int32; +} + +model Toy { + id: int64; + petId: int64; + name: string; +} + +/** Error */ +@error +model Error { + code: int32; + message: string; +} + +/** Not modified */ +model NotModified { + @statusCode _: 304; + @body body: Body; +} + +@friendlyName("{name}ListResults", Item) +model ResponsePage { + items: Item[]; + nextLink?: string; +} + +model PetId { + @path petId: int32; +} + +/** Manage your pets. */ +@route("/pets") +namespace Pets { + /** Delete a pet. */ + @delete + op delete(...PetId): OkResponse | Error; + + @fancyDoc("List pets.") + op list(@query nextLink?: string): ResponsePage | Error; + + /** Returns a pet. Supports eTags. */ + op read(...PetId): Pet | (NotModifiedResponse & Pet) | Error; + + @post op create(@body pet: Pet): Pet | Error; +} + +@route("/pets/{petId}/toys") +namespace ListPetToysResponse { + op list(@path petId: string, @query nameFilter: string): ResponsePage | Error; +}