diff --git a/.chronus/changes/fix-playground-styles-overflow-2024-5-6-19-45-49.md b/.chronus/changes/fix-playground-styles-overflow-2024-5-6-19-45-49.md new file mode 100644 index 0000000000..0fb6b39703 --- /dev/null +++ b/.chronus/changes/fix-playground-styles-overflow-2024-5-6-19-45-49.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/playground" +--- + +Fix issue where hover tooltip would be cropped or not visible diff --git a/.chronus/changes/fix-swagger-ui-missing-2024-5-10-15-15-0.md b/.chronus/changes/fix-swagger-ui-missing-2024-5-10-15-15-0.md new file mode 100644 index 0000000000..0eca1a03ff --- /dev/null +++ b/.chronus/changes/fix-swagger-ui-missing-2024-5-10-15-15-0.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/playground" +--- + +Fix swagger UI missing diff --git a/.chronus/changes/more-logging-vscode-2024-5-6-16-57-48.md b/.chronus/changes/more-logging-vscode-2024-5-6-16-57-48.md new file mode 100644 index 0000000000..4098e34757 --- /dev/null +++ b/.chronus/changes/more-logging-vscode-2024-5-6-16-57-48.md @@ -0,0 +1,10 @@ +--- +changeKind: feature +packages: + - typespec-vscode +--- + +Enhance logging and trace + 1. Support "Developer: Set Log Level..." command to filter logs in TypeSpec output channel + 2. Add "typespecLanguageServer.trace.server" config for whether and how to send the traces from TypeSpec language server to client. (It still depends on client to decide whether to show these traces based on the configured Log Level.) + 3. More logs and traces are added for diagnostic and troubleshooting diff --git a/.chronus/changes/more-logging-vscode-2024-5-7-12-41-0.md b/.chronus/changes/more-logging-vscode-2024-5-7-12-41-0.md new file mode 100644 index 0000000000..35f044cca0 --- /dev/null +++ b/.chronus/changes/more-logging-vscode-2024-5-7-12-41-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +More logs and traces are added for diagnostic and troubleshooting in TypeSpec language server diff --git a/.chronus/changes/versioning-fixBug-2024-5-5-20-59-9.md b/.chronus/changes/versioning-fixBug-2024-5-5-20-59-9.md new file mode 100644 index 0000000000..447dd7970a --- /dev/null +++ b/.chronus/changes/versioning-fixBug-2024-5-5-20-59-9.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/versioning" +--- + +Fix regression in last Versioning PR. diff --git a/.vscode/launch.json b/.vscode/launch.json index a65d4eafe3..802349582d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -108,6 +108,7 @@ "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/typespec-vscode"], + "outFiles": ["${workspaceFolder}/packages/typespec-vscode/dist/**/*.cjs"], "env": { // Log elapsed time for each call to server. //"TYPESPEC_SERVER_LOG_TIMING": "true", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d67393db61..d0b3ffdda8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -374,8 +374,8 @@ Misc labels ### Updating labels -Labels are configured in `eng/common/labels.yaml`. To update labels, edit this file and run `pnpm sync-labels`. -**If you create a new label in github UI without updating the `labels.yaml` file, it WILL be automatically removed** +Labels are configured in `eng/common/config/labels.ts`. To update labels, edit this file and run `pnpm sync-labels`. +**If you create a new label in github UI without updating the `labels.ts` file, it WILL be automatically removed** # TypeSpec Emitters diff --git a/packages/compiler/src/server/compile-service.ts b/packages/compiler/src/server/compile-service.ts index fa793b8c3c..a31b4832f6 100644 --- a/packages/compiler/src/server/compile-service.ts +++ b/packages/compiler/src/server/compile-service.ts @@ -24,7 +24,7 @@ import { doIO, loadFile, resolveTspMain } from "../utils/misc.js"; import { serverOptions } from "./constants.js"; import { FileService } from "./file-service.js"; import { FileSystemCache } from "./file-system-cache.js"; -import { CompileResult, ServerHost } from "./types.js"; +import { CompileResult, ServerHost, ServerLog } from "./types.js"; import { UpdateManger } from "./update-manager.js"; /** @@ -63,7 +63,7 @@ export interface CompileServiceOptions { readonly fileService: FileService; readonly serverHost: ServerHost; readonly compilerHost: CompilerHost; - readonly log: (message: string, details?: unknown) => void; + readonly log: (log: ServerLog) => void; } export function createCompileService({ @@ -100,12 +100,14 @@ export function createCompileService({ const path = await fileService.getPath(document); const mainFile = await getMainFileForDocument(path); const config = await getConfig(mainFile); + log({ level: "debug", message: `config resolved`, detail: config }); const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: path }); const options: CompilerOptions = { ...optionsFromConfig, ...serverOptions, }; + log({ level: "debug", message: `compiler options resolved`, detail: options }); if (!fileService.upToDate(document)) { return undefined; @@ -122,6 +124,10 @@ export function createCompileService({ if (mainFile !== path && !program.sourceFiles.has(path)) { // If the file that changed wasn't imported by anything from the main // file, retry using the file itself as the main file. + log({ + level: "debug", + message: `target file was not included in compiling, try to compile ${path} as main file directly`, + }); program = await compileProgram(compilerHost, path, options, oldPrograms.get(path)); oldPrograms.set(path, program); } @@ -166,6 +172,10 @@ export function createCompileService({ const lookupDir = entrypointStat.isDirectory() ? mainFile : getDirectoryPath(mainFile); const configPath = await findTypeSpecConfigPath(compilerHost, lookupDir, true); if (!configPath) { + log({ + level: "debug", + message: `can't find path with config file, try to use default config`, + }); return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) }; } @@ -210,6 +220,7 @@ export function createCompileService({ */ async function getMainFileForDocument(path: string) { if (path.startsWith("untitled:")) { + log({ level: "debug", message: `untitled document treated as its own main file: ${path}` }); return path; } @@ -237,6 +248,10 @@ export function createCompileService({ const tspMain = resolveTspMain(pkg); if (typeof tspMain === "string") { + log({ + level: "debug", + message: `tspMain resolved from package.json (${pkgPath}) as ${tspMain}`, + }); mainFile = tspMain; } @@ -249,6 +264,7 @@ export function createCompileService({ ); if (stat?.isFile()) { + log({ level: "debug", message: `main file found as ${candidate}` }); return candidate; } @@ -256,16 +272,22 @@ export function createCompileService({ if (parentDir === dir) { break; } + log({ + level: "debug", + message: `main file not found in ${dir}, search in parent directory ${parentDir}`, + }); dir = parentDir; } + log({ level: "debug", message: `reached directory root, using ${path} as main file` }); return path; function logMainFileSearchDiagnostic(diagnostic: TypeSpecDiagnostic) { - log( - `Unexpected diagnostic while looking for main file of ${path}`, - formatDiagnostic(diagnostic) - ); + log({ + level: `error`, + message: `Unexpected diagnostic while looking for main file of ${path}`, + detail: formatDiagnostic(diagnostic), + }); } } } diff --git a/packages/compiler/src/server/file-system-cache.ts b/packages/compiler/src/server/file-system-cache.ts index c95132bdfd..87818e2abb 100644 --- a/packages/compiler/src/server/file-system-cache.ts +++ b/packages/compiler/src/server/file-system-cache.ts @@ -1,6 +1,7 @@ import { FileEvent } from "vscode-languageserver"; import { SourceFile } from "../core/types.js"; import { FileService } from "./file-service.js"; +import { ServerLog } from "./types.js"; export interface FileSystemCache { get(path: string): Promise; @@ -27,8 +28,10 @@ export interface CachedError { } export function createFileSystemCache({ fileService, + log, }: { fileService: FileService; + log: (log: ServerLog) => void; }): FileSystemCache { const cache = new Map(); let changes: FileEvent[] = []; @@ -36,10 +39,21 @@ export function createFileSystemCache({ async get(path: string) { for (const change of changes) { const path = await fileService.fileURLToRealPath(change.uri); + log({ + level: "trace", + message: `FileSystemCache entry with key '${path}' removed`, + }); cache.delete(path); } changes = []; - return cache.get(path); + const r = cache.get(path); + if (!r) { + const target: any = {}; + Error.captureStackTrace(target); + const callstack = target.stack.substring("Error\n".length); + log({ level: "trace", message: `FileSystemCache miss for ${path}`, detail: callstack }); + } + return r; }, set(path: string, entry: CachedFile | CachedError) { cache.set(path, entry); diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts index 0c88394529..48d88a1567 100644 --- a/packages/compiler/src/server/server.ts +++ b/packages/compiler/src/server/server.ts @@ -15,7 +15,7 @@ import { import { NodeHost } from "../core/node-host.js"; import { typespecVersion } from "../utils/misc.js"; import { createServer } from "./serverlib.js"; -import { Server, ServerHost } from "./types.js"; +import { Server, ServerHost, ServerLog } from "./types.js"; let server: Server | undefined = undefined; @@ -45,8 +45,38 @@ function main() { sendDiagnostics(params: PublishDiagnosticsParams) { void connection.sendDiagnostics(params); }, - log(message: string) { - connection.console.log(message); + log(log: ServerLog) { + const message = log.message; + let detail: string | undefined = undefined; + let fullMessage = message; + if (log.detail) { + detail = + typeof log.detail === "string" ? log.detail : JSON.stringify(log.detail, undefined, 2); + fullMessage = `${message}:\n${detail}`; + } + + switch (log.level) { + case "trace": + connection.tracer.log(message, detail); + break; + case "debug": + connection.console.debug(fullMessage); + break; + case "info": + connection.console.info(fullMessage); + break; + case "warning": + connection.console.warn(fullMessage); + break; + case "error": + connection.console.error(fullMessage); + break; + default: + connection.console.error( + `Log Message with invalid LogLevel (${log.level}). Raw Message: ${fullMessage}` + ); + break; + } }, getOpenDocumentByURL(url: string) { return documents.get(url); @@ -58,13 +88,13 @@ function main() { const s = createServer(host); server = s; - s.log(`TypeSpec language server v${typespecVersion}`); - s.log("Module", fileURLToPath(import.meta.url)); - s.log("Process ID", process.pid); - s.log("Command Line", process.argv); + s.log({ level: `info`, message: `TypeSpec language server v${typespecVersion}` }); + s.log({ level: `info`, message: `Module: ${fileURLToPath(import.meta.url)}` }); + s.log({ level: `info`, message: `Process ID: ${process.pid}` }); + s.log({ level: `info`, message: `Command Line`, detail: process.argv }); if (profileDir) { - s.log("CPU profiling enabled", profileDir); + s.log({ level: `info`, message: `CPU profiling enabled with dir: ${profileDir}` }); profileSession = new inspector.Session(); profileSession.connect(); } @@ -152,7 +182,7 @@ function time any>(func: T): T { const start = Date.now(); const ret = await func.apply(undefined!, args); const end = Date.now(); - server!.log(func.name, end - start + " ms"); + server!.log({ level: `trace`, message: `${func.name}: ${end - start + " ms"}` }); return ret; }) as T; } diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index f6a340b5bf..7ea1a69349 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -92,6 +92,7 @@ import { SemanticTokenKind, Server, ServerHost, + ServerLog, ServerSourceFile, ServerWorkspaceFolder, } from "./types.js"; @@ -105,6 +106,7 @@ export function createServer(host: ServerHost): Server { // a file change. const fileSystemCache = createFileSystemCache({ fileService, + log, }); const compilerHost = createCompilerHost(); @@ -121,7 +123,7 @@ export function createServer(host: ServerHost): Server { let workspaceFolders: ServerWorkspaceFolder[] = []; let isInitialized = false; - let pendingMessages: string[] = []; + let pendingMessages: ServerLog[] = []; return { get pendingMessages() { @@ -236,17 +238,17 @@ export function createServer(host: ServerHost): Server { ]; } - log("Workspace Folders", workspaceFolders); + log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders }); return { capabilities }; } function initialized(params: InitializedParams): void { isInitialized = true; - log("Initialization complete."); + log({ level: "info", message: "Initialization complete." }); } async function workspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) { - log("Workspace Folders Changed", e); + log({ level: "info", message: "Workspace Folders Changed", detail: e }); const map = new Map(workspaceFolders.map((f) => [f.uri, f])); for (const folder of e.removed) { map.delete(folder.uri); @@ -258,7 +260,7 @@ export function createServer(host: ServerHost): Server { }); } workspaceFolders = Array.from(map.values()); - log("Workspace Folders", workspaceFolders); + log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders }); } function watchedFilesChanged(params: DidChangeWatchedFilesParams) { @@ -383,6 +385,7 @@ export function createServer(host: ServerHost): Server { // we report diagnostics with no location on the document that changed to // trigger. diagDocument = document; + log({ level: "debug", message: `Diagnostic with no location: ${each.message}` }); } if (!diagDocument || !fileService.upToDate(diagDocument)) { @@ -883,14 +886,9 @@ export function createServer(host: ServerHost): Server { } } - function log(message: string, details: any = undefined) { - message = `[${new Date().toLocaleTimeString()}] ${message}`; - if (details) { - message += ": " + JSON.stringify(details, undefined, 2); - } - + function log(log: ServerLog) { if (!isInitialized) { - pendingMessages.push(message); + pendingMessages.push(log); return; } @@ -899,7 +897,7 @@ export function createServer(host: ServerHost): Server { } pendingMessages = []; - host.log(message); + host.log(log); } function sendDiagnostics(document: TextDocument, diagnostics: VSDiagnostic[]) { diff --git a/packages/compiler/src/server/types.ts b/packages/compiler/src/server/types.ts index cd3ae32f9c..d769df6560 100644 --- a/packages/compiler/src/server/types.ts +++ b/packages/compiler/src/server/types.ts @@ -39,12 +39,19 @@ import { import { TextDocument, TextEdit } from "vscode-languageserver-textdocument"; import type { CompilerHost, Program, SourceFile, TypeSpecScriptNode } from "../core/index.js"; +export type ServerLogLevel = "trace" | "debug" | "info" | "warning" | "error"; +export interface ServerLog { + level: ServerLogLevel; + message: string; + detail?: unknown; +} + export interface ServerHost { readonly compilerHost: CompilerHost; readonly throwInternalErrors?: boolean; readonly getOpenDocumentByURL: (url: string) => TextDocument | undefined; readonly sendDiagnostics: (params: PublishDiagnosticsParams) => void; - readonly log: (message: string) => void; + readonly log: (log: ServerLog) => void; readonly applyEdit: ( paramOrEdit: ApplyWorkspaceEditParams | WorkspaceEdit ) => Promise; @@ -57,7 +64,7 @@ export interface CompileResult { } export interface Server { - readonly pendingMessages: readonly string[]; + readonly pendingMessages: readonly ServerLog[]; readonly workspaceFolders: readonly ServerWorkspaceFolder[]; compile(document: TextDocument | TextDocumentIdentifier): Promise; initialize(params: InitializeParams): Promise; @@ -81,7 +88,7 @@ export interface Server { documentClosed(change: TextDocumentChangeEvent): void; getCodeActions(params: CodeActionParams): Promise; executeCommand(params: ExecuteCommandParams): Promise; - log(message: string, details?: any): void; + log(log: ServerLog): void; } export interface ServerSourceFile extends SourceFile { diff --git a/packages/compiler/src/testing/test-server-host.ts b/packages/compiler/src/testing/test-server-host.ts index d82fe9b698..d11e4d7cea 100644 --- a/packages/compiler/src/testing/test-server-host.ts +++ b/packages/compiler/src/testing/test-server-host.ts @@ -68,8 +68,8 @@ export async function createTestServerHost(options?: TestHostOptions & { workspa } diagnostics.set(params.uri, params.diagnostics); }, - log(message) { - logMessages.push(message); + log(log) { + logMessages.push(`[${log.level}] ${log.message}`); }, getURL(path: string) { if (path.startsWith("untitled:")) { diff --git a/packages/http-client-csharp/emitter/src/lib/converter.ts b/packages/http-client-csharp/emitter/src/lib/converter.ts index 6f332854ae..46a254d99f 100644 --- a/packages/http-client-csharp/emitter/src/lib/converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/converter.ts @@ -273,20 +273,18 @@ function fromUnionType( models: Map, enums: Map ): InputUnionType | InputType { - const itemTypes: InputType[] = []; + const variantTypes: InputType[] = []; for (const value of union.values) { - const inputType = fromSdkType(value, context, models, enums); - itemTypes.push(inputType); + const variantType = fromSdkType(value, context, models, enums); + variantTypes.push(variantType); } - return itemTypes.length > 1 - ? { - Kind: InputTypeKind.Union, - Name: InputTypeKind.Union, - UnionItemTypes: itemTypes, - IsNullable: false, - } - : itemTypes[0]; + return { + Kind: "union", + Name: union.name, + VariantTypes: variantTypes, + IsNullable: false, + }; } function fromSdkConstantType( diff --git a/packages/http-client-csharp/emitter/src/lib/operation.ts b/packages/http-client-csharp/emitter/src/lib/operation.ts index 71fec0cb5a..37ad8229bd 100644 --- a/packages/http-client-csharp/emitter/src/lib/operation.ts +++ b/packages/http-client-csharp/emitter/src/lib/operation.ts @@ -132,7 +132,7 @@ export function loadOperation( if (isInputLiteralType(contentTypeParameter.Type)) { mediaTypes.push(contentTypeParameter.DefaultValue?.Value); } else if (isInputUnionType(contentTypeParameter.Type)) { - for (const unionItem of contentTypeParameter.Type.UnionItemTypes) { + for (const unionItem of contentTypeParameter.Type.VariantTypes) { if (isInputLiteralType(unionItem)) { mediaTypes.push(unionItem.Value as string); } else { diff --git a/packages/http-client-csharp/emitter/src/type/input-type-kind.ts b/packages/http-client-csharp/emitter/src/type/input-type-kind.ts index 0b2c7fe46e..6386a2087b 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type-kind.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type-kind.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. See License.txt in the project root for license information. export enum InputTypeKind { - Union = "Union", Model = "Model", Array = "Array", Dictionary = "Dictionary", diff --git a/packages/http-client-csharp/emitter/src/type/input-type.ts b/packages/http-client-csharp/emitter/src/type/input-type.ts index 9a9ec55c6c..e7a82939d7 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type.ts @@ -61,13 +61,13 @@ export interface InputDurationType extends InputTypeBase { } export interface InputUnionType extends InputTypeBase { - Kind: InputTypeKind.Union; // TODO -- will change to TCGC value in future refactor - Name: InputTypeKind.Union; // union type does not really have a name right now, we just use its kind - UnionItemTypes: InputType[]; + Kind: "union"; + Name: string; + VariantTypes: InputType[]; } export function isInputUnionType(type: InputType): type is InputUnionType { - return type.Kind === InputTypeKind.Union; + return type.Kind === "union"; } export interface InputModelType extends InputTypeBase { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs index d7ac8637a6..b2537161bb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs @@ -21,7 +21,7 @@ internal class ScmTypeFactory : TypeFactory public override CSharpType CreateCSharpType(InputType inputType) => inputType switch { InputLiteralType literalType => CSharpType.FromLiteral(CreateCSharpType(literalType.ValueType), literalType.Value), - InputUnionType unionType => CSharpType.FromUnion(unionType.UnionItemTypes.Select(CreateCSharpType).ToArray(), unionType.IsNullable), + InputUnionType unionType => CSharpType.FromUnion(unionType.VariantTypes.Select(CreateCSharpType).ToArray(), unionType.IsNullable), InputListType { IsEmbeddingsVector: true } listType => new CSharpType(typeof(ReadOnlyMemory<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), InputListType listType => new CSharpType(typeof(IList<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), InputDictionaryType dictionaryType => new CSharpType(typeof(IDictionary<,>), inputType.IsNullable, typeof(string), CreateCSharpType(dictionaryType.ValueType)), diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs index fae66c329a..129e119c34 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs @@ -63,7 +63,7 @@ private static InputParameterExample BuildParameterExample(InputParameter parame // when it is constant, it could have DefaultValue value = InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); } - else if (parameter.Type is InputUnionType unionType && unionType.UnionItemTypes.First() is InputLiteralType literalType) + else if (parameter.Type is InputUnionType unionType && unionType.VariantTypes[0] is InputLiteralType literalType) { // or it could be a union of literal types value = InputExampleValue.Value(parameter.Type, literalType.Value); @@ -106,7 +106,7 @@ private static InputParameterExample BuildParameterExample(InputParameter parame InputPrimitiveType primitiveType => BuildPrimitiveExampleValue(primitiveType, hint), InputLiteralType literalType => InputExampleValue.Value(literalType, literalType.Value), InputModelType modelType => BuildModelExampleValue(modelType, useAllParameters, visitedModels), - InputUnionType unionType => BuildExampleValue(unionType.UnionItemTypes.First(), hint, useAllParameters, visitedModels), + InputUnionType unionType => BuildExampleValue(unionType.VariantTypes[0], hint, useAllParameters, visitedModels), InputDateTimeType dateTimeType => BuildDateTimeExampleValue(dateTimeType), InputDurationType durationType => BuildDurationExampleValue(durationType), _ => InputExampleValue.Object(type, new Dictionary()) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs index 4f27f894c5..916e54ec92 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs @@ -7,11 +7,11 @@ namespace Microsoft.Generator.CSharp.Input { public class InputUnionType : InputType { - public InputUnionType(string name, IReadOnlyList unionItemTypes, bool isNullable) : base(name, isNullable) + public InputUnionType(string name, IReadOnlyList variantTypes, bool isNullable) : base(name, isNullable) { - UnionItemTypes = unionItemTypes; + VariantTypes = variantTypes; } - public IReadOnlyList UnionItemTypes { get; } + public IReadOnlyList VariantTypes { get; internal set; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs index 81ff5fe127..8a7ab3f21d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs @@ -32,19 +32,8 @@ public static InputModelType CreateModelType(ref Utf8JsonReader reader, string? id = id ?? throw new JsonException(); - // skip every other property until we have a name - while (name == null) - { - var hasName = reader.TryReadString(nameof(InputModelType.Name), ref name); - if (!hasName) - { - reader.SkipProperty(); - } - } - name = name ?? throw new JsonException("Model must have name"); - // create an empty model to resolve circular references - var model = new InputModelType(name, null, null, null, null, InputModelTypeUsage.None, null!, null, new List(), null, null, null, false); + var model = new InputModelType(name!, null, null, null, null, InputModelTypeUsage.None, null!, null, new List(), null, null, null, false); resolver.AddReference(id, model); bool isNullable = false; @@ -62,7 +51,8 @@ public static InputModelType CreateModelType(ref Utf8JsonReader reader, string? // read all possible properties and throw away the unknown properties while (reader.TokenType != JsonTokenType.EndObject) { - var isKnownProperty = reader.TryReadBoolean(nameof(InputModelType.IsNullable), ref isNullable) + var isKnownProperty = reader.TryReadString(nameof(InputModelType.Name), ref name) + || reader.TryReadBoolean(nameof(InputModelType.IsNullable), ref isNullable) || reader.TryReadString(nameof(InputModelType.Namespace), ref ns) || reader.TryReadString(nameof(InputModelType.Accessibility), ref accessibility) || reader.TryReadString(nameof(InputModelType.Deprecated), ref deprecated) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs index f6209b83e8..590f5d6340 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs @@ -50,7 +50,7 @@ private InputType CreateObject(ref Utf8JsonReader reader, JsonSerializerOptions } private const string LiteralKind = "constant"; - private const string UnionKind = "Union"; + private const string UnionKind = "union"; private const string ModelKind = "Model"; private const string EnumKind = "enum"; private const string ArrayKind = "Array"; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs index d7203f812d..c6159934a1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -24,59 +24,39 @@ public override void Write(Utf8JsonWriter writer, InputUnionType value, JsonSeri public static InputUnionType CreateInputUnionType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) { - var isFirstProperty = id == null; + if (id == null) + { + reader.TryReadReferenceId(ref id); + } + + id = id ?? throw new JsonException(); + + // create an empty model to resolve circular references + var union = new InputUnionType(null!, null!, false); + resolver.AddReference(id, union); + bool isNullable = false; - var unionItemTypes = new List(); + IReadOnlyList? variantTypes = null; while (reader.TokenType != JsonTokenType.EndObject) { - var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputUnionType.Name), ref name) + var isKnownProperty = reader.TryReadString(nameof(InputUnionType.Name), ref name) + || reader.TryReadWithConverter(nameof(InputUnionType.VariantTypes), options, ref variantTypes) || reader.TryReadBoolean(nameof(InputUnionType.IsNullable), ref isNullable); - if (isKnownProperty) - { - continue; - } - - if (reader.GetString() == nameof(InputUnionType.UnionItemTypes)) - { - reader.Read(); - CreateUnionItemTypes(ref reader, unionItemTypes, options); - } - else + if (!isKnownProperty) { reader.SkipProperty(); } } - name = name ?? throw new JsonException($"{nameof(InputLiteralType)} must have a name."); - if (unionItemTypes == null || unionItemTypes.Count == 0) + union.Name = name ?? throw new JsonException($"{nameof(InputLiteralType)} must have a name."); + if (variantTypes == null || variantTypes.Count == 0) { throw new JsonException("Union must have a least one union type"); } - - var unionType = new InputUnionType(name, unionItemTypes, isNullable); - if (id != null) - { - resolver.AddReference(id, unionType); - } - return unionType; - } - - private static void CreateUnionItemTypes(ref Utf8JsonReader reader, ICollection itemTypes, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new JsonException(); - } - reader.Read(); - - while (reader.TokenType != JsonTokenType.EndArray) - { - var type = reader.ReadWithConverter(options); - itemTypes.Add(type ?? throw new JsonException($"null {nameof(InputType)} isn't allowed")); - } - reader.Read(); + union.VariantTypes = variantTypes; + union.IsNullable = isNullable; + return union; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs index 0687674834..9f7399ae62 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs @@ -80,6 +80,10 @@ public async Task ExecuteAsync() helperWriter.Write(); generateFilesTasks.Add(workspace.AddGeneratedFile(Path.Combine("src", "Generated", "Internal", $"{ArgumentProvider.Instance.Type.Name}.cs"), helperWriter.ToString())); + helperWriter = CodeModelPlugin.Instance.GetWriter(OptionalProvider.Instance); + helperWriter.Write(); + generateFilesTasks.Add(workspace.AddGeneratedFile(Path.Combine("src", "Generated", "Internal", $"{OptionalProvider.Instance.Type.Name}.cs"), helperWriter.ToString())); + // Add all the generated files to the workspace await Task.WhenAll(generateFilesTasks); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs new file mode 100644 index 0000000000..a8a8453211 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal class OptionalProvider : TypeProvider + { + private static readonly Lazy _instance = new(() => new OptionalProvider()); + public static OptionalProvider Instance => _instance.Value; + + private class ListTemplate { } + + private readonly CSharpType _t = typeof(ListTemplate<>).GetGenericArguments()[0]; + private readonly CSharpType _tKey = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[0]; + private readonly CSharpType _tValue = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[1]; + private readonly CSharpType _genericChangeTrackingList; + private readonly CSharpType _genericChangeTrackingDictionary; + + private OptionalProvider() + { + _genericChangeTrackingList = ChangeTrackingListProvider.Instance.Type; + _genericChangeTrackingDictionary = ChangeTrackingDictionaryProvider.Instance.Type; + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + } + + public override string Name => "Optional"; + + protected override MethodProvider[] BuildMethods() + { + return + [ + BuildIsListDefined(), + BuildIsDictionaryDefined(), + BuildIsReadOnlyDictionaryDefined(), + IsStructDefined(), + IsObjectDefined(), + IsJsonElementDefined(), + IsStringDefined(), + ]; + } + + private MethodSignature GetIsDefinedSignature(ParameterProvider valueParam, IReadOnlyList? genericArguments = null, IReadOnlyList? genericParameterConstraints = null) => new( + "IsDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + [valueParam], + GenericArguments: genericArguments, + GenericParameterConstraints: genericParameterConstraints); + + private MethodSignature GetIsCollectionDefinedSignature(ParameterProvider collectionParam, params CSharpType[] cSharpTypes) => new( + "IsCollectionDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + [collectionParam], + GenericArguments: cSharpTypes); + + private MethodProvider IsStringDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(string)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReferenceSnippet(valueParam), Null)) + }); + } + + private MethodProvider IsJsonElementDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(JsonElement)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(new JsonElementSnippet(new ParameterReferenceSnippet(valueParam)).ValueKindNotEqualsUndefined()) + }); + } + + private MethodProvider IsObjectDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(object)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReferenceSnippet(valueParam), Null)) + }); + } + + private MethodProvider IsStructDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", _t.WithNullable(true)); + var signature = GetIsDefinedSignature(valueParam, new[] { _t }, new[] { Where.Struct(_t) }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(new MemberExpression(new ParameterReferenceSnippet(valueParam), "HasValue")) + }); + } + + private MethodProvider BuildIsReadOnlyDictionaryDefined() + { + var collectionParam = new ParameterProvider("collection", $"The value.", new CSharpType(typeof(IReadOnlyDictionary<,>), _tKey, _tValue)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private MethodProvider BuildIsDictionaryDefined() + { + var collectionParam = new ParameterProvider("collection", $"The collection.", new CSharpType(typeof(IDictionary<,>), _tKey, _tValue)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private MethodProvider BuildIsListDefined() + { + var collectionParam = new ParameterProvider("collection", $"The collection.", new CSharpType(typeof(IEnumerable<>), _t)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _t); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingList, new CodeWriterDeclaration("changeTrackingList")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + internal BoolSnippet IsDefined(TypedSnippet value) + { + return new BoolSnippet(new InvokeStaticMethodExpression(Type, "IsDefined", [ value ])); + } + + internal BoolSnippet IsCollectionDefined(TypedSnippet collection) + { + return new BoolSnippet(new InvokeStaticMethodExpression(Type, "IsCollectionDefined", [ collection ])); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs new file mode 100644 index 0000000000..3ee8df04cf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using UnbrandedTypeSpec; +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class OptionalTests + { + [Test] + public void List_Undefined_ReturnsFalse() + { + var list = new ChangeTrackingList(); + Assert.IsFalse(Optional.IsCollectionDefined(list)); + } + + [Test] + public void List_Defined_ReturnsTrue() + { + IList innerList = new List { 1, 2, 3 }; + var list = new ChangeTrackingList(innerList); + Assert.IsTrue(Optional.IsCollectionDefined(list)); + } + + [Test] + public void Dict_Undefined_ReturnsFalse() + { + var dict = new ChangeTrackingDictionary(); + Assert.IsFalse(Optional.IsCollectionDefined((IDictionary)dict)); + } + + [Test] + public void Dict_Defined_ReturnsTrue() + { + IDictionary innerDict = new Dictionary { { 1, "one" }, { 2, "two" } }; + var dict = new ChangeTrackingDictionary(innerDict); + Assert.IsTrue(Optional.IsCollectionDefined((IDictionary)dict)); + } + + [Test] + public void ReadOnlyDict_Undefined_ReturnsFalse() + { + var dict = new ChangeTrackingDictionary(); + IReadOnlyDictionary readOnlyDict = dict; + Assert.IsFalse(Optional.IsCollectionDefined(readOnlyDict)); + } + + [Test] + public void ReadOnlyDict_Defined_ReturnsTrue() + { + IReadOnlyDictionary innerDict = new Dictionary { { 1, "one" }, { 2, "two" } }; + var dict = new ChangeTrackingDictionary(innerDict); + IReadOnlyDictionary readOnlyDict = dict; + Assert.IsTrue(Optional.IsCollectionDefined(readOnlyDict)); + } + + [Test] + public void Nullable_HasValue_ReturnsTrue() + { + int? value = 5; + Assert.IsTrue(Optional.IsDefined(value)); + } + + [Test] + public void Nullable_NoValue_ReturnsFalse() + { + int? value = null; + Assert.IsFalse(Optional.IsDefined(value)); + } + + [Test] + public void Obj_NotNull_ReturnsTrue() + { + var value = new object(); + Assert.IsTrue(Optional.IsDefined(value)); + } + + + [Test] + public void Json_Defined_ReturnsTrue() + { + var value = JsonDocument.Parse("{}").RootElement; + Assert.IsTrue(Optional.IsDefined(value)); + } + + [Test] + public void Json_Undefined_ReturnsFalse() + { + var value = new JsonElement(); + Assert.IsFalse(Optional.IsDefined(value)); + } + + [Test] + public void Str_NotNull_ReturnsTrue() + { + string value = "test"; + Assert.IsTrue(Optional.IsDefined(value)); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs new file mode 100644 index 0000000000..117cd9c4a2 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs @@ -0,0 +1,55 @@ +// + +#nullable disable + +using System.Collections.Generic; +using System.Text.Json; + +namespace UnbrandedTypeSpec +{ + internal static partial class Optional + { + /// The collection. + public static bool IsCollectionDefined(IEnumerable collection) + { + return !(collection is ChangeTrackingList changeTrackingList && changeTrackingList.IsUndefined); + } + + /// The collection. + public static bool IsCollectionDefined(IDictionary collection) + { + return !(collection is ChangeTrackingDictionary changeTrackingDictionary && changeTrackingDictionary.IsUndefined); + } + + /// The value. + public static bool IsCollectionDefined(IReadOnlyDictionary collection) + { + return !(collection is ChangeTrackingDictionary changeTrackingDictionary && changeTrackingDictionary.IsUndefined); + } + + /// The value. + public static bool IsDefined(T? value) + where T : struct + { + return value.HasValue; + } + + /// The value. + public static bool IsDefined(object value) + { + return value != null; + } + + /// The value. + public static bool IsDefined(System.Text.Json.JsonElement value) + { + return value.ValueKind != System.Text.Json.JsonValueKind.Undefined; + } + + /// The value. + public static bool IsDefined(string value) + { + return value != null; + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json index f5954ee394..c0caf4b6c8 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json @@ -428,9 +428,9 @@ "Description": "required Union", "Type": { "$id": "64", - "Kind": "Union", - "Name": "Union", - "UnionItemTypes": [ + "Kind": "union", + "Name": "ThingRequiredUnion", + "VariantTypes": [ { "$id": "65", "Kind": "string", diff --git a/packages/playground/src/react/editor.tsx b/packages/playground/src/react/editor.tsx index bb21acba39..1cdce05796 100644 --- a/packages/playground/src/react/editor.tsx +++ b/packages/playground/src/react/editor.tsx @@ -53,7 +53,7 @@ export const Editor: FunctionComponent = ({ model, options, actions return (
diff --git a/packages/playground/src/react/output-tabs/output-tabs.module.css b/packages/playground/src/react/output-tabs/output-tabs.module.css index 89228b7686..ef98e3a5c3 100644 --- a/packages/playground/src/react/output-tabs/output-tabs.module.css +++ b/packages/playground/src/react/output-tabs/output-tabs.module.css @@ -1,6 +1,7 @@ .tabs { border-bottom: 1px solid var(--colorNeutralStroke1); box-shadow: var(--shadow2); + overflow-y: auto; } .tab-divider { diff --git a/packages/playground/src/react/output-view/file-viewer.tsx b/packages/playground/src/react/output-view/file-viewer.tsx index 6d3f6a95ae..500da88cf7 100644 --- a/packages/playground/src/react/output-view/file-viewer.tsx +++ b/packages/playground/src/react/output-view/file-viewer.tsx @@ -2,11 +2,15 @@ import { FolderListRegular } from "@fluentui/react-icons"; import { useCallback, useEffect, useState } from "react"; import { FileOutput } from "../file-output/file-output.js"; import { OutputTabs } from "../output-tabs/output-tabs.js"; -import type { OutputViewerProps, ProgramViewer } from "../types.js"; +import type { FileOutputViewer, OutputViewerProps, ProgramViewer } from "../types.js"; import style from "./output-view.module.css"; -const FileViewerComponent = ({ program, outputFiles }: OutputViewerProps) => { +const FileViewerComponent = ({ + program, + outputFiles, + fileViewers, +}: OutputViewerProps & { fileViewers: Record }) => { const [filename, setFilename] = useState(""); const [content, setContent] = useState(""); @@ -42,15 +46,20 @@ const FileViewerComponent = ({ program, outputFiles }: OutputViewerProps) => {
- +
); }; -export const FileViewer: ProgramViewer = { - key: "file-output", - label: "Output explorer", - icon: , - render: FileViewerComponent, -}; +export function createFileViewer(fileViewers: FileOutputViewer[]): ProgramViewer { + const viewerMap = Object.fromEntries(fileViewers.map((x) => [x.key, x])); + return { + key: "file-output", + label: "Output explorer", + icon: , + render: (props) => { + return ; + }, + }; +} diff --git a/packages/playground/src/react/output-view/output-view.tsx b/packages/playground/src/react/output-view/output-view.tsx index 0c2575408e..1d1dd514c6 100644 --- a/packages/playground/src/react/output-view/output-view.tsx +++ b/packages/playground/src/react/output-view/output-view.tsx @@ -2,7 +2,7 @@ import { Tab, TabList, type SelectTabEventHandler } from "@fluentui/react-compon import { useCallback, useMemo, useState, type FunctionComponent } from "react"; import type { PlaygroundEditorsOptions } from "../playground.js"; import type { CompilationState, CompileResult, FileOutputViewer, ProgramViewer } from "../types.js"; -import { FileViewer } from "./file-viewer.js"; +import { createFileViewer } from "./file-viewer.js"; import { TypeGraphViewer } from "./type-graph-viewer.js"; import style from "./output-view.module.css"; @@ -43,10 +43,10 @@ function resolveViewers( viewers: ProgramViewer[] | undefined, fileViewers: FileOutputViewer[] | undefined ): ResolvedViewers { + const fileViewer = createFileViewer(fileViewers ?? []); const output: ResolvedViewers = { - fileViewers: {}, programViewers: { - [FileViewer.key]: FileViewer, + [fileViewer.key]: fileViewer, [TypeGraphViewer.key]: TypeGraphViewer, }, }; @@ -54,15 +54,11 @@ function resolveViewers( for (const item of viewers ?? []) { output.programViewers[item.key] = item; } - for (const item of fileViewers ?? []) { - output.fileViewers[item.key] = item; - } return output; } interface ResolvedViewers { - fileViewers: Record; programViewers: Record; } diff --git a/packages/playground/src/react/playground.module.css b/packages/playground/src/react/playground.module.css index 80fcee1243..6f6b9ac94d 100644 --- a/packages/playground/src/react/playground.module.css +++ b/packages/playground/src/react/playground.module.css @@ -5,3 +5,8 @@ height: 100%; overflow: hidden; } + +.edit-pane { + display: flex; + flex-direction: column; +} diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index b677999830..6a36f32fc1 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -261,7 +261,7 @@ export const Playground: FunctionComponent = (props) => { - + { if (client) { await client.stop(); await client.start(); + outputChannel.debug("TypeSpec server restarted"); } } async function launchLanguageClient(context: ExtensionContext) { const exe = await resolveTypeSpecServer(context); + outputChannel.debug("TypeSpec server resolved as ", exe); const options: LanguageClientOptions = { synchronize: { // Synchronize the setting section 'typespec' to the server @@ -61,24 +68,24 @@ async function launchLanguageClient(context: ExtensionContext) { }; const name = "TypeSpec"; - const id = "typespecLanguageServer"; + const id = "typespec"; try { client = new LanguageClient(id, name, { run: exe, debug: exe }, options); await client.start(); + outputChannel.debug("TypeSpec server started"); } catch (e) { if (typeof e === "string" && e.startsWith("Launching server using command")) { const workspaceFolder = workspace.workspaceFolders?.[0]?.uri?.fsPath ?? ""; - client?.error( + outputChannel.error( [ `TypeSpec server executable was not found: '${exe.command}' is not found. Make sure either:`, ` - TypeSpec is installed locally at the root of this workspace ("${workspaceFolder}") or in a parent directory.`, " - TypeSpec is installed globally with `npm install -g @typespec/compiler'.", " - TypeSpec server path is configured with https://github.com/microsoft/typespec#installing-vs-code-extension.", - ].join("\n"), - undefined, - false + ].join("\n") ); + outputChannel.error("Error detail", e); throw `TypeSpec server executable was not found: '${exe.command}' is not found.`; } else { throw e; @@ -96,6 +103,7 @@ async function resolveTypeSpecServer(context: ExtensionContext): Promise o) ?? []; + outputChannel.debug("TypeSpec server resolved in development mode"); return { command: "node", args: [...options, script, ...args] }; } @@ -116,11 +124,14 @@ async function resolveTypeSpecServer(context: ExtensionContext): Promise { + return this.delegate.onDidChangeLogLevel; + } + trace(message: string, ...args: any[]): void { + this.delegate.trace(message, ...args); + } + debug(message: string, ...args: any[]): void { + this.delegate.debug(message, ...args); + } + info(message: string, ...args: any[]): void { + this.delegate.info(message, ...args); + } + warn(message: string, ...args: any[]): void { + this.delegate.warn(message, ...args); + } + error(error: string | Error, ...args: any[]): void { + this.delegate.error(error, ...args); + } + get name(): string { + return this.delegate.name; + } + replace(value: string): void { + this.delegate.replace(value); + } + clear(): void { + this.delegate.clear(); + } + show(preserveFocus?: boolean | undefined): void; + show(column?: vscode.ViewColumn | undefined, preserveFocus?: boolean | undefined): void; + show(column?: unknown, preserveFocus?: unknown): void { + // eslint-disable-next-line deprecation/deprecation + this.delegate.show(column as any, preserveFocus as any); + } + hide(): void { + this.delegate.hide(); + } + dispose(): void { + this.delegate.dispose(); + } + + append(value: string): void { + this.logToDelegate(value); + } + appendLine(value: string): void { + this.logToDelegate(value); + } + + private preLevel: "trace" | "debug" | "info" | "warning" | "error" | "" = ""; + private logToDelegate(value: string) { + if (TRACE_PREFIX.test(value)) { + this.preLevel = "trace"; + this.delegate.trace(value.replace(TRACE_PREFIX, "")); + } else if (DEBUG_PREFIX.test(value)) { + this.preLevel = "debug"; + this.delegate.debug(value.replace(DEBUG_PREFIX, "")); + } else if (INFO_PREFIX.test(value)) { + this.preLevel = "info"; + this.delegate.info(value.replace(INFO_PREFIX, "")); + } else if (WARN_PREFIX.test(value)) { + this.preLevel = "warning"; + this.delegate.warn(value.replace(WARN_PREFIX, "")); + } else if (ERROR_PREFIX.test(value)) { + this.preLevel = "error"; + this.delegate.error(value.replace(ERROR_PREFIX, "")); + } else { + // a msg sent without a level prefix should be because a message is sent by calling multiple appendLine() + // so just log it with the previous level + switch (this.preLevel) { + case "trace": + this.delegate.trace(value); + break; + case "debug": + this.delegate.debug(value); + break; + case "info": + this.delegate.info(value); + break; + case "warning": + this.delegate.warn(value); + break; + case "error": + this.delegate.error(value); + break; + default: + this.delegate.debug( + `Log Message with invalid log level (${this.preLevel}). Raw message: ${value}` + ); + } + } + } +} diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index 9dded3dbfd..76b6d46d75 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -236,12 +236,43 @@ function getParentAddedVersionInTimeline( } /** - * Returns true if the first version modifier was @added, false if @removed. + * Uses the added, removed and parent metadata to resolve any issues with + * implicit versioning and return the added array with this taken into account. + * @param added the array of versions from the `@added` decorator + * @param removed the array of versions from the `@removed` decorator + * @param parentAdded the version when the parent type was added + * @returns the added array, with any implicit versioning taken into consideration. */ -function resolveAddedFirst(added: Version[], removed: Version[]): boolean { - if (added.length === 0) return false; - if (removed.length === 0) return true; - return added[0].index < removed[0].index; +function resolveWhenFirstAdded( + added: Version[], + removed: Version[], + parentAdded: Version +): Version[] { + const implicitlyAvailable = !added.length && !removed.length; + if (implicitlyAvailable) { + // if type has no version info, it inherits from the parent + return [parentAdded]; + } + + if (added.length) { + const addedFirst = !removed.length || added[0].index < removed[0].index; + if (addedFirst) { + // if the type was added first, then implicitly it wasn't available before + // and thus should NOT inherit from its parent + return added; + } + } + + if (removed.length) { + const removedFirst = !added.length || removed[0].index < added[0].index; + if (removedFirst) { + // if the type was removed first the implicitly it was available before + // and thus SHOULD inherit from its parent + return [parentAdded, ...added]; + } + } + // we shouldn't get here, but if we do, then make no change to the added array + return added; } export function getAvailabilityMap( @@ -254,7 +285,8 @@ export function getAvailabilityMap( // if unversioned then everything exists if (allVersions === undefined) return undefined; - const parentAdded = getParentAddedVersion(program, type, allVersions); + const firstVersion = allVersions[0]; + const parentAdded = getParentAddedVersion(program, type, allVersions) ?? firstVersion; let added = getAddedOnVersions(program, type) ?? []; const removed = getRemovedOnVersions(program, type) ?? []; const typeChanged = getTypeChangedFrom(program, type); @@ -270,24 +302,7 @@ export function getAvailabilityMap( ) return undefined; - const wasAddedFirst = resolveAddedFirst(added, removed); - - // implicitly, all versioned things are assumed to have been added at - // v1 if not specified, or inherited from their parent. - if (!wasAddedFirst && !parentAdded) { - // if the first version modifier was @removed, and the parent is implicitly available, - // then assume the type was available. - added = [allVersions[0], ...added]; - } else if (!added.length && !parentAdded) { - // no version information on the item or its parent implicitly means it has always been available - added.push(allVersions[0]); - } else if (!added.length && parentAdded) { - // if no version info on type but is on parent, inherit that parent's "added" version - added.push(parentAdded); - } else if (added.length && parentAdded) { - // if "added" info on both the type and parent, combine them - added = [parentAdded, ...added]; - } + added = resolveWhenFirstAdded(added, removed, parentAdded); // something isn't available by default let isAvail = false; @@ -316,7 +331,8 @@ export function getAvailabilityMapInTimeline( ): Map | undefined { const avail = new Map(); - const parentAdded = getParentAddedVersionInTimeline(program, type, timeline); + const firstVersion = timeline.first().versions().next().value; + const parentAdded = getParentAddedVersionInTimeline(program, type, timeline) ?? firstVersion; let added = getAddedOnVersions(program, type) ?? []; const removed = getRemovedOnVersions(program, type) ?? []; const typeChanged = getTypeChangedFrom(program, type); @@ -332,25 +348,7 @@ export function getAvailabilityMapInTimeline( ) return undefined; - const wasAddedFirst = resolveAddedFirst(added, removed); - - // implicitly, all versioned things are assumed to have been added at - // v1 if not specified, or inherited from their parent. - const firstVersion = timeline.first().versions().next().value; - if (!wasAddedFirst && !parentAdded) { - // if the first version modifier was @removed, and the parent is implicitly available, - // then assume the type was available. - added = [firstVersion, ...added]; - } else if (!added.length && !parentAdded) { - // no version information on the item or its parent implicitly means it has always been available - added.push(firstVersion); - } else if (!added.length && parentAdded) { - // if no version info on type but is on parent, inherit that parent's "added" version - added.push(parentAdded); - } else if (added.length && parentAdded) { - // if "added" info on both the type and parent, combine them - added = [parentAdded, ...added]; - } + added = resolveWhenFirstAdded(added, removed, parentAdded); // something isn't available by default let isAvail = false; diff --git a/packages/versioning/test/versioning.test.ts b/packages/versioning/test/versioning.test.ts index e2635e891f..3cb09f7eb6 100644 --- a/packages/versioning/test/versioning.test.ts +++ b/packages/versioning/test/versioning.test.ts @@ -248,6 +248,37 @@ describe("versioning: logic", () => { ); }); + it("can be added after parent", async () => { + const { + source, + projections: [v1, v2], + } = await versionedModel( + ["v1", "v2"], + `@added(Versions.v1) + model Test { + a: int32; + @added(Versions.v2) + b: NewThing; + } + + @added(Versions.v2) + model NewThing { + val: string; + } + ` + ); + assertHasProperties(v1, ["a"]); + assertHasProperties(v2, ["a", "b"]); + + assertModelProjectsTo( + [ + [v1, "v1"], + [v2, "v2"], + ], + source + ); + }); + it("can be removed", async () => { const { source,