Skip to content

Commit d2e1743

Browse files
authored
Merge pull request #325 from tlaplus/module-paths
Module search paths can be shared between tools.
2 parents 766ee98 + 5e032cf commit d2e1743

File tree

7 files changed

+216
-2
lines changed

7 files changed

+216
-2
lines changed

package.json

+11
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@
401401
"type": "boolean",
402402
"default": true,
403403
"description": "Mark proof step states using whole line."
404+
},
405+
"tlaplus.moduleSearchPaths": {
406+
"type": "array",
407+
"items": {"type": "string"},
408+
"description": "Paths to look for TLA+ modules.",
409+
"default": []
404410
}
405411
}
406412
},
@@ -489,6 +495,11 @@
489495
"id": "tlaplus.current-proof-step",
490496
"name": "Current Proof Step",
491497
"type": "webview"
498+
},
499+
{
500+
"id": "tlaplus.module-search-paths",
501+
"name": "Module Search Paths",
502+
"type": "tree"
492503
}
493504
]
494505
}

src/main.ts

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { TlaDocumentInfos } from './model/documentInfo';
2828
import { syncTlcStatisticsSetting, listenTlcStatConfigurationChanges } from './commands/tlcStatisticsCfg';
2929
import { TlapsClient } from './tlaps';
3030
import { CurrentProofStepWebviewViewProvider } from './panels/currentProofStepWebviewViewProvider';
31+
import { moduleSearchPaths } from './paths';
32+
import { ModuleSearchPathsTreeDataProvider } from './panels/moduleSearchPathsTreeDataProvider';
3133

