addons/vscode/extension/vscodePlatform.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ServerPlatform} from 'isl-server/src/serverPlatform';
b69ab319import type {RepositoryContext} from 'isl-server/src/serverTypes';
b69ab3110import type {
b69ab3111 AbsolutePath,
b69ab3112 Diagnostic,
b69ab3113 DiagnosticSeverity,
b69ab3114 PlatformSpecificClientToServerMessages,
b69ab3115 RepoRelativePath,
b69ab3116 ServerToClientMessage,
b69ab3117} from 'isl/src/types';
b69ab3118import type {Json} from 'shared/typeUtils';
b69ab3119
b69ab3120import {Repository} from 'isl-server/src/Repository';
b69ab3121import type {CodeReviewIssue} from 'isl/src/firstPassCodeReview/types';
b69ab3122import {arraysEqual} from 'isl/src/utils';
b69ab3123import * as pathModule from 'node:path';
b69ab3124import * as vscode from 'vscode';
b69ab3125import {executeVSCodeCommand} from './commands';
b69ab3126import {PERSISTED_STORAGE_KEY_PREFIX} from './config';
b69ab3127import {t} from './i18n';
b69ab3128import {Internal} from './Internal';
b69ab3129import {openFileInRepo} from './openFile';
b69ab3130import {ActionTriggerType} from './types';
b69ab3131
b69ab3132export type VSCodeServerPlatform = ServerPlatform & {
b69ab3133 panelOrView: undefined | vscode.WebviewPanel | vscode.WebviewView;
b69ab3134};
b69ab3135
b69ab3136function diagnosticSeverity(severity: vscode.DiagnosticSeverity): DiagnosticSeverity {
b69ab3137 switch (severity) {
b69ab3138 case vscode.DiagnosticSeverity.Error:
b69ab3139 return 'error';
b69ab3140 case vscode.DiagnosticSeverity.Warning:
b69ab3141 return 'warning';
b69ab3142 case vscode.DiagnosticSeverity.Information:
b69ab3143 return 'info';
b69ab3144 case vscode.DiagnosticSeverity.Hint:
b69ab3145 return 'hint';
b69ab3146 }
b69ab3147}
b69ab3148
b69ab3149export const getVSCodePlatform = (context: vscode.ExtensionContext): VSCodeServerPlatform => ({
b69ab3150 platformName: 'vscode',
b69ab3151 sessionId: vscode.env.sessionId,
b69ab3152 panelOrView: undefined,
b69ab3153 async handleMessageFromClient(
b69ab3154 this: VSCodeServerPlatform,
b69ab3155 repo: Repository | undefined,
b69ab3156 ctx: RepositoryContext,
b69ab3157 message: PlatformSpecificClientToServerMessages,
b69ab3158 postMessage: (message: ServerToClientMessage) => void,
b69ab3159 onDispose: (cb: () => unknown) => void,
b69ab3160 ) {
b69ab3161 try {
b69ab3162 switch (message.type) {
b69ab3163 case 'platform/openFiles': {
b69ab3164 for (const path of message.paths) {
b69ab3165 if (repo == null) {
b69ab3166 return;
b69ab3167 }
b69ab3168 // don't use preview mode for opening multiple files, since they would overwrite each other
b69ab3169 openFileInRepo(
b69ab3170 repo,
b69ab3171 path,
b69ab3172 message.options?.line,
b69ab3173 /* preview */ false,
b69ab3174 /* onError */ (err: Error) => {
b69ab3175 // Opening multiple files at once can throw errors even when the files are successfully opened
b69ab3176 // We check here if the error is unwarranted and the file actually exists in the tab group
b69ab3177 const uri = vscode.Uri.file(pathModule.join(repo.info.repoRoot, path));
b69ab3178 const isTabOpen = vscode.window.tabGroups.all
b69ab3179 .flatMap(group => group.tabs)
b69ab3180 .some(
b69ab3181 tab =>
b69ab3182 tab.input instanceof vscode.TabInputText &&
b69ab3183 uri.fsPath == tab.input.uri.fsPath,
b69ab3184 );
b69ab3185
b69ab3186 if (!isTabOpen) {
b69ab3187 vscode.window.showErrorMessage(err.message ?? String(err));
b69ab3188 }
b69ab3189 },
b69ab3190 );
b69ab3191 }
b69ab3192 break;
b69ab3193 }
b69ab3194 case 'platform/openFile': {
b69ab3195 if (repo != null) {
b69ab3196 openFileInRepo(repo, message.path, message.options?.line, undefined);
b69ab3197 }
b69ab3198 break;
b69ab3199 }
b69ab31100 case 'platform/openDiff': {
b69ab31101 if (repo == null) {
b69ab31102 break;
b69ab31103 }
b69ab31104 const path: AbsolutePath = pathModule.join(repo.info.repoRoot, message.path);
b69ab31105 const uri = vscode.Uri.file(path);
b69ab31106 executeVSCodeCommand('sapling.open-file-diff', uri, message.comparison);
b69ab31107 break;
b69ab31108 }
b69ab31109 case 'platform/openExternal': {
b69ab31110 vscode.env.openExternal(vscode.Uri.parse(message.url));
b69ab31111 break;
b69ab31112 }
b69ab31113 case 'platform/changeTitle': {
b69ab31114 if (this.panelOrView != null) {
b69ab31115 this.panelOrView.title = message.title;
b69ab31116 }
b69ab31117 break;
b69ab31118 }
b69ab31119 case 'platform/checkForDiagnostics': {
b69ab31120 const diagnosticMap = new Map<RepoRelativePath, Array<Diagnostic>>();
b69ab31121 const repoRoot = repo?.info.repoRoot;
b69ab31122 if (repoRoot) {
b69ab31123 for (const path of message.paths) {
b69ab31124 const uri = vscode.Uri.file(pathModule.join(repoRoot, path));
b69ab31125 const diagnostics = vscode.languages.getDiagnostics(uri);
b69ab31126 if (diagnostics.length > 0) {
b69ab31127 diagnosticMap.set(
b69ab31128 path,
b69ab31129 diagnostics.map(diagnostic => ({
b69ab31130 message: diagnostic.message,
b69ab31131 range: {
b69ab31132 startLine: diagnostic.range.start.line,
b69ab31133 startCol: diagnostic.range.start.character,
b69ab31134 endLine: diagnostic.range.end.line,
b69ab31135 endCol: diagnostic.range.end.character,
b69ab31136 },
b69ab31137 severity: diagnosticSeverity(diagnostic.severity),
b69ab31138 source: diagnostic.source,
b69ab31139 code:
b69ab31140 typeof diagnostic.code === 'object'
b69ab31141 ? String(diagnostic.code.value)
b69ab31142 : String(diagnostic.code),
b69ab31143 })),
b69ab31144 );
b69ab31145 }
b69ab31146 }
b69ab31147 }
b69ab31148 postMessage({type: 'platform/gotDiagnostics', diagnostics: diagnosticMap});
b69ab31149 break;
b69ab31150 }
b69ab31151 case 'platform/confirm': {
b69ab31152 const OKButton = t('isl.confirmModalOK');
b69ab31153 const result = await vscode.window.showInformationMessage(
b69ab31154 message.message,
b69ab31155 {
b69ab31156 detail: message.details,
b69ab31157 modal: true,
b69ab31158 },
b69ab31159 OKButton,
b69ab31160 );
b69ab31161 postMessage({type: 'platform/confirmResult', result: result === OKButton});
b69ab31162 break;
b69ab31163 }
b69ab31164 case 'platform/subscribeToUnsavedFiles': {
b69ab31165 let previous: Array<{path: RepoRelativePath; uri: string}> = [];
b69ab31166 const postUnsavedFiles = () => {
b69ab31167 if (repo == null) {
b69ab31168 return;
b69ab31169 }
b69ab31170 const files = getUnsavedFiles(repo).map(document => {
b69ab31171 return {
b69ab31172 path: pathModule.relative(repo.info.repoRoot, document.fileName),
b69ab31173 uri: document.uri.toString(),
b69ab31174 };
b69ab31175 });
b69ab31176
b69ab31177 if (!arraysEqual(files, previous)) {
b69ab31178 postMessage({
b69ab31179 type: 'platform/unsavedFiles',
b69ab31180 unsaved: files,
b69ab31181 });
b69ab31182 previous = files;
b69ab31183 }
b69ab31184 };
b69ab31185
b69ab31186 const disposables = [
b69ab31187 vscode.workspace.onDidChangeTextDocument(postUnsavedFiles),
b69ab31188 vscode.workspace.onDidSaveTextDocument(postUnsavedFiles),
b69ab31189 ];
b69ab31190 postUnsavedFiles();
b69ab31191 onDispose(() => disposables.forEach(d => d.dispose()));
b69ab31192 break;
b69ab31193 }
b69ab31194 case 'platform/subscribeToSuggestedEdits': {
b69ab31195 const dispose = Internal.suggestedEdits?.onDidChangeSuggestedEdits(
b69ab31196 (files: Array<AbsolutePath>) => {
b69ab31197 postMessage({
b69ab31198 type: 'platform/onDidChangeSuggestedEdits',
b69ab31199 files,
b69ab31200 });
b69ab31201 },
b69ab31202 );
b69ab31203 onDispose(() => dispose?.dispose());
b69ab31204 break;
b69ab31205 }
b69ab31206 case 'platform/resolveSuggestedEdits': {
b69ab31207 Internal.suggestedEdits?.resolveSuggestedEdits(message.action, message.files);
b69ab31208 break;
b69ab31209 }
b69ab31210 case 'platform/saveAllUnsavedFiles': {
b69ab31211 if (repo == null) {
b69ab31212 return;
b69ab31213 }
b69ab31214 Promise.all(getUnsavedFiles(repo).map(doc => doc.save())).then(results => {
b69ab31215 postMessage({
b69ab31216 type: 'platform/savedAllUnsavedFiles',
b69ab31217 success: results.every(result => result),
b69ab31218 });
b69ab31219 });
b69ab31220 break;
b69ab31221 }
b69ab31222 case 'platform/subscribeToAvailableCwds': {
b69ab31223 const postAllAvailableCwds = async () => {
b69ab31224 const options = await Promise.all(
b69ab31225 (vscode.workspace.workspaceFolders ?? []).map(folder => {
b69ab31226 const cwd = folder.uri.fsPath;
b69ab31227 return Repository.getCwdInfo({...ctx, cwd});
b69ab31228 }),
b69ab31229 );
b69ab31230 postMessage({
b69ab31231 type: 'platform/availableCwds',
b69ab31232 options,
b69ab31233 });
b69ab31234 };
b69ab31235
b69ab31236 postAllAvailableCwds();
b69ab31237 const dispose = vscode.workspace.onDidChangeWorkspaceFolders(postAllAvailableCwds);
b69ab31238 onDispose(() => dispose.dispose());
b69ab31239 break;
b69ab31240 }
b69ab31241 case 'platform/setVSCodeConfig': {
b69ab31242 vscode.workspace
b69ab31243 .getConfiguration()
b69ab31244 .update(
b69ab31245 message.config,
b69ab31246 message.value,
b69ab31247 message.scope === 'global'
b69ab31248 ? vscode.ConfigurationTarget.Global
b69ab31249 : vscode.ConfigurationTarget.Workspace,
b69ab31250 );
b69ab31251 break;
b69ab31252 }
b69ab31253 case 'platform/setPersistedState': {
b69ab31254 const {key, data} = message;
b69ab31255 context.globalState.update(PERSISTED_STORAGE_KEY_PREFIX + key, data);
b69ab31256 break;
b69ab31257 }
b69ab31258 case 'platform/subscribeToVSCodeConfig': {
b69ab31259 const sendLatestValue = () =>
b69ab31260 postMessage({
b69ab31261 type: 'platform/vscodeConfigChanged',
b69ab31262 config: message.config,
b69ab31263 value: vscode.workspace.getConfiguration().get<Json>(message.config),
b69ab31264 });
b69ab31265 const dispose = vscode.workspace.onDidChangeConfiguration(e => {
b69ab31266 if (e.affectsConfiguration(message.config)) {
b69ab31267 sendLatestValue();
b69ab31268 }
b69ab31269 });
b69ab31270 sendLatestValue();
b69ab31271 onDispose(() => dispose.dispose());
b69ab31272 break;
b69ab31273 }
b69ab31274 case 'platform/executeVSCodeCommand': {
b69ab31275 vscode.commands.executeCommand(message.command, ...message.args);
b69ab31276 break;
b69ab31277 }
b69ab31278 case 'platform/resolveAllCommentsWithAI': {
b69ab31279 const {diffId, comments, filePaths, repoPath, userContext} = message;
b69ab31280 Internal.promptAIAgent?.(
b69ab31281 {type: 'resolveAllComments', diffId, comments, filePaths, repoPath, userContext},
b69ab31282 ActionTriggerType.ISL2SmartActions,
b69ab31283 );
b69ab31284 break;
b69ab31285 }
b69ab31286 case 'platform/resolveFailedSignalsWithAI': {
b69ab31287 const {diffId, diffVersionNumber, repoPath, userContext} = message;
b69ab31288 Internal.promptAIAgent?.(
b69ab31289 {type: 'resolveFailedSignals', diffId, diffVersionNumber, repoPath, userContext},
b69ab31290 ActionTriggerType.ISL2SmartActions,
b69ab31291 );
b69ab31292 break;
b69ab31293 }
b69ab31294 case 'platform/fillCommitMessageWithAI': {
b69ab31295 const {source, userContext} = message;
b69ab31296 Internal.promptAIAgent?.(
b69ab31297 {type: 'fillCommitMessage', userContext},
b69ab31298 source === 'commitInfoView'
b69ab31299 ? ActionTriggerType.ISL2CommitInfoView
b69ab31300 : ActionTriggerType.ISL2SmartActions,
b69ab31301 );
b69ab31302 break;
b69ab31303 }
b69ab31304 case 'platform/splitCommitWithAI': {
b69ab31305 const {diffCommit, args, repoPath, userContext} = message;
b69ab31306 Internal.promptAIAgent?.(
b69ab31307 {type: 'splitCommit', diffCommit, args, repoPath, userContext},
b69ab31308 ActionTriggerType.ISL2SplitCommit,
b69ab31309 );
b69ab31310 break;
b69ab31311 }
b69ab31312 case 'platform/createTestForModifiedCodeWithAI': {
b69ab31313 Internal.promptTestGeneration?.();
b69ab31314 break;
b69ab31315 }
b69ab31316 case 'platform/recommendTestPlanWithAI': {
b69ab31317 const {commitHash, userContext} = message;
b69ab31318 Internal.promptAIAgent?.(
b69ab31319 {type: 'recommendTestPlan', commitHash, userContext},
b69ab31320 ActionTriggerType.ISL2CommitInfoView,
b69ab31321 );
b69ab31322 break;
b69ab31323 }
b69ab31324 case 'platform/generateSummaryWithAI': {
b69ab31325 const {commitHash, userContext} = message;
b69ab31326 Internal.promptAIAgent?.(
b69ab31327 {type: 'generateSummary', commitHash, userContext},
b69ab31328 ActionTriggerType.ISL2CommitInfoView,
b69ab31329 );
b69ab31330 break;
b69ab31331 }
b69ab31332 case 'platform/validateChangesWithAI': {
b69ab31333 const {userContext} = message;
b69ab31334 Internal.promptAIAgent?.(
b69ab31335 {type: 'validateChanges', userContext},
b69ab31336 ActionTriggerType.ISL2SmartActions,
b69ab31337 );
b69ab31338 break;
b69ab31339 }
b69ab31340 case 'platform/resolveAllConflictsWithAI': {
b69ab31341 const {conflicts, userContext} = message;
b69ab31342 Internal.promptAIAgent?.(
b69ab31343 {type: 'resolveAllConflicts', conflicts, repository: repo, context: ctx, userContext},
b69ab31344 ActionTriggerType.ISL2MergeConflictView,
b69ab31345 );
b69ab31346 break;
b69ab31347 }
b69ab31348 case 'platform/runAICodeReviewPlatform': {
b69ab31349 const {cwd} = message;
b69ab31350 try {
b69ab31351 const results = await Internal.runAICodeReview?.(cwd);
b69ab31352 if (results != null) {
b69ab31353 const aiReviewCommentGroup = Internal.aiReviewCommentGroup?.();
b69ab31354 if (aiReviewCommentGroup == null) {
b69ab31355 break;
b69ab31356 }
b69ab31357 aiReviewCommentGroup.clearComments();
b69ab31358 for (const comment of [...results.values()].flat()) {
b69ab31359 aiReviewCommentGroup.addComment(
b69ab31360 comment.filepath,
b69ab31361 comment.startLine,
b69ab31362 comment.description,
b69ab31363 );
b69ab31364 }
b69ab31365 }
b69ab31366 } catch (err) {
b69ab31367 postMessage({
b69ab31368 type: 'platform/gotAIReviewComments',
b69ab31369 comments: {
b69ab31370 error: err as Error,
b69ab31371 },
b69ab31372 });
b69ab31373 }
b69ab31374 break;
b69ab31375 }
b69ab31376 case 'platform/runAICodeReviewChat': {
b69ab31377 const {source, reviewScope, userContext} = message;
b69ab31378 await Internal.promptAIAgent?.(
b69ab31379 {type: 'reviewCode', repoPath: repo?.info.repoRoot, reviewScope, userContext},
b69ab31380 source === 'commitInfoView'
b69ab31381 ? ActionTriggerType.ISL2CommitInfoView
b69ab31382 : ActionTriggerType.ISL2SmartActions,
b69ab31383 );
b69ab31384 break;
b69ab31385 }
b69ab31386 case 'platform/subscribeToAIReviewComments': {
b69ab31387 const aiReviewCommentGroup = Internal.aiReviewCommentGroup?.();
b69ab31388 if (aiReviewCommentGroup == null) {
b69ab31389 break;
b69ab31390 }
b69ab31391 aiReviewCommentGroup.onDidChangeComments(
b69ab31392 // Avoids importing FB-specific type
b69ab31393 // TODO: Should we just import the type?
b69ab31394 (
b69ab31395 comments: {
b69ab31396 comment: {
b69ab31397 id: string;
b69ab31398 filename: string;
b69ab31399 line: number;
b69ab31400 html: string;
b69ab31401 };
b69ab31402 }[],
b69ab31403 ) => {
b69ab31404 postMessage({
b69ab31405 type: 'platform/gotAIReviewComments',
b69ab31406 comments: {
b69ab31407 value: comments.map(
b69ab31408 (comment): CodeReviewIssue => ({
b69ab31409 issueID: comment.comment.id,
b69ab31410 filepath: comment.comment.filename,
b69ab31411 startLine: comment.comment.line,
b69ab31412 endLine: comment.comment.line, // TODO: get actual end line
b69ab31413 description: comment.comment.html,
b69ab31414 severity: 'medium', // TODO: get severity from comment
b69ab31415 }),
b69ab31416 ),
b69ab31417 },
b69ab31418 });
b69ab31419 },
b69ab31420 );
b69ab31421 break;
b69ab31422 }
b69ab31423 }
b69ab31424 } catch (err) {
b69ab31425 vscode.window.showErrorMessage(`error handling message ${JSON.stringify(message)}\n${err}`);
b69ab31426 }
b69ab31427 },
b69ab31428});
b69ab31429
b69ab31430function getUnsavedFiles(repo: Repository): Array<vscode.TextDocument> {
b69ab31431 return vscode.workspace.textDocuments.filter(
b69ab31432 document => document.isDirty && repo.isPathInsideRepo(document.fileName),
b69ab31433 );
b69ab31434}