Skip to content

Commit 34c4001

Browse files
committed
wire commands and tools through services
1 parent 55bcb66 commit 34c4001

9 files changed

Lines changed: 436 additions & 842 deletions

File tree

src/commands/checkModel.ts

Lines changed: 84 additions & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
1-
import { ChildProcess } from 'child_process';
2-
import * as path from 'path';
31
import * as vscode from 'vscode';
42
import { LANG_TLAPLUS, LANG_TLAPLUS_CFG } from '../common';
5-
import { applyDCollection } from '../diagnostic';
6-
import { ModelCheckResult, ModelCheckResultSource, SpecFiles } from '../model/check';
7-
import { ToolOutputChannel } from '../outputChannels';
8-
import { saveStreamToFile } from '../outputSaver';
9-
import {
10-
revealEmptyCheckResultView,
11-
revealLastCheckResultView,
12-
updateCheckResultView
13-
} from '../panels/checkResultView';
14-
import { TlcModelCheckerStdoutParser } from '../parsers/tlc';
15-
import { runTlc, stopProcess } from '../tla2tools';
16-
import { ModelResolveMode, resolveModelForUri } from './modelResolver';
17-
import { TlcCoverageDecorationProvider } from '../tlcCoverage';
18-
import { tlcTraceToPuml } from '../generators/tlcTraceToPuml';
19-
import { showSequenceDiagramFromPuml } from '../panels/sequenceDiagramView';
3+
import { SpecFiles } from '../model/check';
4+
import { ExtensionRuntime } from '../runtime/extensionRuntime';
5+
import { CheckSession } from '../services/checkService';
6+
import { CheckResultViewController } from '../panels/checkResultView';
7+
import { ModelResolveMode } from './modelResolver';
208

219
export const CMD_CHECK_MODEL_RUN = 'tlaplus.model.check.run';
2210
export const CMD_CHECK_MODEL_RUN_AGAIN = 'tlaplus.model.check.runAgain';
@@ -27,64 +15,51 @@ export const CMD_SHOW_TLC_OUTPUT = 'tlaplus.showTlcOutput';
2715
export const CTX_TLC_RUNNING = 'tlaplus.tlc.isRunning';
2816
export const CTX_TLC_CAN_RUN_AGAIN = 'tlaplus.tlc.canRunAgain';
2917

30-
const CFG_CREATE_OUT_FILES = 'tlaplus.tlc.modelChecker.createOutFiles';
31-
const CFG_SEQ_DIAGRAM_TRACE_VAR = 'tlaplus.tlc.sequenceDiagram.traceVariable';
32-
let checkProcess: ChildProcess | undefined;
33-
let lastCheckFiles: SpecFiles | undefined;
34-
let coverageProvider: TlcCoverageDecorationProvider | undefined;
35-
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
36-
export const outChannel = new ToolOutputChannel('TLC', mapTlcOutputLine);
37-
38-
class CheckResultHolder {
39-
checkResult: ModelCheckResult | undefined;
40-
}
41-
42-
/**
43-
* Sets the coverage provider to be used for visualization.
44-
*/
45-
export function setCoverageProvider(provider: TlcCoverageDecorationProvider): void {
46-
coverageProvider = provider;
47-
}
48-
49-
/**
50-
* Runs TLC on a TLA+ specification.
51-
*/
5218
export async function checkModel(
5319
fileUri: vscode.Uri | undefined,
54-
diagnostic: vscode.DiagnosticCollection,
55-
extContext: vscode.ExtensionContext
20+
runtime: ExtensionRuntime,
21+
extContext: vscode.ExtensionContext,
22+
checkResultView: CheckResultViewController
5623
): Promise<void> {
57-
const uri = fileUri ? fileUri : getActiveEditorFileUri(extContext);
24+
const uri = fileUri ? fileUri : getActiveEditorFileUri();
5825
if (!uri) {
5926
return;
6027
}
6128

62-
const specFiles = await getSpecFiles(uri, true);
29+
const specFiles = await runtime.checkService.resolveSpecFiles(uri, true);
6330
if (!specFiles) {
6431
return;
6532
}
66-
await doCheckModel(specFiles, true, extContext, diagnostic, true);
33+
34+
await runCheckSession(specFiles, runtime, extContext, checkResultView, {
35+
showOptionsPrompt: true,
36+
showFullOutput: true,
37+
}, true);
6738
}
6839

