Skip to content

Commit e581b07

Browse files
committed
Add java.import.mode setting for on-demand project import
Adds the `java.import.mode` setting with "full" (default) and "ondemand" values. In on-demand mode, projects are imported lazily when a Java file is opened instead of importing everything at startup. The import mode is also passed as a JVM arg (-Djdt.ls.importMode) so JDT-LS can read it early in plugin startup, before LSP initializationOptions are available. This allows skipping waitForProjectRegistryRefreshJob in on-demand mode to avoid refreshing stale projects from a previous crashed session. Signed-off-by: azerr <azerr@redhat.com>
1 parent 4e49f18 commit e581b07

4 files changed

Lines changed: 60 additions & 22 deletions

File tree

‎package.json‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,21 @@
503503
"title": "Project Import/Update",
504504
"order": 20,
505505
"properties": {
506+
"java.import.mode": {
507+
"type": "string",
508+
"enum": [
509+
"full",
510+
"ondemand"
511+
],
512+
"enumDescriptions": [
513+
"All projects are imported at startup.",
514+
"Projects are imported lazily when a file is opened."
515+
],
516+
"default": "full",
517+
"markdownDescription": "Controls how projects are imported into the workspace.\n- `full`: all projects are discovered and imported at startup (default). Safe and well-tested, but slow for large workspaces.\n- `ondemand`: projects are imported lazily when the user first opens a file belonging to that module. This drastically reduces startup time and memory usage for large multi-module workspaces (e.g. Quarkus with 1300+ modules).",
518+
"scope": "window",
519+
"order": 5
520+
},
506521
"java.import.projectSelection": {
507522
"type": "string",
508523
"enum": [

‎src/extension.ts‎

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -758,28 +758,34 @@ async function startStandardServer(
758758
}
759759

760760
async function doStartStandardServer(context: ExtensionContext, requirements: requirements.RequirementsData, clientOptions: LanguageClientOptions, workspacePath: string, triggeredByCommand: boolean = false): Promise<LanguageClient | undefined> {
761-
const selector: BuildFileSelector = new BuildFileSelector(context, []);
762-
const importMode: ImportMode = await getImportMode(context, selector);
763-
if (importMode === ImportMode.automatic) {
764-
if (!await ensureNoBuildToolConflicts(context, clientOptions)) {
765-
return undefined;
766-
}
767-
} else {
768-
const buildFiles: string[] = [];
769-
if (importMode === ImportMode.manual) {
770-
const cache = context.workspaceState.get<string[]>(PICKED_BUILD_FILES);
771-
if (cache === undefined || cache.length === 0 && triggeredByCommand) {
772-
buildFiles.push(...await selector.selectBuildFiles() || []);
773-
} else {
774-
buildFiles.push(...cache);
761+
// In on-demand mode, the build tool is determined by the file being opened
762+
// (each importer tries its own build file lookup), so no upfront project
763+
// selection or build tool conflict prompt is needed.
764+
const javaImportMode = getJavaConfiguration().get<string>("import.mode", "full");
765+
if (javaImportMode !== "ondemand") {
766+
const selector: BuildFileSelector = new BuildFileSelector(context, []);
767+
const importMode: ImportMode = await getImportMode(context, selector);
768+
if (importMode === ImportMode.automatic) {
769+
if (!await ensureNoBuildToolConflicts(context, clientOptions)) {
770+
return undefined;
775771
}
772+
} else {
773+
const buildFiles: string[] = [];
774+
if (importMode === ImportMode.manual) {
775+
const cache = context.workspaceState.get<string[]>(PICKED_BUILD_FILES);
776+
if (cache === undefined || cache.length === 0 && triggeredByCommand) {
777+
buildFiles.push(...await selector.selectBuildFiles() || []);
778+
} else {
779+
buildFiles.push(...cache);
780+
}
781+
}
782+
if (buildFiles.length === 0) {
783+
commands.executeCommand('setContext', 'java:serverMode', ServerMode.lightWeight);
784+
serverStatusBarProvider.showNotImportedStatus();
785+
return undefined;
786+
}
787+
clientOptions.initializationOptions.projectConfigurations = buildFiles;
776788
}
777-
if (buildFiles.length === 0) {
778-
commands.executeCommand('setContext', 'java:serverMode', ServerMode.lightWeight);
779-
serverStatusBarProvider.showNotImportedStatus();
780-
return undefined;
781-
}
782-
clientOptions.initializationOptions.projectConfigurations = buildFiles;
783789
}
784790

785791
if (apiManager.getApiInstance().serverMode === ServerMode.lightWeight) {
@@ -789,7 +795,13 @@ async function doStartStandardServer(context: ExtensionContext, requirements: re
789795
}
790796
await standardClient.initialize(context, requirements, clientOptions, workspacePath, jdtEventEmitter);
791797
standardClient.start().then(async () => {
792-
standardClient.registerLanguageClientActions(context, await fse.pathExists(path.join(workspacePath, ".metadata", ".plugins")), jdtEventEmitter);
798+
// Register handlers immediately (no await before) so they are ready
799+
// when the server sends synchronizeBundles and ServiceReady right after 'initialized'.
800+
// In on-demand mode with no Java file open, zero projects are loaded at startup,
801+
// so the server reaches synchronizeBundles so fast that an await here would cause
802+
// the ExecuteClientCommandRequest handler to not be registered in time.
803+
const hasImportedPromise = fse.pathExists(path.join(workspacePath, ".metadata", ".plugins"));
804+
standardClient.registerLanguageClientActions(context, hasImportedPromise, jdtEventEmitter);
793805
// Send auto-detected JDKs after server startup to avoid blocking initialization
794806
standardClient.getClient().sendNotification(DidChangeConfigurationNotification.type, {
795807
settings: {

‎src/javaServerStarter.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,16 @@ function prepareParams(requirements: RequirementsData, workspacePath, context: E
266266
if (vmargs.indexOf('-DDetectVMInstallationsJob.disabled=') < 0) {
267267
params.push('-DDetectVMInstallationsJob.disabled=true');
268268
}
269+
// Pass import mode as JVM arg so JDT-LS can read it early in plugin startup,
270+
// before LSP initializationOptions are available. This allows skipping
271+
// waitForProjectRegistryRefreshJob in on-demand mode to avoid refreshing
272+
// stale projects from a previous crashed session.
273+
if (vmargs.indexOf('-Djdt.ls.importMode=') < 0) {
274+
const importMode = getJavaConfiguration().get<string>('import.mode', 'full');
275+
if (importMode !== 'full') {
276+
params.push(`-Djdt.ls.importMode=${importMode}`);
277+
}
278+
}
269279
const encodingKey = '-Dfile.encoding=';
270280
if (vmargs.indexOf(encodingKey) < 0) {
271281
params.push(encodingKey + getJavaEncoding());

‎src/standardLanguageClient.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class StandardLanguageClient {
118118
this.status = ClientStatus.initialized;
119119
}
120120

121-
public registerLanguageClientActions(context: ExtensionContext, hasImported: boolean, jdtEventEmitter: EventEmitter<Uri>) {
121+
public registerLanguageClientActions(context: ExtensionContext, hasImportedPromise: Promise<boolean>, jdtEventEmitter: EventEmitter<Uri>) {
122122
activationProgressNotification.showProgress();
123123
this.languageClient.onNotification(StatusNotification.type, async (report) => {
124124
// Resolve serverRunning on the first status notification from the server,
@@ -142,6 +142,7 @@ export class StandardLanguageClient {
142142
logger.error(error);
143143
}
144144
activationProgressNotification.hide();
145+
const hasImported = await hasImportedPromise;
145146
if (!hasImported) {
146147
showImportFinishNotification(context);
147148
}

0 commit comments

Comments
 (0)