Repository: microsoft/typespec
Pull request #7239: Leverage LSP to do compile instead of CLI
Author: login: chunyu3
Labels: name: ide
description: Issues for VS, VSCode, Monaco, etc.
Created: 2025-05-06T05:06:47Z | Merged: 2025-05-15T06:31:56Z

Fix https://github.com/microsoft/typespec/issues/6663

Now we are having many problems when triggering the compile from CLI and handling its output in our Output window like multiple loglevel, how to handle ```stderr``, coloring and so on.

So to resolve all these problems, let's trigger compile through LSP instead of CLI so that we can retrieve the result of compile directly which is more reliable and also easier to do post-processing.


Diff:
diff --git a/.chronus/changes/lsp-2025-4-9-9-38-32.md b/.chronus/changes/lsp-2025-4-9-9-38-32.md
new file mode 100644
index 00000000000..9f497512784
--- /dev/null
+++ b/.chronus/changes/lsp-2025-4-9-9-38-32.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - typespec-vscode
+---
+
+Use language server to compile project instead of CLI
\ No newline at end of file
diff --git a/.chronus/changes/lsp-2025-4-9-9-38-5.md b/.chronus/changes/lsp-2025-4-9-9-38-5.md
new file mode 100644
index 00000000000..a10ec3ed269
--- /dev/null
+++ b/.chronus/changes/lsp-2025-4-9-9-38-5.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@typespec/compiler"
+---
+
+[LSP] Expose new compile project command
\ No newline at end of file
diff --git a/packages/compiler/src/internals/index.ts b/packages/compiler/src/internals/index.ts
index 5b27ff61229..a2926d5ac9e 100644
--- a/packages/compiler/src/internals/index.ts
+++ b/packages/compiler/src/internals/index.ts
@@ -10,3 +10,4 @@ if (!(globalThis as any).enableCompilerInternalsExport) {
 export { NodeSystemHost } from "../core/node-system-host.js";
 export { InitTemplateSchema } from "../init/init-template.js";
 export { makeScaffoldingConfig, scaffoldNewProject } from "../init/scaffold.js";
+export { InternalCompileResult, ServerDiagnostic } from "../server/index.js";
diff --git a/packages/compiler/src/server/compile-service.ts b/packages/compiler/src/server/compile-service.ts
index beaa02f390f..380f40338d9 100644
--- a/packages/compiler/src/server/compile-service.ts
+++ b/packages/compiler/src/server/compile-service.ts
@@ -28,6 +28,7 @@ import { parseYaml } from "../yaml/parser.js";
 import { serverOptions } from "./constants.js";
 import { FileService } from "./file-service.js";
 import { FileSystemCache } from "./file-system-cache.js";
+import { trackActionFunc } from "./server-track-action-task.js";
 import { CompileResult, ServerHost, ServerLog } from "./types.js";
 import { UpdateManger } from "./update-manager.js";
 
@@ -44,7 +45,14 @@ export interface CompileService {
    * @param document The document to compile. This is not necessarily the entrypoint, compile will try to guess which entrypoint to compile to include this one.
    * @returns the compiled result or undefined if compilation was aborted.
    */
-  compile(document: TextDocument | TextDocumentIdentifier): Promise<CompileResult | undefined>;
+  compile(
+    document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    compileOptions?: {
+      bypassCache?: boolean;
+      trackAction?: boolean;
+    },
+  ): Promise<CompileResult | undefined>;
 
   /**
    * Load the AST for the given document.
@@ -112,18 +120,26 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    runOptions?: {
+      bypassCache?: boolean;
+      trackAction?: boolean;
+    },
   ): Promise<CompileResult | undefined> {
     const path = await fileService.getPath(document);
     const mainFile = await getMainFileForDocument(path);
     const config = await getConfig(mainFile);
     configFilePath = config.filename;
     log({ level: "debug", message: `config resolved`, detail: config });
-
-    const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: path });
+    const [optionsFromConfig, _] = resolveOptionsFromConfig(config, {
+      cwd: getDirectoryPath(path),
+    });
     const options: CompilerOptions = {
       ...optionsFromConfig,
       ...serverOptions,
+      ...(additionalOptions ?? {}),
     };
+
     // add linter rule for unused using if user didn't configure it explicitly
     const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
     if (
@@ -154,7 +170,22 @@ export function createCompileService({
 
     let program: Program;
     try {
-      program = await compileProgram(compilerHost, mainFile, options, oldPrograms.get(mainFile));
+      program = await compileProgram(
+        runOptions?.trackAction
+          ? {
+              ...compilerHost,
+              logSink: {
+                log: compilerHost.logSink.log,
+                getPath: compilerHost.logSink.getPath,
+                trackAction: (message, finalMessage, action) =>
+                  trackActionFunc(serverHost.log, message, finalMessage, action),
+              },
+            }
+          : compilerHost,
+        mainFile,
+        options,
+        runOptions?.bypassCache ? undefined : oldPrograms.get(mainFile),
+      );
       oldPrograms.set(mainFile, program);
       if (!fileService.upToDate(document)) {
         return undefined;
diff --git a/packages/compiler/src/server/index.ts b/packages/compiler/src/server/index.ts
index cce1a78d68c..3f21efa5333 100644
--- a/packages/compiler/src/server/index.ts
+++ b/packages/compiler/src/server/index.ts
@@ -8,10 +8,12 @@ export type {
   InitProjectTemplate,
   InitProjectTemplateEmitterTemplate,
   InitProjectTemplateLibrarySpec,
+  InternalCompileResult,
   SemanticToken,
   SemanticTokenKind,
   Server,
   ServerCustomCapacities,
+  ServerDiagnostic,
   ServerHost,
   ServerInitializeResult,
   ServerLog,
diff --git a/packages/compiler/src/server/server-track-action-task.ts b/packages/compiler/src/server/server-track-action-task.ts
new file mode 100644
index 00000000000..0eedab5b4b3
--- /dev/null
+++ b/packages/compiler/src/server/server-track-action-task.ts
@@ -0,0 +1,87 @@
+import { TaskStatus, TrackActionTask } from "../core/types.js";
+import { ServerLog } from "./types.js";
+
+export class ServerTrackActionTask implements TrackActionTask {
+  #log: (log: ServerLog) => void;
+  #message: string;
+  #running: boolean;
+  #finalMessage: string;
+
+  constructor(message: string, finalMessage: string, log: (log: ServerLog) => void) {
+    this.#message = message;
+    this.#finalMessage = finalMessage;
+    this.#log = log;
+    this.#running = true;
+  }
+
+  get message() {
+    return this.#message;
+  }
+
+  get isStopped() {
+    return !this.#running;
+  }
+
+  set message(newMessage: string) {
+    this.#message = newMessage;
+  }
+
+  start() {
+    this.#log({
+      level: "info",
+      message: this.#message,
+    });
+  }
+
+  succeed(message?: string) {
+    this.stop("success", message);
+  }
+  fail(message?: string) {
+    this.stop("failure", message);
+  }
+  warn(message?: string) {
+    this.stop("warn", message);
+  }
+  skip(message?: string) {
+    this.stop("skipped", message);
+  }
+
+  stop(status: TaskStatus, message?: string) {
+    this.#running = false;
+    this.#message = message ?? this.#finalMessage;
+    this.#log({
+      level: status !== "failure" ? "info" : "error",
+      message: `[${TaskStatusText[status]}] ${this.#message}\n`,
+    });
+  }
+}
+
+const TaskStatusText = {
+  success: "succeed",
+  failure: "failed",
+  warn: "succeeded with warnings",
+  skipped: "skipped",
+};
+
+/** internal */
+export async function trackActionFunc<T>(
+  log: (log: ServerLog) => void,
+  message: string,
+  finalMessage: string,
+  asyncAction: (task: TrackActionTask) => Promise<T>,
+): Promise<T> {
+  const task = new ServerTrackActionTask(message, finalMessage, log);
+  task.start();
+
+  try {
+    const result = await asyncAction(task);
+    if (!task.isStopped) {
+      task.succeed();
+    }
+
+    return result;
+  } catch (error) {
+    task.fail(message);
+    throw error;
+  }
+}
diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts
index 09ee35c2cc6..edd082164bb 100644
--- a/packages/compiler/src/server/server.ts
+++ b/packages/compiler/src/server/server.ts
@@ -136,6 +136,8 @@ function main() {
   connection.onRequest(getInitProjectContextRequestName, profile(s.getInitProjectContext));
   const initProjectRequestName: CustomRequestName = "typespec/initProject";
   connection.onRequest(initProjectRequestName, profile(s.initProject));
+  const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
+  connection.onRequest(compileProjectRequestName, profile(s.internalCompile));
 
   documents.onDidChangeContent(profile(s.checkChange));
   documents.onDidClose(profile(s.documentClosed));
diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts
index 12462e85c04..1305af6b8b3 100644
--- a/packages/compiler/src/server/serverlib.ts
+++ b/packages/compiler/src/server/serverlib.ts
@@ -57,6 +57,7 @@ import { builtInLinterRule_UnusedTemplateParameter } from "../core/linter-rules/
 import { builtInLinterRule_UnusedUsing } from "../core/linter-rules/unused-using.rule.js";
 import { builtInLinterLibraryName } from "../core/linter.js";
 import { formatLog } from "../core/logger/index.js";
+import { CompilerOptions } from "../core/options.js";
 import { getPositionBeforeTrivia } from "../core/parser-utils.js";
 import { getNodeAtPosition, getNodeAtPositionDetail, visitChildren } from "../core/parser.js";
 import {
@@ -117,9 +118,11 @@ import {
   CompileResult,
   InitProjectConfig,
   InitProjectContext,
+  InternalCompileResult,
   SemanticTokenKind,
   Server,
   ServerCustomCapacities,
+  ServerDiagnostic,
   ServerHost,
   ServerInitializeResult,
   ServerLog,
@@ -199,6 +202,7 @@ export function createServer(host: ServerHost): Server {
     getInitProjectContext,
     validateInitProjectTemplate,
     initProject,
+    internalCompile,
   };
 
   async function initialize(params: InitializeParams): Promise<InitializeResult> {
@@ -301,6 +305,7 @@ export function createServer(host: ServerHost): Server {
       getInitProjectContext: true,
       initProject: true,
       validateInitProjectTemplate: true,
+      internalCompile: true,
     };
     // the file path is expected to be .../@typespec/compiler/dist/src/server/serverlib.js
     const curFile = normalizePath(compilerHost.fileURLToPath(import.meta.url));
@@ -367,6 +372,61 @@ export function createServer(host: ServerHost): Server {
     }
   }
 
+  async function internalCompile(param: {
+    doc: TextDocumentIdentifier;
+    options: CompilerOptions;
+  }): Promise<InternalCompileResult> {
+    const option: CompilerOptions = {
+      ...param.options,
+    };
+
+    const result = await compileService.compile(param.doc, option, {
+      bypassCache: true,
+      trackAction: true,
+    });
+    if (result === undefined) {
+      return {
+        hasError: true,
+        diagnostics: [
+          {
+            code: "internal-error",
+            message:
+              "Failed to get compiler result, please check the compilation output for details",
+            severity: "error",
+            target: NoTarget,
+            url: undefined,
+          },
+        ],
+        entrypoint: undefined,
+        options: undefined,
+      };
+    } else {
+      return {
+        hasError: result.program.hasError(),
+        diagnostics: result.program.diagnostics.map((diagnostic) => {
+          const target = getSourceLocation(diagnostic.target, { locateId: true });
+          let position = undefined;
+          if (target?.file) {
+            const lineAndCharacter = target.file.getLineAndCharacterOfPosition(target.pos);
+            position = {
+              line: lineAndCharacter.line + 1,
+              column: lineAndCharacter.character + 1,
+            };
+          }
+          return {
+            code: diagnostic.code,
+            message: diagnostic.message,
+            severity: diagnostic.severity,
+            target: { ...target, position: position },
+            url: diagnostic.url,
+          } as ServerDiagnostic;
+        }),
+        entrypoint: result.document?.uri,
+        options: result.program.compilerOptions,
+      };
+    }
+  }
+
   async function renameFiles(params: RenameFilesParams): Promise<void> {
     const firstFilePath = params.files[0];
     if (!firstFilePath) {
diff --git a/packages/compiler/src/server/types.ts b/packages/compiler/src/server/types.ts
index 54586a6868e..15f32e2575e 100644
--- a/packages/compiler/src/server/types.ts
+++ b/packages/compiler/src/server/types.ts
@@ -40,7 +40,14 @@ import {
 import type { TextDocument, TextEdit } from "vscode-languageserver-textdocument";
 import type { CompilerOptions } from "../core/options.js";
 import type { Program } from "../core/program.js";
-import type { CompilerHost, SourceFile, TypeSpecScriptNode } from "../core/types.js";
+import type {
+  CompilerHost,
+  Diagnostic,
+  NoTarget,
+  SourceFile,
+  SourceLocation,
+  TypeSpecScriptNode,
+} from "../core/types.js";
 import { LoadedCoreTemplates } from "../init/core-templates.js";
 import { EmitterTemplate, InitTemplate, InitTemplateLibrarySpec } from "../init/init-template.js";
 import { ScaffoldingConfig } from "../init/scaffold.js";
@@ -70,6 +77,17 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface ServerDiagnostic extends Diagnostic {
+  target: (SourceLocation & { position?: { line: number; column: number } }) | typeof NoTarget;
+}
+
+export interface InternalCompileResult {
+  readonly hasError: boolean;
+  readonly diagnostics: ServerDiagnostic[];
+  readonly entrypoint?: string;
+  readonly options?: CompilerOptions;
+}
+
 export interface Server {
   readonly pendingMessages: readonly ServerLog[];
   readonly workspaceFolders: readonly ServerWorkspaceFolder[];
@@ -106,6 +124,10 @@ export interface Server {
   getInitProjectContext(): Promise<InitProjectContext>;
   validateInitProjectTemplate(param: { template: InitTemplate }): Promise<boolean>;
   initProject(param: { config: InitProjectConfig }): Promise<boolean>;
+  internalCompile(param: {
+    doc: TextDocumentIdentifier;
+    options: CompilerOptions;
+  }): Promise<InternalCompileResult>;
 }
 
 export interface ServerSourceFile extends SourceFile {
@@ -156,12 +178,13 @@ export interface SemanticToken {
 export type CustomRequestName =
   | "typespec/getInitProjectContext"
   | "typespec/initProject"
-  | "typespec/validateInitProjectTemplate";
-
+  | "typespec/validateInitProjectTemplate"
+  | "typespec/internalCompile";
 export interface ServerCustomCapacities {
   getInitProjectContext?: boolean;
   validateInitProjectTemplate?: boolean;
   initProject?: boolean;
+  internalCompile?: boolean;
 }
 
 export interface ServerInitializeResult extends InitializeResult {
diff --git a/packages/typespec-vscode/src/extension-context.ts b/packages/typespec-vscode/src/extension-context.ts
new file mode 100644
index 00000000000..5b98dc742e7
--- /dev/null
+++ b/packages/typespec-vscode/src/extension-context.ts
@@ -0,0 +1,7 @@
+import { TspLanguageClient } from "./tsp-language-client";
+
+export let tspLanguageClient: TspLanguageClient | undefined;
+
+export function setTspLanguageClient(newTspLanguageClient: TspLanguageClient | undefined) {
+  tspLanguageClient = newTspLanguageClient;
+}
diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts
index c647a971732..ddd2aea8cc3 100644
--- a/packages/typespec-vscode/src/extension.ts
+++ b/packages/typespec-vscode/src/extension.ts
@@ -5,6 +5,7 @@ import "./pre-extension-activate.js";
 import vscode, { commands, ExtensionContext, TabInputText } from "vscode";
 import { State } from "vscode-languageclient";
 import { createCodeActionProvider } from "./code-action-provider.js";
+import { setTspLanguageClient, tspLanguageClient } from "./extension-context.js";
 import { ExtensionStateManager } from "./extension-state-manager.js";
 import { ExtensionLogListener, getPopupAction } from "./log/extension-log-listener.js";
 import logger from "./log/logger.js";
@@ -31,7 +32,6 @@ import { importFromOpenApi3 } from "./vscode-cmd/import-from-openapi3.js";
 import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js";
 import { clearOpenApi3PreviewTempFolders, showOpenApi3 } from "./vscode-cmd/openapi3-preview.js";
 
-let client: TspLanguageClient | undefined;
 /**
  * Workaround: LogOutputChannel doesn't work well with LSP RemoteConsole, so having a customized LogOutputChannel to make them work together properly
  * More detail can be found at https://github.com/microsoft/vscode-discussions/discussions/1149
@@ -45,6 +45,11 @@ export async function activate(context: ExtensionContext) {
     async (tel: OperationTelemetryEvent) => {
       const stateManager = new ExtensionStateManager(context);
       telemetryClient.Initialize(stateManager);
+      /**
+       * workaround: vscode output cannot display ANSI color.
+       * Set the NO_COLOR environment variable to suppress the addition of ANSI color escape codes.
+       */
+      process.env["NO_COLOR"] = "true";
       context.subscriptions.push(telemetryClient);
 
       context.subscriptions.push(createTaskProvider());
@@ -108,10 +113,10 @@ export async function activate(context: ExtensionContext) {
                       tel.lastStep = "Recreate LSP client in force";
                       return await recreateLSPClient(context, tel.activityId);
                     }
-                    if (client && client.state === State.Running) {
+                    if (tspLanguageClient && tspLanguageClient.state === State.Running) {
                       tel.lastStep = "Restart LSP client";
-                      await client.restart();
-                      return { code: ResultCode.Success, value: client };
+                      await tspLanguageClient.restart();
+                      return { code: ResultCode.Success, value: tspLanguageClient };
                     } else {
                       logger.info(
                         "TypeSpec LSP server is not running which is not expected, try to recreate and start...",
@@ -154,7 +159,7 @@ export async function activate(context: ExtensionContext) {
           await telemetryClient.doOperationWithTelemetry(
             TelemetryEventName.PreviewOpenApi3,
             async (tel): Promise<ResultCode> => {
-              return await showOpenApi3(uri, context, client!, tel);
+              return await showOpenApi3(uri, context, tspLanguageClient!, tel);
             },
           );
         }),
@@ -220,12 +225,12 @@ export async function activate(context: ExtensionContext) {
               );
             };
             const tspClientStateToResultCode = () => {
-              return client && client.state === State.Running
+              return tspLanguageClient && tspLanguageClient.state === State.Running
                 ? ResultCode.Success
                 : ResultCode.Fail;
             };
             await startLspWithProgress();
-            if (client) {
+            if (tspLanguageClient) {
               ssTel.lastStep = "LSP client created (first try)";
               return tspClientStateToResultCode();
             }
@@ -285,7 +290,7 @@ export async function activate(context: ExtensionContext) {
 }
 
 export async function deactivate() {
-  await client?.stop();
+  await tspLanguageClient?.stop();
   await clearOpenApi3PreviewTempFolders();
 }
 
@@ -294,21 +299,21 @@ async function recreateLSPClient(
   activityId: string,
 ): Promise<Result<TspLanguageClient>> {
   logger.info("Recreating TypeSpec LSP server...");
-  const oldClient = client;
-  client = await TspLanguageClient.create(activityId, context, outputChannel);
+  const oldClient = tspLanguageClient;
+  setTspLanguageClient(await TspLanguageClient.create(activityId, context, outputChannel));
   await oldClient?.stop();
-  if (!client) {
+  if (!tspLanguageClient) {
     telemetryClient.logOperationDetailTelemetry(activityId, {
       error: "Failed to create TspLanguageClient",
     });
     return { code: ResultCode.Fail, details: "Failed to create TspLanguageClient." };
   } else {
-    await client.start(activityId);
-    if (client.state === State.Running) {
+    await tspLanguageClient.start(activityId);
+    if (tspLanguageClient.state === State.Running) {
       telemetryClient.logOperationDetailTelemetry(activityId, {
-        compilerVersion: client.initializeResult?.serverInfo?.version ?? "< 0.64.0",
+        compilerVersion: tspLanguageClient.initializeResult?.serverInfo?.version ?? "< 0.64.0",
       });
-      return { code: ResultCode.Success, value: client };
+      return { code: ResultCode.Success, value: tspLanguageClient };
     } else {
       telemetryClient.logOperationDetailTelemetry(activityId, {
         error: `Failed to start TspLanguageClient.`,
diff --git a/packages/typespec-vscode/src/tsp-language-client.ts b/packages/typespec-vscode/src/tsp-language-client.ts
index 6c52fc7ef1c..76b79b67b3c 100644
--- a/packages/typespec-vscode/src/tsp-language-client.ts
+++ b/packages/typespec-vscode/src/tsp-language-client.ts
@@ -1,13 +1,20 @@
 import type {
+  CompilerOptions,
   CustomRequestName,
   InitProjectConfig,
   InitProjectContext,
   InitProjectTemplate,
   ServerInitializeResult,
 } from "@typespec/compiler";
+import { InternalCompileResult } from "@typespec/compiler/internals";
 import { inspect } from "util";
 import { ExtensionContext, LogOutputChannel, RelativePattern, workspace } from "vscode";
-import { Executable, LanguageClient, LanguageClientOptions } from "vscode-languageclient/node.js";
+import {
+  Executable,
+  LanguageClient,
+  LanguageClientOptions,
+  TextDocumentIdentifier,
+} from "vscode-languageclient/node.js";
 import { TspConfigFileName } from "./const.js";
 import logger from "./log/logger.js";
 import telemetryClient from "./telemetry/telemetry-client.js";
@@ -88,6 +95,30 @@ export class TspLanguageClient {
     }
   }
 
+  public async compileProject(
+    doc: TextDocumentIdentifier,
+    options?: CompilerOptions,
+  ): Promise<InternalCompileResult | undefined> {
+    const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
+    try {
+      if (this.initializeResult?.customCapacities?.internalCompile !== true) {
+        logger.warning("Compile project is not supported by the current TypeSpec Compiler's LSP.");
+        return undefined;
+      }
+      const result = await this.client.sendRequest<InternalCompileResult>(
+        compileProjectRequestName,
+        {
+          doc: doc,
+          options: { ...options, dryRun: false },
+        },
+      );
+      return result;
+    } catch (e) {
+      logger.error("Unexpected error when compiling project", [e]);
+      return undefined;
+    }
+  }
+
   async runCliCommand(args: string[], cwd: string): Promise<ExecOutput | undefined> {
     if (isWhitespaceStringOrUndefined(this.initializeResult?.compilerCliJsPath)) {
       logger.warning(
diff --git a/packages/typespec-vscode/src/typespec-utils.ts b/packages/typespec-vscode/src/typespec-utils.ts
index 294ded69274..e8cb37a95ce 100644
--- a/packages/typespec-vscode/src/typespec-utils.ts
+++ b/packages/typespec-vscode/src/typespec-utils.ts
@@ -1,3 +1,5 @@
+import { SourceLocation } from "@typespec/compiler";
+import { ServerDiagnostic } from "@typespec/compiler/internals";
 import { readFile } from "fs/promises";
 import path from "path";
 import vscode from "vscode";
@@ -56,6 +58,25 @@ export async function TraverseMainTspFileInWorkspace() {
     );
 }
 
+export function formatDiagnostic(diagnostic: ServerDiagnostic): string {
+  const code = diagnostic.code ? ` ${diagnostic.code}` : "";
+  const content = code ? `${code}: ${diagnostic.message}` : diagnostic.message;
+  const root = diagnostic.target as SourceLocation & {
+    position?: { line: number; column: number };
+  };
+  if (root && root.file) {
+    const path = root.file.path;
+    const pos = root.pos ?? 0;
+    const formattedLocation = root.position
+      ? `${path}:${root.position.line}:${root.position.column}`
+      : `${path}:${pos}`;
+
+    return `${formattedLocation} - ${content}`;
+  } else {
+    return content;
+  }
+}
+
 /**
  * Install TypeSpec Compiler/CLI with UI interaction.
  *  - 'npm install -g @typespec/compiler' will be used if paths is empty or user choose "Global".
diff --git a/packages/typespec-vscode/src/utils.ts b/packages/typespec-vscode/src/utils.ts
index 0312ab0be5c..a91898646ff 100644
--- a/packages/typespec-vscode/src/utils.ts
+++ b/packages/typespec-vscode/src/utils.ts
@@ -2,7 +2,7 @@ import type { ModuleResolutionResult, PackageJson, ResolveModuleHost } from "@ty
 import { spawn, SpawnOptions } from "child_process";
 import { mkdtemp, readdir, readFile, realpath, stat } from "fs/promises";
 import { dirname } from "path";
-import { CancellationToken } from "vscode";
+import vscode, { CancellationToken } from "vscode";
 import { Executable } from "vscode-languageclient/node.js";
 import which from "which";
 import { parseDocument } from "yaml";
@@ -474,3 +474,8 @@ export function throttle<T extends (...args: any[]) => any>(fn: T, blockInMs: nu
     }
   } as T;
 }
+
+export function getVscodeUriFromPath(path: string): string {
+  const uri = vscode.Uri.file(path);
+  return uri.toString();
+}
diff --git a/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
index e28ce9f4280..dbfb66d6871 100644
--- a/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
+++ b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
@@ -3,9 +3,9 @@ import { readFile, writeFile } from "fs/promises";
 import path from "path";
 import { inspect } from "util";
 import vscode, { QuickInputButton, Uri } from "vscode";
-import { Executable } from "vscode-languageclient/node.js";
 import { Document, isScalar, isSeq } from "yaml";
 import { StartFileName, TspConfigFileName } from "../../const.js";
+import { tspLanguageClient } from "../../extension-context.js";
 import logger from "../../log/logger.js";
 import { InstallAction, npmDependencyType, NpmUtil } from "../../npm-utils.js";
 import { getDirectoryPath } from "../../path-utils.js";
@@ -13,11 +13,15 @@ import telemetryClient from "../../telemetry/telemetry-client.js";
 import { OperationTelemetryEvent } from "../../telemetry/telemetry-event.js";
 import { resolveTypeSpecCli } from "../../tsp-executable-resolver.js";
 import { ResultCode } from "../../types.js";
-import { getEntrypointTspFile, TraverseMainTspFileInWorkspace } from "../../typespec-utils.js";
+import {
+  formatDiagnostic,
+  getEntrypointTspFile,
+  TraverseMainTspFileInWorkspace,
+} from "../../typespec-utils.js";
 import {
   ExecOutput,
+  getVscodeUriFromPath,
   isFile,
-  spawnExecutionAndLogToOutput,
   tryParseYaml,
   tryReadFile,
 } from "../../utils.js";
@@ -444,18 +448,50 @@ async function doEmit(
             emitterVersion: e.version ?? (await npmUtil.loadNpmPackage(e.package))?.version,
           });
         });
+
         telemetryClient.logOperationDetailTelemetry(tel.activityId, {
           CompileStartTime: new Date().toISOString(), // ISO format: YYYY-MM-DDTHH:mm:ss.sssZ
         });
-        const compileResult = await compile(
-          cli,
-          mainTspFile,
-          emitters.map((e) => {
-            return { name: e.package, options: {} };
-          }),
-          false,
+        if (!tspLanguageClient) {
+          logger.error("LSP client is not started.");
+          logger.error(`Emitting ${codeInfoStr}...Failed.`, [], {
+            showOutput: true,
+            showPopup: true,
+          });
+          return ResultCode.Fail;
+        }
+        const compileResult = await tspLanguageClient.compileProject(
+          {
+            uri: getVscodeUriFromPath(mainTspFile),
+          },
+          { emit: emitters.map((e) => e.package) },
+        );
+        if (!compileResult) {
+          logger.error(`Emitting ${codeInfoStr}...Failed.`, [], {
+            showOutput: true,
+            showPopup: true,
+          });
+          return ResultCode.Fail;
+        }
+        const addSuffix = (count: number, suffix: string) =>
+          count > 1 ? `${count} ${suffix}s` : count === 1 ? `${count} ${suffix}` : undefined;
+        const warningDiagnostics = compileResult.diagnostics.filter(
+          (d) => d.severity === "warning",
         );
-        if (compileResult.exitCode !== 0) {
+        if (warningDiagnostics.length > 0) {
+          logger.warning(`Found ${addSuffix(warningDiagnostics.length, "warning")}`);
+          for (const diag of warningDiagnostics) {
+            logger.warning(formatDiagnostic(diag));
+          }
+        }
+        const errorDiagnostics = compileResult.diagnostics.filter((d) => d.severity === "error");
+        if (errorDiagnostics.length > 0) {
+          logger.error(`Found ${addSuffix(errorDiagnostics.length, "error")}`);
+          for (const diag of errorDiagnostics) {
+            logger.error(formatDiagnostic(diag));
+          }
+        }
+        if (compileResult.hasError) {
           logger.error(`Emitting ${codeInfoStr}...Failed`, [], {
             showOutput: true,
             showPopup: true,
@@ -761,32 +797,3 @@ export async function emitCode(
     }
   }
 }
-
-async function compile(
-  cli: Executable,
-  startFile: string,
-  emitters: { name: string; options: Record<string, string> }[],
-  logPretty?: boolean,
-): Promise<ExecOutput> {
-  const args: string[] = cli.args ?? [];
-  args.push("compile");
-  args.push(startFile);
-  if (emitters) {
-    for (const emitter of emitters) {
-      args.push("--emit", emitter.name);
-      if (emitter.options) {
-        for (const [key, value] of Object.entries(emitter.options)) {
-          args.push("--option", `${emitter.name}.${key}=${value}`);
-        }
-      }
-    }
-  }
-  if (logPretty !== undefined) {
-    args.push("--pretty");
-    args.push(logPretty ? "true" : "false");
-  }
-
-  return await spawnExecutionAndLogToOutput(cli.command, args, getDirectoryPath(startFile), {
-    NO_COLOR: "true",
-  });
-}


Reviews:
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T18:00:38Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T18:02:00Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T00:38:03Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:23:36Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:44:39Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:45:09Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:46:19Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:47:06Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:48:54Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:50:07Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:51:54Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:52:25Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:53:17Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:53:55Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T00:54:12Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T01:09:33Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T01:41:01Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:19:39Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T02:03:16Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T08:52:47Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T14:21:19Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T14:25:24Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:25:35Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T02:06:45Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T02:20:11Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:01:43Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:03:12Z
user: login: timotheeguerin
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T15:03:22Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:05:20Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:08:31Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:10:01Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:10:45Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:12:50Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:16:11Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:16:52Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:23:29Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:11:17Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:20:25Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:27:15Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T06:17:34Z
user: login: chunyu3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:11:40Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:25:02Z
user: login: RodgeFu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:26:41Z
user: login: lirenhe
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T03:45:28Z
user: login: lirenhe
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T06:02:16Z

Review comments:
diff_hunk: @@ -210,6 +210,7 @@ export {
 export {
   CompileResult,
   createServer,
+  CustomCompileResult,
path: packages/compiler/src/index.ts
user: login: timotheeguerin
body: this is currently too taylored for the typespec-vscode use case and doesn't make sense to be exposed as a core export. Move it to the internals until we have a universal solution for compiler stats
created_at: 2025-05-06T18:00:38Z
author_association: MEMBER
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: is this now logging all overt the place? This compile service is called a lot in the LSP
created_at: 2025-05-06T18:02:00Z
author_association: MEMBER
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: This is only logged when emit code. Other compiles such as compiling for authoring does not.
created_at: 2025-05-07T00:38:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -210,6 +210,7 @@ export {
 export {
   CompileResult,
   createServer,
+  CustomCompileResult,
path: packages/compiler/src/index.ts
user: login: chunyu3
body: @timotheeguerin thanks. move `CustomCompileResult` to internal export
created_at: 2025-05-07T01:23:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: whats the purposse of bypassCache?

thoses options should also be an option bag not boolean positional
created_at: 2025-05-07T15:44:39Z
author_association: MEMBER
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: I guess its more can't this be defined in the server host construction instead of injected here for some irrelevant behavior of the compile service?
created_at: 2025-05-07T15:45:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,63 @@
+import { StatusIcons } from "../core/logger/dynamic-task.js";
+import { TaskStatus, TrackActionTask } from "../core/types.js";
+import { ServerLog } from "./types.js";
+
+export class DynamicServerTask implements TrackActionTask {
+  #log: (log: ServerLog) => void;
+  #message: string;
+  #interval: NodeJS.Timeout | undefined;
path: packages/compiler/src/server/dynamic-server-task.ts
user: login: timotheeguerin
body: do you need this interval? seems like this task tracker is not really dynamic?
created_at: 2025-05-07T15:46:19Z
author_association: MEMBER
diff_hunk: @@ -367,6 +370,47 @@ export function createServer(host: ServerHost): Server {
     }
   }
 
+  async function compileProject(param: {
+    doc: TextDocumentIdentifier;
+    options: CompilerOptions;
+  }): Promise<CustomCompileResult> {
+    const option: CompilerOptions = {
+      ...param.options,
+    };
+
+    const result = await compileService.compile(param.doc, option, true, true);
+    if (result === undefined) {
+      return {
+        hasError: true,
+        errorDiagnostics: [
+          "Failed to get compiler result, please check the compilation output for details",
+        ],
+        entryPoint: undefined,
+        options: undefined,
+      };
+    } else {
+      let errorDiagnostics: string[] | undefined = undefined;
+      let warningDiagnostics: string[] | undefined = undefined;
+      if (result.program.diagnostics.length > 0) {
path: packages/compiler/src/server/serverlib.ts
user: login: timotheeguerin
body: can we not expose those as problems instead of logging to the output window which should be for debugging the extensiopn not for users to look at
created_at: 2025-05-07T15:47:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,8 @@
+---
+changeKind: feature
+packages:
+  - "@typespec/compiler"
+  - typespec-vscode
+---
+
+leverage lsp to emit code instead of cli
path: .chronus/changes/lsp-2025-4-6-10-5-13.md
user: login: timotheeguerin
body: Split this changelog in 2 for the 2 packages as its not a comment relevant to the compiler.

```
Use language server to compile project instead of CLI 
```

```
[LSP] Expose new compile project command
```
created_at: 2025-05-07T15:48:53Z
author_association: MEMBER
diff_hunk: @@ -70,6 +70,14 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface CustomCompileResult {
+  readonly hasError: boolean;
+  readonly warningDiagnostics?: string[];
+  readonly errorDiagnostics?: string[];
+  readonly entryPoint?: string;
path: packages/compiler/src/server/types.ts
user: login: timotheeguerin
body: ```suggestion
  readonly entrypoint?: string;
```
created_at: 2025-05-07T15:50:06Z
author_association: MEMBER
diff_hunk: @@ -70,6 +70,14 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface CustomCompileResult {
+  readonly hasError: boolean;
+  readonly warningDiagnostics?: string[];
path: packages/compiler/src/server/types.ts
user: login: timotheeguerin
body: why not returning the actual diagnostics.

This is way too specific for your current use case right now.

A reminder that adding this is now a new API that despite not exposing the type is now accessible to everybody.
created_at: 2025-05-07T15:51:54Z
author_association: MEMBER
diff_hunk: @@ -156,8 +168,8 @@ export interface SemanticToken {
 export type CustomRequestName =
   | "typespec/getInitProjectContext"
   | "typespec/initProject"
-  | "typespec/validateInitProjectTemplate";
-
+  | "typespec/validateInitProjectTemplate"
+  | "typespec/compileProject";
path: packages/compiler/src/server/types.ts
user: login: timotheeguerin
body: ```suggestion
  | "typespec/compile";
```

woudl just call that compile
created_at: 2025-05-07T15:52:24Z
author_association: MEMBER
diff_hunk: @@ -156,8 +168,8 @@ export interface SemanticToken {
 export type CustomRequestName =
   | "typespec/getInitProjectContext"
   | "typespec/initProject"
-  | "typespec/validateInitProjectTemplate";
-
+  | "typespec/validateInitProjectTemplate"
+  | "typespec/compileProject";
path: packages/compiler/src/server/types.ts
user: login: timotheeguerin
body: or `internal-compile` to aleviante concern about an api that isn't really meant for anybody else right now 
created_at: 2025-05-07T15:53:17Z
author_association: MEMBER
diff_hunk: @@ -53,3 +53,8 @@ export async function TraverseMainTspFileInWorkspace() {
         .map((uri) => normalizeSlashes(uri.fsPath)),
     );
 }
+
+export function GetVscodeUriFromPath(path: string) {
path: packages/typespec-vscode/src/typespec-utils.ts
user: login: timotheeguerin
body: camelCase


isn't that what the fileService already gives you too?
created_at: 2025-05-07T15:53:55Z
author_association: MEMBER
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: When there is a cache, and it is the same as before, compiling will complete after parse the tsp files and emitting will be cancelled. 
So for emit-code request, we will bypassCache
created_at: 2025-05-08T00:53:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: There will be only one `ServerHost` instance used across all scenarios, including authoring, hovering, and others. In these general scenarios, we do `not` log compile-time tracking information. Therefore, the `trackAction` will be `undefined` by default.

However, when handling the `emit-code` operation, we do need to log tracking data. In that case, we explicitly set `trackAction`and wrap the `ServerHost` in a new instance to ensure this logging does not interfere with other scenarios like authoring.
created_at: 2025-05-08T01:09:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,3 +53,8 @@ export async function TraverseMainTspFileInWorkspace() {
         .map((uri) => normalizeSlashes(uri.fsPath)),
     );
 }
+
+export function GetVscodeUriFromPath(path: string) {
path: packages/typespec-vscode/src/typespec-utils.ts
user: login: chunyu3
body: VscodeUri is encoded. fileService provide non-encoded one
e.g. 
fileService:    c:\\dev\\demoProject\\main.tsp
vscodeuri:     file:///c%3A/dev/demoProject/main.tsp

in lsp server cache, it will use vscodeUri as the key,
created_at: 2025-05-08T01:41:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+import { StatusIcons } from "../core/logger/dynamic-task.js";
+import { TaskStatus, TrackActionTask } from "../core/types.js";
+import { ServerLog } from "./types.js";
+
+export class DynamicServerTask implements TrackActionTask {
+  #log: (log: ServerLog) => void;
+  #message: string;
+  #interval: NodeJS.Timeout | undefined;
path: packages/compiler/src/server/dynamic-server-task.ts
user: login: chunyu3
body: removed
created_at: 2025-05-09T01:19:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,8 +168,8 @@ export interface SemanticToken {
 export type CustomRequestName =
   | "typespec/getInitProjectContext"
   | "typespec/initProject"
-  | "typespec/validateInitProjectTemplate";
-
+  | "typespec/validateInitProjectTemplate"
+  | "typespec/compileProject";
path: packages/compiler/src/server/types.ts
user: login: chunyu3
body: update to internalCompile
created_at: 2025-05-09T02:03:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,6 +70,14 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface CustomCompileResult {
+  readonly hasError: boolean;
+  readonly warningDiagnostics?: string[];
path: packages/compiler/src/server/types.ts
user: login: chunyu3
body: updated to return diagnotics object
created_at: 2025-05-09T08:52:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: i mean why do you need to bypass cache
created_at: 2025-05-12T14:21:19Z
author_association: MEMBER
diff_hunk: @@ -112,18 +118,25 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
+    trackAction: boolean = false,
   ): Promise<CompileResult | undefined> {
     const path = await fileService.getPath(document);
     const mainFile = await getMainFileForDocument(path);
     const config = await getConfig(mainFile);
     configFilePath = config.filename;
     log({ level: "debug", message: `config resolved`, detail: config });
-
-    const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: path });
+    const [optionsFromConfig, _] = resolveOptionsFromConfig(config, {
+      cwd: getDirectoryPath(path),
+    });
     const options: CompilerOptions = {
       ...optionsFromConfig,
       ...serverOptions,
     };
+    if (additionalOptions) {
+      Object.assign(options, additionalOptions);
+    }
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: ```suggestion
       ...additionalOptions
    };
```

or `...(additionalOptions ?? {})` is it complains about undefined
created_at: 2025-05-12T14:25:24Z
author_association: MEMBER
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: this feels like this maybe means you need to create a separate server host in that specific command instead, no?

This just feels complketely out of scope of this function to define a tracker. I'll also note that I am not sure there is a purpose to connect a tracker there when you are most likely not going to see it
created_at: 2025-05-12T16:25:34Z
author_association: MEMBER
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: Hello @timotheeguerin  when there is a program cache, and it is same as before, it will stop the whole compiling, the emitters will not be run, so the code is not emitted.

https://github.com/microsoft/typespec/blob/68dc81320736eb97a05cfd1b6d1f25f86598dffc/packages/compiler/src/core/program.ts#L265

and when CreateProgram return `shouldAbort:true`, the whole compiling will complete, the emitter is not run.
https://github.com/microsoft/typespec/blob/68dc81320736eb97a05cfd1b6d1f25f86598dffc/packages/compiler/src/core/program.ts#L173

But we need to emit code every time, so we need to bypass cache, in sure emitter is run everytime
created_at: 2025-05-13T02:06:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: hello @timotheeguerin There is only one compile service which has a single property `serverHost` and one property `compilerHost` which will service current all scenarios. 
```
export interface CompileServiceOptions {
  readonly fileSystemCache: FileSystemCache;
  readonly fileService: FileService;
  readonly serverHost: ServerHost;
  readonly compilerHost: CompilerHost;
  readonly log: (log: ServerLog) => void;
}
```
When `emit-code`, we need to log `trackAction` logs to IDE output, so we create a separate `compilerHost` for each emit-code request.
Currently I don't think we need to add a separate compilerHost `internalCompilerHost` and add it as a property of compile service as a global one. and switch to the `internalCompilerHost` when handle emit-code request.

Do you think we need to log the compile trackAction log to IDE? I think, we should, the log show what emitter is running and the result of each emitter.
When we have the result of emitter in compileResult, we may not need those trackAction. and IDE will retrieve every needed information from compile result and show in output
created_at: 2025-05-13T02:20:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: i see, can you just change the options to be an option bag then not positional booleans
created_at: 2025-05-13T15:01:43Z
author_association: MEMBER
diff_hunk: @@ -152,9 +164,43 @@ export function createCompileService({
       return undefined;
     }
 
+    async function trackActionFunc<T>(
+      message: string,
+      finalMessage: string,
+      asyncAction: (task: TrackActionTask) => Promise<T>,
+    ): Promise<T> {
+      const task = new DynamicServerTask(message, finalMessage, serverHost.log);
path: packages/compiler/src/server/compile-service.ts
user: login: timotheeguerin
body: I see, still feels like we doing things in the wrong place but this is all internal so we can clean that up later and reorganize.
created_at: 2025-05-13T15:03:12Z
author_association: MEMBER
diff_hunk: @@ -70,6 +77,17 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface ServerDiagnostic extends Diagnostic {
+  target: (SourceLocation & { position?: { line: number; column: number } }) | typeof NoTarget;
+}
+
+export interface CustomCompileResult {
path: packages/compiler/src/server/types.ts
user: login: RodgeFu
body: Better to call it InternalCompileResult to be consistent with the internalCompile()
created_at: 2025-05-14T03:05:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,7 +29,7 @@ import { importFromOpenApi3 } from "./vscode-cmd/import-from-openapi3.js";
 import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js";
 import { clearOpenApi3PreviewTempFolders, showOpenApi3 } from "./vscode-cmd/openapi3-preview.js";
 
-let client: TspLanguageClient | undefined;
+export let client: TspLanguageClient | undefined;
path: packages/typespec-vscode/src/extension.ts
user: login: RodgeFu
body: it's better to pass client to emit-code as parameter instead of through global export to avoid circle dependency when possible
created_at: 2025-05-14T03:08:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +95,23 @@ export class TspLanguageClient {
     }
   }
 
+  public async compileProject(
+    doc: TextDocumentIdentifier,
+    options?: CompilerOptions,
+  ): Promise<CustomCompileResult | undefined> {
+    const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
path: packages/typespec-vscode/src/tsp-language-client.ts
user: login: RodgeFu
body: Please add the 'internalCompile' in the servier's customCapacities and check it first. You can refer to other customRequest functions as example.
created_at: 2025-05-14T03:10:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +95,23 @@ export class TspLanguageClient {
     }
   }
 
+  public async compileProject(
+    doc: TextDocumentIdentifier,
+    options?: CompilerOptions,
+  ): Promise<CustomCompileResult | undefined> {
+    const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
+    try {
+      const result = await this.client.sendRequest<CustomCompileResult>(compileProjectRequestName, {
+        doc: doc,
+        options: { ...options, dryRun: false },
path: packages/typespec-vscode/src/tsp-language-client.ts
user: login: RodgeFu
body: why do we need to set the "dryRun" to false explicitly here?
created_at: 2025-05-14T03:10:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,3 +55,27 @@ export async function TraverseMainTspFileInWorkspace() {
         .map((uri) => normalizeSlashes(uri.fsPath)),
     );
 }
+
+export function getVscodeUriFromPath(path: string) {
path: packages/typespec-vscode/src/typespec-utils.ts
user: login: RodgeFu
body: Feels not that necessary considering it's just one line code. But if you do think it's useful, please move it to util instead of typespec-util which is not related to typespec.
created_at: 2025-05-14T03:12:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,18 +443,50 @@ async function doEmit(
             emitterVersion: e.version ?? (await npmUtil.loadNpmPackage(e.package))?.version,
           });
         });
+
         telemetryClient.logOperationDetailTelemetry(tel.activityId, {
           CompileStartTime: new Date().toISOString(), // ISO format: YYYY-MM-DDTHH:mm:ss.sssZ
         });
-        const compileResult = await compile(
-          cli,
-          mainTspFile,
-          emitters.map((e) => {
-            return { name: e.package, options: {} };
-          }),
-          false,
+        if (!client) {
+          logger.error("LSP client is not started.");
+          logger.error(`Emitting ${codeInfoStr}...Failed.`, [], {
+            showOutput: true,
+            showPopup: true,
+          });
path: packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
user: login: RodgeFu
body: set the lastStep for telemetry?
created_at: 2025-05-14T03:16:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,18 +443,50 @@ async function doEmit(
             emitterVersion: e.version ?? (await npmUtil.loadNpmPackage(e.package))?.version,
           });
         });
+
         telemetryClient.logOperationDetailTelemetry(tel.activityId, {
           CompileStartTime: new Date().toISOString(), // ISO format: YYYY-MM-DDTHH:mm:ss.sssZ
         });
-        const compileResult = await compile(
-          cli,
-          mainTspFile,
-          emitters.map((e) => {
-            return { name: e.package, options: {} };
-          }),
-          false,
+        if (!client) {
+          logger.error("LSP client is not started.");
+          logger.error(`Emitting ${codeInfoStr}...Failed.`, [], {
+            showOutput: true,
+            showPopup: true,
+          });
+          return ResultCode.Fail;
+        }
+        const compileResult = await client.compileProject(
+          {
+            uri: getVscodeUriFromPath(mainTspFile).toString(),
path: packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
user: login: RodgeFu
body: getVscodeUriFromPath() will return string already, right?
created_at: 2025-05-14T03:16:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +95,23 @@ export class TspLanguageClient {
     }
   }
 
+  public async compileProject(
+    doc: TextDocumentIdentifier,
+    options?: CompilerOptions,
+  ): Promise<CustomCompileResult | undefined> {
+    const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
+    try {
+      const result = await this.client.sendRequest<CustomCompileResult>(compileProjectRequestName, {
+        doc: doc,
+        options: { ...options, dryRun: false },
path: packages/typespec-vscode/src/tsp-language-client.ts
user: login: chunyu3
body: Yes. "dryRun" default is true. and if "dryRun" is true, the code will not emit.
created_at: 2025-05-14T03:23:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,18 +443,50 @@ async function doEmit(
             emitterVersion: e.version ?? (await npmUtil.loadNpmPackage(e.package))?.version,
           });
         });
+
         telemetryClient.logOperationDetailTelemetry(tel.activityId, {
           CompileStartTime: new Date().toISOString(), // ISO format: YYYY-MM-DDTHH:mm:ss.sssZ
         });
-        const compileResult = await compile(
-          cli,
-          mainTspFile,
-          emitters.map((e) => {
-            return { name: e.package, options: {} };
-          }),
-          false,
+        if (!client) {
+          logger.error("LSP client is not started.");
+          logger.error(`Emitting ${codeInfoStr}...Failed.`, [], {
+            showOutput: true,
+            showPopup: true,
+          });
path: packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
user: login: chunyu3
body: Already set lastStep above
created_at: 2025-05-14T05:11:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,6 +77,17 @@ export interface CompileResult {
   readonly optionsFromConfig: CompilerOptions;
 }
 
+export interface ServerDiagnostic extends Diagnostic {
+  target: (SourceLocation & { position?: { line: number; column: number } }) | typeof NoTarget;
+}
+
+export interface CustomCompileResult {
path: packages/compiler/src/server/types.ts
user: login: chunyu3
body: updated
created_at: 2025-05-14T05:20:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +95,23 @@ export class TspLanguageClient {
     }
   }
 
+  public async compileProject(
+    doc: TextDocumentIdentifier,
+    options?: CompilerOptions,
+  ): Promise<CustomCompileResult | undefined> {
+    const compileProjectRequestName: CustomRequestName = "typespec/internalCompile";
path: packages/typespec-vscode/src/tsp-language-client.ts
user: login: chunyu3
body: add
created_at: 2025-05-14T05:27:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,17 +119,22 @@ export function createCompileService({
    */
   async function compile(
     document: TextDocument | TextDocumentIdentifier,
+    additionalOptions?: CompilerOptions,
+    bypassCache: boolean = false,
path: packages/compiler/src/server/compile-service.ts
user: login: chunyu3
body: move those runtime options to option bag.
created_at: 2025-05-14T06:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,7 +29,7 @@ import { importFromOpenApi3 } from "./vscode-cmd/import-from-openapi3.js";
 import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js";
 import { clearOpenApi3PreviewTempFolders, showOpenApi3 } from "./vscode-cmd/openapi3-preview.js";
 
-let client: TspLanguageClient | undefined;
+export let client: TspLanguageClient | undefined;
path: packages/typespec-vscode/src/extension.ts
user: login: chunyu3
body: move client into another file to resolve circle dependency.
created_at: 2025-05-14T08:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+import { TspLanguageClient } from "./tsp-language-client";
+
+export let client: TspLanguageClient | undefined;
path: packages/typespec-vscode/src/extension-component.ts
user: login: RodgeFu
body: please rename it to tspLanguageClient. "client" is too general when exposed as global variable. Same to the function below.
created_at: 2025-05-14T10:25:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+import { TspLanguageClient } from "./tsp-language-client";
path: packages/typespec-vscode/src/extension-component.ts
user: login: RodgeFu
body: the filename "extension-component" is confusing. How about extension-context?
created_at: 2025-05-14T10:26:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-06T05:09:19Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@typespec/compiler`
 - :white_check_mark: `typespec-vscode`

<details>
<summary>Show changes</summary>

### `typespec-vscode` - _feature_ [✏️](https://github.com/chunyu3/typespec/edit/lsp/.chronus/changes/lsp-2025-4-9-9-38-32.md?pr=/microsoft/typespec/pull/7239)
> Use language server to compile project instead of CLI
### `@typespec/compiler` - _feature_ [✏️](https://github.com/chunyu3/typespec/edit/lsp/.chronus/changes/lsp-2025-4-9-9-38-5.md?pr=/microsoft/typespec/pull/7239)
> [LSP] Expose new compile project command
</details>

author_association: CONTRIBUTOR
user: login: azure-sdk
created_at: 2025-05-06T05:33:22Z
body: <!-- _TSP_TRYIT_COMMENT_ -->
You can try these changes here

| [🛝 Playground]( https://cadlplayground.z22.web.core.windows.net/prs/7239/)|[🌐 Website](https://tspwebsitepr.z22.web.core.windows.net/prs/7239/)|[🛝 VSCode Extension]( https://artprodcus3.artifacts.visualstudio.com/A0fb41ef4-5012-48a9-bf39-4ee3de03ee35/590cfd2a-581c-4dcb-a12e-6568ce786175/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL2F6dXJlLXNkay9wcm9qZWN0SWQvNTkwY2ZkMmEtNTgxYy00ZGNiLWExMmUtNjU2OGNlNzg2MTc1L2J1aWxkSWQvNDg2OTM3NC9hcnRpZmFjdE5hbWUvdnNjb2RlLWV4dGVuc2lvbg2/content?format=zip) |
| ---|---|--- |
author_association: COLLABORATOR
user: login: chunyu3
created_at: 2025-05-06T06:05:06Z
body: /azp run
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-05-06T06:05:45Z
body: <samp>
Azure Pipelines successfully started running 2 pipeline(s).<br>

</samp>
author_association: NONE