6940
export async function runLastCheckAgain(
70-
diagnostic: vscode.DiagnosticCollection,
71-
extContext: vscode.ExtensionContext
41+
runtime: ExtensionRuntime,
42+
extContext: vscode.ExtensionContext,
43+
checkResultView: CheckResultViewController
7244
): Promise<void> {
45+
const lastCheckFiles = runtime.checkService.sessions.lastSpecFiles();
7346
if (!lastCheckFiles) {
7447
vscode.window.showWarningMessage('No last check to run');
7548
return;
7649
}
77-
if (!canRunTlc(extContext)) {
78-
return;
79-
}
80-
await doCheckModel(lastCheckFiles, true, extContext, diagnostic, false);
50+
51+
await runCheckSession(lastCheckFiles, runtime, extContext, checkResultView, {
52+
showOptionsPrompt: false,
53+
showFullOutput: true,
54+
}, true);
8155
}
8256

8357
export async function checkModelCustom(
84-
diagnostic: vscode.DiagnosticCollection,
85-
extContext: vscode.ExtensionContext
58+
runtime: ExtensionRuntime,
59+
extContext: vscode.ExtensionContext,
60+
checkResultView: CheckResultViewController,
8661
): Promise<void> {
87-
const editor = getEditorIfCanRunTlc(extContext);
62+
const editor = getEditorIfCanRunTlc();
8863
if (!editor) {
8964
return;
9065
}
@@ -94,57 +69,40 @@ export async function checkModelCustom(
9469
'File in the active editor is not a .tla or .cfg, it cannot be checked as a model');
9570
return;
9671
}
97-
const specFiles = await getSpecFiles(doc.uri, true, true, 'customPick');
72+
const specFiles = await runtime.checkService.resolveSpecFiles(doc.uri, true, true, 'customPick');
9873
if (!specFiles) {
9974
return;
10075
}
101-
await doCheckModel(specFiles, true, extContext, diagnostic, true);
76+
77+
await runCheckSession(specFiles, runtime, extContext, checkResultView, {
78+
showOptionsPrompt: true,
79+
showFullOutput: true,
80+
}, true);
10281
}
10382

