Skip to content

Leverage LSP to do compile instead of CLI #7239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
749bc60
implement compile request in server
chunyu3 Apr 20, 2025
11a6604
implement custom compile in compile server
chunyu3 Apr 25, 2025
432cc86
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 Apr 25, 2025
d3714ca
remove unused comment
chunyu3 Apr 25, 2025
c4b0d66
define trackAction log
chunyu3 Apr 25, 2025
32fc655
only trackAction when run custom compile
chunyu3 Apr 25, 2025
ecb8339
separate warning diagnostic and error diagnostic
chunyu3 Apr 27, 2025
5bfcf06
check the compile result
chunyu3 Apr 27, 2025
b7f5a38
reuse statusIcon from core
chunyu3 Apr 29, 2025
b890aac
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 6, 2025
c5c3ec0
add change log
chunyu3 May 6, 2025
50f0f56
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 6, 2025
64f5290
remove unused code
chunyu3 May 6, 2025
2a6cd2d
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 7, 2025
5aa5580
internal export CustomeCompileResult type
chunyu3 May 7, 2025
946dce2
Merge branch 'main' into lsp
chunyu3 May 7, 2025
9c10f23
Update packages/compiler/src/server/types.ts
chunyu3 May 8, 2025
2842198
resolve comments
chunyu3 May 8, 2025
5794426
resolve playground break issue
chunyu3 May 9, 2025
2e92ad2
split change log for each package
chunyu3 May 9, 2025
2a49fd5
resolve comment
chunyu3 May 9, 2025
1ed8d13
return diagnostic object instead of log string
chunyu3 May 9, 2025
a931e6b
return target position
chunyu3 May 9, 2025
4f61626
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 12, 2025
6334848
resolve comment
chunyu3 May 13, 2025
22cdef4
Merge branch 'main' into lsp
chunyu3 May 13, 2025
c85aa52
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 14, 2025
1c52f40
resolve comment
chunyu3 May 14, 2025
32808fe
Merge branch 'lsp' of https://github.com/chunyu3/typespec into lsp
chunyu3 May 14, 2025
a469699
add internalCompile capability
chunyu3 May 14, 2025
6393be9
define option bag for optional parameters
chunyu3 May 14, 2025
2188687
Merge branch 'main' of https://github.com/microsoft/typespec into lsp
chunyu3 May 14, 2025
2728043
resolve circle dependency
chunyu3 May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/lsp-2025-4-6-10-5-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
- typespec-vscode
---

leverage lsp to emit code instead of cli
2 changes: 1 addition & 1 deletion packages/compiler/src/core/logger/dynamic-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import isUnicodeSupported from "is-unicode-supported";
import pc from "picocolors";
import { TaskStatus, TrackActionTask } from "../types.js";

const StatusIcons = {
export const StatusIcons = {
success: pc.green("✔"),
failure: pc.red("×"),
warn: pc.yellow("⚠"),
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/internals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { CustomCompileResult } from "../server/index.js";
54 changes: 50 additions & 4 deletions packages/compiler/src/server/compile-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getDirectoryPath, joinPaths } from "../core/path-utils.js";
import { compile as compileProgram, Program } from "../core/program.js";
import type {
CompilerHost,
TrackActionTask,
Diagnostic as TypeSpecDiagnostic,
TypeSpecScriptNode,
} from "../core/types.js";
Expand All @@ -26,6 +27,7 @@ import { resolveTspMain } from "../utils/misc.js";
import { getLocationInYamlScript } from "../yaml/diagnostics.js";
import { parseYaml } from "../yaml/parser.js";
import { serverOptions } from "./constants.js";
import { DynamicServerTask } from "./dynamic-server-task.js";
import { FileService } from "./file-service.js";
import { FileSystemCache } from "./file-system-cache.js";
import { CompileResult, ServerHost, ServerLog } from "./types.js";
Expand All @@ -44,7 +46,12 @@ 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,
bypassCache?: boolean,
trackAction?: boolean,
): Promise<CompileResult | undefined>;