3234
const TLAPLUS_FILE_SELECTOR: vscode.DocumentSelector = { scheme: 'file', language: LANG_TLAPLUS };
3335
const TLAPLUS_CFG_FILE_SELECTOR: vscode.DocumentSelector = { scheme: 'file', language: LANG_TLAPLUS_CFG };
@@ -44,6 +46,8 @@ let tlapsClient: TlapsClient | undefined;
4446
* Extension entry point.
4547
*/
4648
export function activate(context: vscode.ExtensionContext): void {
49+
moduleSearchPaths.setup(context);
50+
4751
const currentProofStepWebviewViewProvider = new CurrentProofStepWebviewViewProvider(context.extensionUri);
4852
diagnostic = vscode.languages.createDiagnosticCollection(LANG_TLAPLUS);
4953
context.subscriptions.push(
@@ -167,6 +171,10 @@ export function activate(context: vscode.ExtensionContext): void {
167171
vscode.window.registerWebviewViewProvider(
168172
CurrentProofStepWebviewViewProvider.viewType,
169173
currentProofStepWebviewViewProvider,
174+
),
175+
vscode.window.registerTreeDataProvider(
176+
ModuleSearchPathsTreeDataProvider.viewType,
177+
new ModuleSearchPathsTreeDataProvider(context)
170178
)
171179
);
172180
tlapsClient = new TlapsClient(context, details => {

src/model/paths.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface InitRequestInItializationOptions {
2+
moduleSearchPaths: string[] | null | undefined
3+
}
4+
5+
export interface InitResponseCapabilitiesExperimental {
6+
moduleSearchPaths: string[] | null | undefined
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as vscode from 'vscode';
2+
import { moduleSearchPaths, ModuleSearchPathSource } from '../paths';
3+
4+
const sourceIcon = new vscode.ThemeIcon('folder-library');
5+
const zipDirIcon = new vscode.ThemeIcon('file-zip');
6+
const folderIcon = new vscode.ThemeIcon('folder');
7+
8+
class mspSource extends vscode.TreeItem {
9+
level = 'source';
10+
constructor(
11+
public source: ModuleSearchPathSource
12+
) {
13+
super(source.description, vscode.TreeItemCollapsibleState.Expanded);
14+
}
15+
iconPath = sourceIcon;
16+
}
17+
18+
class mspPath extends vscode.TreeItem {
19+
level = 'path';
20+
constructor(
21+
path: string
22+
) {
23+
super(path, vscode.TreeItemCollapsibleState.None);
24+
this.iconPath = path.startsWith('jar://') ? zipDirIcon : folderIcon;
25+
}
26+
}
27+
28+
type msp = mspSource | mspPath
29+
30+
export class ModuleSearchPathsTreeDataProvider implements vscode.TreeDataProvider<msp> {
31+
static readonly viewType = 'tlaplus.module-search-paths';
32+
private _onDidChangeTreeData = new vscode.EventEmitter<msp | msp[] | undefined | null | void>();
33+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
34+
35+
constructor(
36+
private context: vscode.ExtensionContext
37+
) {
38+
context.subscriptions.push(moduleSearchPaths.updates(_ => {
39+
this._onDidChangeTreeData.fire();
40+
}));
41+
}
42+
43+
getChildren(element?: msp): Thenable<msp[]> {
44+
if (!element) {
45+
return Promise.resolve(
46+
moduleSearchPaths.getSources().map(s => new mspSource(s))
47+
);
48+
}
49+
if (element.level === 'source') {
50+
return Promise.resolve(
51+
moduleSearchPaths.getSourcePaths((element as mspSource).source.name).map(p => new mspPath(p))
52+
);
53+
}
54+
return Promise.resolve([]);
55+
}
56+
57+
getTreeItem(element: msp): vscode.TreeItem {
58+
return element;
59+
}
60+
}

src/paths.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as vscode from 'vscode';
2+
import * as tla2tools from './tla2tools';
3+
4+
export const TLAPS = 'TLAPS';
5+
export const TLC = 'TLC';
6+
export const CFG = 'CFG';
7+
8+
export interface ModuleSearchPathSource {
9+
name: string;
10+
description: string;
11+
}
12+
13+
class ModuleSearchPaths {
14+
private priority = [CFG, TLC, TLAPS];
15+
private sources: { [source: string]: string } = {};
16+
private paths: { [source: string]: string[] } = {};
17+
18+
private _updates = new vscode.EventEmitter<void>();
19+
readonly updates = this._updates.event;
20+
21+
public setup(context: vscode.ExtensionContext) {
22+
this.setSourcePaths(TLC, 'TLC Model Checker', tla2tools.moduleSearchPaths());
23+
const fromCfg = () => {
24+
const config = vscode.workspace.getConfiguration();
25+
const cfgPaths = config.get<string[]>('tlaplus.moduleSearchPaths');
26+
this.setSourcePaths(CFG, 'Configured Paths', cfgPaths ? cfgPaths : []);
27+
};
28+
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => {
29+
if (e.affectsConfiguration('tlaplus.moduleSearchPaths')) {
30+
fromCfg();
31+
vscode.commands.executeCommand(
32+
'tlaplus.tlaps.moduleSearchPaths.updated.lsp',
33+
...this.getOtherPaths(TLAPS)
34+
);
35+
}
36+
}));
37+
fromCfg();
38+
}
39+
40+
// Order first by the defined priority, then all the remaining alphabetically, just to be predictable.
41+
private sourceOrder(): string[] {
42+
const sourceNames = Object.keys(this.sources);
43+
const ordered: string[] = [];
44+
this.priority.forEach(sn => {
45+
if (sourceNames.includes(sn)) {
46+
ordered.push(sn);
47+
}
48+
});
49+
const other = sourceNames.filter(sn => !this.priority.includes(sn));
50+
other.sort();
51+
ordered.push(...other);
52+
return ordered;
53+
}
54+
55+
public getSources(): ModuleSearchPathSource[] {
56+
return this.sourceOrder().map<ModuleSearchPathSource>(sn => {
57+
return {
58+
name: sn,
59+
description: this.sources[sn]
60+
};
61+
});
62+
}
63+
64+
public getSourcePaths(source: string): string[] {
65+
return this.paths[source];
66+
}
67+
68+
// Return all the paths except the specified source.
69+
public getOtherPaths(source: string): string[] {
70+
return this.sourceOrder().reduce<string[]>((acc, s) => {
71+
if (s !== source) {
72+
acc.push(... this.paths[s]);
73+
}
74+
return acc;
75+
}, []);
76+
}
77+
78+
public setSourcePaths(source: string, description: string, paths: string[]) {
79+
this.sources[source] = description;
80+
this.paths[source] = paths;
81+
this._updates.fire();
82+
}
83+
}
84+
85+
export const moduleSearchPaths = new ModuleSearchPaths();

src/tla2tools.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as cp from 'child_process';
22
import { ChildProcess, spawn } from 'child_process';
33
import * as fs from 'fs';
44
import * as path from 'path';
5+
import * as paths from './paths';
56
import * as vscode from 'vscode';
67
import { CFG_TLC_STATISTICS_TYPE, ShareOption } from './commands/tlcStatisticsCfg';
78
import { pathToUri } from './common';
@@ -24,12 +25,15 @@ const NO_ERROR = 0;
2425
const MIN_TLA_ERROR = 10; // Exit codes not related to tooling start from this number
2526
const LOWEST_JAVA_VERSION = 8;
2627
const DEFAULT_GC_OPTION = '-XX:+UseParallelGC';
28+
const TLA_CMODS_LIB_NAME = 'CommunityModules-deps.jar';
2729
const TLA_TOOLS_LIB_NAME = 'tla2tools.jar';
2830
const TLA_TOOLS_LIB_NAME_END_UNIX = '/' + TLA_TOOLS_LIB_NAME;
2931
const TLA_TOOLS_LIB_NAME_END_WIN = '\\' + TLA_TOOLS_LIB_NAME;
3032
const toolsJarPath = path.resolve(__dirname, '../tools/' + TLA_TOOLS_LIB_NAME);
33+
const cmodsJarPath = path.resolve(__dirname, '../tools/' + TLA_CMODS_LIB_NAME);
3134
const javaCmd = 'java' + (process.platform === 'win32' ? '.exe' : '');
3235
const javaVersionChannel = new ToolOutputChannel('TLA+ Java version');
36+
const TLA_TOOLS_STANDARD_MODULES = '/tla2sany/StandardModules';
3337

3438
let lastUsedJavaHome: string | undefined;
3539
let cachedJavaPath: string | undefined;
@@ -69,6 +73,14 @@ export class JavaVersion {
6973
) {}
7074
}
7175

76+
function makeTlaLibraryJavaOpt(): string {
77+
const libPaths = paths.moduleSearchPaths.
78+
getOtherPaths(paths.TLC).
79+
filter(p => !p.startsWith('jar:')). // TODO: Support archive paths as well.
80+
join(path.delimiter);
81+
return '-DTLA-Library=' + libPaths;
82+
}
83+
7284
export async function runPlusCal(tlaFilePath: string): Promise<ToolProcessInfo> {
7385
const customOptions = getConfigOptions(CFG_PLUSCAL_OPTIONS);
7486
return runTool(
@@ -84,7 +96,7 @@ export async function runSany(tlaFilePath: string): Promise<ToolProcessInfo> {
8496
TlaTool.SANY,
8597
tlaFilePath,
8698
[ path.basename(tlaFilePath) ],
87-
[]
99+
[ makeTlaLibraryJavaOpt() ]
88100
);
89101
}
90102

@@ -116,7 +128,7 @@ export async function runTlc(
116128
return undefined;
117129
}
118130
const customOptions = extraOpts.concat(promptedOptions);
119-
const javaOptions = [];
131+
const javaOptions = [ makeTlaLibraryJavaOpt() ];
120132
const shareStats = vscode.workspace.getConfiguration().get<ShareOption>(CFG_TLC_STATISTICS_TYPE);
121133
if (shareStats !== ShareOption.DoNotShare) {
122134
javaOptions.push('-Dtlc2.TLC.ide=vscode');
@@ -136,6 +148,7 @@ async function runTool(
136148
javaOptions: string[]
137149
): Promise<ToolProcessInfo> {
138150
const javaPath = await obtainJavaPath();
151+
// TODO: Merge cfgOptions with javaOptions to avoid complete overrides.
139152
const cfgOptions = getConfigOptions(CFG_JAVA_OPTIONS);
140153
const args = buildJavaOptions(cfgOptions, toolsJarPath).concat(javaOptions);
141154
args.push(toolName);
@@ -145,6 +158,13 @@ async function runTool(
145158
return new ToolProcessInfo(buildCommandLine(javaPath, args), proc);
146159
}
147160

161+
export function moduleSearchPaths(): string[] {
162+
return [
163+
'jar:file:' + toolsJarPath + '!' + TLA_TOOLS_STANDARD_MODULES,
164+
'jar:file:' + cmodsJarPath + '!' + '/'
165+
];
166+
}
167+
148168
/**
149169
* Kills the given process.
150170
*/

src/tlaps.ts

+23
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@ import {
1515
LanguageClient,
1616
LanguageClientOptions,
1717
Range,
18+
State,
19+
StateChangeEvent,
1820
TextDocumentIdentifier,
1921
TransportKind,
2022
VersionedTextDocumentIdentifier
2123
} from 'vscode-languageclient/node';
2224
import { TlapsProofStepDetails } from './model/tlaps';
2325
import { DelayedFn } from './common';
26+
import {
27+
InitRequestInItializationOptions,
28+
InitResponseCapabilitiesExperimental,
29+
} from './model/paths';
30+
import { moduleSearchPaths, TLAPS } from './paths';
2431

2532
export enum proofStateNames {
2633
proved = 'proved',
@@ -186,6 +193,9 @@ export class TlapsClient {
186193
};
187194
const clientOptions: LanguageClientOptions = {
188195
documentSelector: [{ scheme: 'file', language: 'tlaplus' }],
196+
initializationOptions: {
197+
moduleSearchPaths: moduleSearchPaths.getOtherPaths(TLAPS)
198+
} as InitRequestInItializationOptions
189199
};
190200
this.client = new LanguageClient(
191201
'tlaplus.tlaps.lsp',
@@ -202,6 +212,19 @@ export class TlapsClient {
202212
'tlaplus/tlaps/currentProofStep',
203213
this.currentProofStepDetailsListener
204214
));
215+
this.context.subscriptions.push(this.client.onDidChangeState((e: StateChangeEvent) => {
216+
if (e.oldState !== State.Running && e.newState === State.Running && this.client) {
217+
const initResult = this.client.initializeResult;
218+
if (!initResult || !initResult.capabilities.experimental) {
219+
return;
220+
}
221+
const experimental = initResult.capabilities.experimental as InitResponseCapabilitiesExperimental;
222+
if (!experimental.moduleSearchPaths) {
223+
return;
224+
}
225+
moduleSearchPaths.setSourcePaths(TLAPS, 'TLA Proof System', experimental.moduleSearchPaths);
226+
}
227+
}));
205228
this.client.start();
206229
}
207230

0 commit comments

Comments
 (0)