104-
/**
105-
* Reveals model checking view panel.
106-
*/
107-
export function displayModelChecking(extContext: vscode.ExtensionContext): void {
108-
revealLastCheckResultView(extContext);
83+
export function displayModelChecking(checkResultView: CheckResultViewController): void {
84+
checkResultView.revealLastResult();
10985
}
11086

111-
/**
112-
* Stops the current model checking process.
113-
*/
11487
export function stopModelChecking(
88+
runtime: ExtensionRuntime,
11589
terminateLastRun: (lastSpecFiles: SpecFiles | undefined) => boolean =
116-
(): boolean => { return true; },
117-
silent: boolean = false
90+
(): boolean => true,
91+
silent = false
11892
): void {
119-
if (checkProcess && terminateLastRun(lastCheckFiles)) {
120-
stopProcess(checkProcess);
121-
} else if (!silent) {
93+
const stopped = runtime.checkService.stopLatestSession(
94+
(session) => terminateLastRun(session.specFiles)
95+
);
96+
if (!stopped && !silent) {
12297
vscode.window.showInformationMessage("There're no currently running model checking processes");
12398
}
12499
}
125100

126-
export function showTlcOutput(): void {
127-
outChannel.revealWindow();
128-
}
129-
130-
function getActiveEditorFileUri(extContext: vscode.ExtensionContext): vscode.Uri | undefined {
131-
const editor = getEditorIfCanRunTlc(extContext);
132-
if (!editor) {
133-
return undefined;
134-
}
135-
const doc = editor.document;
136-
if (doc.languageId !== LANG_TLAPLUS && doc.languageId !== LANG_TLAPLUS_CFG) {
137-
vscode.window.showWarningMessage(
138-
'File in the active editor is not a .tla or .cfg file, it cannot be checked as a model');
139-
return undefined;
140-
}
141-
return doc.uri;
101+
export function showTlcOutput(runtime: ExtensionRuntime): void {
102+
runtime.checkService.revealOutput();
142103
}
143104

144-
export function getEditorIfCanRunTlc(extContext: vscode.ExtensionContext): vscode.TextEditor | undefined {
145-
if (!canRunTlc(extContext)) {
146-
return undefined;
147-
}
105+
export function getEditorIfCanRunTlc(): vscode.TextEditor | undefined {
148106
const editor = vscode.window.activeTextEditor;
149107
if (!editor) {
150108
vscode.window.showWarningMessage('No editor is active, cannot find a TLA+ model to check');
@@ -153,166 +111,55 @@ export function getEditorIfCanRunTlc(extContext: vscode.ExtensionContext): vscod
153111
return editor;
154112
}
155113

156-
function canRunTlc(extContext: vscode.ExtensionContext): boolean {
157-
if (checkProcess) {
158-
vscode.window.showWarningMessage(
159-
'Another model checking process is currently running',
160-
'Show currently running process'
161-
).then(() => revealLastCheckResultView(extContext));
162-
return false;
163-
}
164-
return true;
165-
}
166-
167-
export async function doCheckModel(
168-
specFiles: SpecFiles,
169-
showCheckResultView: boolean,
170-
extContext: vscode.ExtensionContext,
171-
diagnostic: vscode.DiagnosticCollection,
172-
showOptionsPrompt: boolean,
173-
extraOpts: string[] = [],
174-
debuggerPortCallback?: (port?: number) => void
175-
): Promise<ModelCheckResult | undefined> {
176-
try {
177-
const procInfo = await runTlc(
178-
specFiles.tlaFilePath, specFiles.cfgFilePath, showOptionsPrompt, extraOpts);
179-
if (procInfo === undefined) {
180-
// Command cancelled by user, make sure UI state is reset
181-
vscode.commands.executeCommand('setContext', CTX_TLC_CAN_RUN_AGAIN, !!lastCheckFiles);
182-
updateStatusBarItem(false, lastCheckFiles);
183-
return undefined;
184-
}
185-
lastCheckFiles = specFiles;
186-
vscode.commands.executeCommand('setContext', CTX_TLC_CAN_RUN_AGAIN, true);
187-
updateStatusBarItem(true, specFiles);
188-
outChannel.bindTo(procInfo);
189-
checkProcess = procInfo.process;
190-
checkProcess.on('close', () => {
191-
checkProcess = undefined;
192-
updateStatusBarItem(false, lastCheckFiles);
193-
});
194-
if (showCheckResultView) {
195-
attachFileSaver(specFiles, checkProcess);
196-
revealEmptyCheckResultView(extContext);
197-
}
198-
const resultHolder = new CheckResultHolder();
199-
const checkResultCallback = (checkResult: ModelCheckResult) => {
200-
resultHolder.checkResult = checkResult;
201-
if (showCheckResultView) {
202-
updateCheckResultView(checkResult);
203-
}
204-
205-
// Update coverage visualization
206-
if (coverageProvider && checkResult.coverageStat.length > 0) {
207-
// Get total distinct states from the last entry in initialStatesStat
208-
let totalDistinctStates = 0;
209-
if (checkResult.initialStatesStat.length > 0) {
210-
const lastStat = checkResult.initialStatesStat[checkResult.initialStatesStat.length - 1];
211-
totalDistinctStates = lastStat.distinct;
212-
}
213-
coverageProvider.updateCoverage(checkResult.coverageStat, totalDistinctStates);
214-
}
215-
};
216-
// Accumulate raw stdout for sequence-diagram generation
217-
const rawChunks: string[] = [];
218-
if (checkProcess.stdout) {
219-
checkProcess.stdout.on('data', (chunk: Buffer | string) => {
220-
rawChunks.push(String(chunk));
221-
});
222-
}
223-
const stdoutParser = new TlcModelCheckerStdoutParser(
224-
ModelCheckResultSource.Process,
225-
checkProcess.stdout,
226-
specFiles,
227-
true,
228-
checkResultCallback,
229-
debuggerPortCallback
230-
);
231-
const dCol = await stdoutParser.readAll();
232-
applyDCollection(dCol, diagnostic);
233-
// Auto-generate PlantUML sequence diagram from TLC error trace
234-
tryGenerateSequenceDiagram(rawChunks.join(''), specFiles, extContext);
235-
return resultHolder.checkResult;
236-
} catch (err) {
237-
statusBarItem.hide();
238-
vscode.window.showErrorMessage(`Error checking model: ${err}`);
239-
}
240-
return undefined;
241-
}
242-
243-
function attachFileSaver(specFiles: SpecFiles, proc: ChildProcess) {
244-
const createOutFiles = vscode.workspace.getConfiguration().get<boolean>(CFG_CREATE_OUT_FILES);
245-
if (typeof(createOutFiles) === 'undefined' || createOutFiles) {
246-
const outFilePath = path.join(specFiles.outputDir, `${specFiles.modelName}.out`);
247-
saveStreamToFile(proc.stdout, outFilePath);
248-
}
249-
}
250-
251-
/**
252-
* Finds all files that needed to run model check.
253-
*/
254-
export async function getSpecFiles(
114+
export async function resolveSpecFiles(
115+
runtime: ExtensionRuntime,
255116
fileUri: vscode.Uri,
256117
warn = true,
257118
interactive = true,
258119
mode: ModelResolveMode = 'adjacent'
259120
): Promise<SpecFiles | undefined> {
260-
const resolved = await resolveModelForUri(fileUri, warn, interactive, mode);
261-
if (!resolved) {
262-
return undefined;
263-
}
264-
return new SpecFiles(
265-
resolved.tlaPath,
266-
resolved.cfgPath,
267-
resolved.modelName,
268-
resolved.outputDir
269-
);
121+
return runtime.checkService.resolveSpecFiles(fileUri, warn, interactive, mode);
270122
}
271123

272-
function updateStatusBarItem(active: boolean, specFiles: SpecFiles | undefined) {
273-
statusBarItem.text = 'TLC' + (active ?
274-
(specFiles === undefined ? '' : ` (Model: ${specFiles.modelName})`)
275-
+ ' $(gear~spin)' : '');
276-
statusBarItem.tooltip = 'TLA+ model checking' + (active ?
277-
(specFiles === undefined ? '' : ` of model ${specFiles.modelName}`)
278-
+ ' is running' : ' result');
279-
statusBarItem.command = CMD_CHECK_MODEL_DISPLAY;
280-
statusBarItem.show();
281-
vscode.commands.executeCommand('setContext', CTX_TLC_RUNNING, active);
282-
}
124+
export async function runCheckSession(
125+
specFiles: SpecFiles,
126+
runtime: ExtensionRuntime,
127+
extContext: vscode.ExtensionContext,
128+
checkResultView: CheckResultViewController,
129+
request: {
130+
showOptionsPrompt: boolean;
131+
showFullOutput?: boolean;
132+
extraOpts?: string[];
133+
debuggerPortCallback?: (port?: number) => void;
134+
},
135+
showCheckResultView: boolean,
136+
): Promise<CheckSession | undefined> {
137+
if (showCheckResultView) {
138+
checkResultView.revealEmpty();
139+
}
283140

141+
const session = await runtime.checkService.startSession(specFiles, request);
142+
if (!session) {
143+
return undefined;
144+
}
284145

285-
export function mapTlcOutputLine(line: string): string | undefined {
286-
if (line === '') {
287-
return line;
146+
await session.completion;
147+
if (showCheckResultView) {
148+
await runtime.checkService.maybeGenerateSequenceDiagram(session, extContext);
288149
}
289-
const cleanLine = line.replace(/@!@!@(START|END)MSG \d+(:\d+)? @!@!@/g, '');
290-
return cleanLine === '' ? undefined : cleanLine;
150+
return session;
291151
}
292152

293-
/**
294-
* Attempts to generate a PlantUML sequence diagram from raw TLC output.
295-
* Errors are logged to the TLC output channel, never thrown.
296-
*/
297-
function tryGenerateSequenceDiagram(
298-
rawOutput: string,
299-
specFiles: SpecFiles,
300-
extContext: vscode.ExtensionContext,
301-
): void {
302-
try {
303-
const traceVar = vscode.workspace.getConfiguration()
304-
.get<string>(CFG_SEQ_DIAGRAM_TRACE_VAR) ?? '_seqDiagramTrace';
305-
const puml = tlcTraceToPuml(rawOutput, {
306-
traceVariable: traceVar,
307-
title: specFiles.modelName,
308-
});
309-
if (puml) {
310-
showSequenceDiagramFromPuml(puml, extContext, specFiles.modelName)
311-
.catch(err => {
312-
outChannel.appendLine(`[Sequence Diagram] Failed to open editor: ${err}`);
313-
});
314-
}
315-
} catch (err) {
316-
outChannel.appendLine(`[Sequence Diagram] Failed to generate: ${err}`);
153+
function getActiveEditorFileUri(): vscode.Uri | undefined {
154+
const editor = getEditorIfCanRunTlc();
155+
if (!editor) {
156+
return undefined;
157+
}
158+
const doc = editor.document;
159+
if (doc.languageId !== LANG_TLAPLUS && doc.languageId !== LANG_TLAPLUS_CFG) {
160+
vscode.window.showWarningMessage(
161+
'File in the active editor is not a .tla or .cfg file, it cannot be checked as a model');
162+
return undefined;
317163
}
164+
return doc.uri;
318165
}

0 commit comments

Comments
 (0)