/**
* Load the AST for the given document.
Expand Down Expand Up @@ -112,17 +119,22 @@ 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,
...additionalOptions,
};
// add linter rule for unused using if user didn't configure it explicitly
const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
Expand Down Expand Up @@ -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);
task.start();

try {
const result = await asyncAction(task);
if (!task.isStopped) {
task.succeed();
}

return result;
} catch (error) {
task.fail(message);
throw error;
}
}
let program: Program;
try {
program = await compileProgram(compilerHost, mainFile, options, oldPrograms.get(mainFile));
program = await compileProgram(
trackAction
? {
...compilerHost,
logSink: {
log: compilerHost.logSink.log,
trackAction: (message, finalMessage, action) =>
trackActionFunc(message, finalMessage, action),
},
}
: compilerHost,
mainFile,
options,
bypassCache ? undefined : oldPrograms.get(mainFile),
);
oldPrograms.set(mainFile, program);
if (!fileService.upToDate(document)) {
return undefined;
Expand Down
63 changes: 63 additions & 0 deletions packages/compiler/src/server/dynamic-server-task.ts
Original file line number Diff line number Diff line change
@@ -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;
#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;
if (this.#interval) {
clearInterval(this.#interval);
this.#interval = undefined;
}
this.#log({
level: status !== "failure" ? "info" : "error",
message: `${StatusIcons[status]} ${this.#message}\n`,
});
}
}
1 change: 1 addition & 0 deletions packages/compiler/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { TypeSpecLanguageConfiguration } from "./language-config.js";
export { createServer } from "./serverlib.js";
export type {
CompileResult,
CustomCompileResult,
CustomRequestName,
InitProjectConfig,
InitProjectContext,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/compileProject";
connection.onRequest(compileProjectRequestName, profile(s.compileProject));

documents.onDidChangeContent(profile(s.checkChange));
documents.onDidClose(profile(s.documentClosed));
Expand Down
46 changes: 45 additions & 1 deletion packages/compiler/src/server/serverlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ import { getEntityName, getTypeName } from "../core/helpers/type-name-utils.js";
import { builtInLinterRule_UnusedTemplateParameter } from "../core/linter-rules/unused-template-parameter.rule.js";
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 { formatDiagnostic, 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 {
Expand Down Expand Up @@ -115,6 +116,7 @@ import {
} from "./type-details.js";
import {
CompileResult,
CustomCompileResult,
InitProjectConfig,
InitProjectContext,
SemanticTokenKind,
Expand Down Expand Up @@ -199,6 +201,7 @@ export function createServer(host: ServerHost): Server {
getInitProjectContext,
validateInitProjectTemplate,
initProject,
compileProject,
};

async function initialize(params: InitializeParams): Promise<InitializeResult> {
Expand Down Expand Up @@ -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) {
errorDiagnostics = result.program.diagnostics
.filter((diag) => diag.severity === "error")
.map((diagnostic) => formatDiagnostic(diagnostic, { pretty: false }));

warningDiagnostics = result.program.diagnostics
.filter((diag) => diag.severity === "warning")
.map((diagnostic) => formatDiagnostic(diagnostic, { pretty: false }));
}

return {
hasError: result.program.hasError(),
errorDiagnostics: errorDiagnostics,
warningDiagnostics: warningDiagnostics,
entryPoint: result.document?.uri,
options: result.program.compilerOptions,
};
}
}

async function renameFiles(params: RenameFilesParams): Promise<void> {
const firstFilePath = params.files[0];
if (!firstFilePath) {
Expand Down
16 changes: 14 additions & 2 deletions packages/compiler/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
readonly options?: CompilerOptions;
}

export interface Server {
readonly pendingMessages: readonly ServerLog[];
readonly workspaceFolders: readonly ServerWorkspaceFolder[];
Expand Down Expand Up @@ -106,6 +114,10 @@ export interface Server {
getInitProjectContext(): Promise<InitProjectContext>;
validateInitProjectTemplate(param: { template: InitTemplate }): Promise<boolean>;
initProject(param: { config: InitProjectConfig }): Promise<boolean>;
compileProject(param: {
doc: TextDocumentIdentifier;
options: CompilerOptions;
}): Promise<CustomCompileResult>;
}

export interface ServerSourceFile extends SourceFile {
Expand Down Expand Up @@ -156,8 +168,8 @@ export interface SemanticToken {
export type CustomRequestName =
| "typespec/getInitProjectContext"
| "typespec/initProject"
| "typespec/validateInitProjectTemplate";

| "typespec/validateInitProjectTemplate"
| "typespec/compileProject";
export interface ServerCustomCapacities {
getInitProjectContext?: boolean;
validateInitProjectTemplate?: boolean;
Expand Down
7 changes: 6 additions & 1 deletion packages/typespec-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
* 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
Expand All @@ -40,6 +40,11 @@ logger.registerLogListener("extension-log", new ExtensionLogListener(outputChann
export async function activate(context: ExtensionContext) {
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());
Expand Down
26 changes: 25 additions & 1 deletion packages/typespec-vscode/src/tsp-language-client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import type {
CompilerOptions,
CustomRequestName,
InitProjectConfig,
InitProjectContext,
InitProjectTemplate,
ServerInitializeResult,
} from "@typespec/compiler";
import { CustomCompileResult } 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";
Expand Down Expand Up @@ -88,6 +95,23 @@ export class TspLanguageClient {
}
}

public async compileProject(
doc: TextDocumentIdentifier,
options?: CompilerOptions,
): Promise<CustomCompileResult | undefined> {
const compileProjectRequestName: CustomRequestName = "typespec/compileProject";
try {
const result = await this.client.sendRequest<CustomCompileResult>(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(
Expand Down
5 changes: 5 additions & 0 deletions packages/typespec-vscode/src/typespec-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ export async function TraverseMainTspFileInWorkspace() {
.map((uri) => normalizeSlashes(uri.fsPath)),
);
}

export function GetVscodeUriFromPath(path: string) {
const uri = vscode.Uri.file(path);
return uri.toString();
}
Loading
Loading