Skip to content
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

Implement experimental async initialization #409

Merged
merged 5 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions libs/natls/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {

testImplementation project(':testhelpers')
testImplementation libraries.slf4j_nop
testImplementation 'org.awaitility:awaitility:4.2.0'
}

shadowJar {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.amshove.natls.config;

public class InitilizationConfiguration
{
private boolean async;

public boolean isAsync()
{
return async;
}

public void setAsync(boolean async)
{
this.async = async;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class LSConfiguration

private CompletionConfiguration completion;
private InlayHintsConfiguration inlayhints;
private InitilizationConfiguration initialization;

public static LSConfiguration createDefault()
{
Expand All @@ -18,6 +19,10 @@ public static LSConfiguration createDefault()
inlay.setShowAssignmentTargetType(false);
config.setInlayhints(inlay);

var init = new InitilizationConfiguration();
init.setAsync(false);
config.setInitialization(init);

return config;
}

Expand All @@ -40,4 +45,14 @@ public void setInlayhints(InlayHintsConfiguration inlayhints)
{
this.inlayhints = inlayhints;
}

public InitilizationConfiguration getInitialization()
{
return initialization;
}

public void setInitialization(InitilizationConfiguration initialization)
{
this.initialization = initialization;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.amshove.natls.languageserver;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.amshove.natls.App;
import org.amshove.natls.codeactions.CodeActionRegistry;
import org.amshove.natls.config.LSConfiguration;
import org.amshove.natls.markupcontent.MarkdownContentBuilder;
import org.amshove.natls.markupcontent.MarkupContentBuilderFactory;
import org.amshove.natls.progress.ClientProgressType;
import org.amshove.natls.progress.MessageProgressMonitor;
import org.amshove.natls.progress.ProgressTasks;
import org.amshove.natls.progress.WorkDoneProgressMonitor;
import org.amshove.natls.progress.*;
import org.amshove.natparse.natural.project.NaturalFileType;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
Expand Down Expand Up @@ -40,6 +40,11 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params)
log.info("Starting initialization");
var capabilities = new ServerCapabilities();

var config = params.getInitializationOptions() != null
? new Gson().fromJson((JsonObject) params.getInitializationOptions(), LSConfiguration.class)
: LSConfiguration.createDefault();
NaturalLanguageService.setConfiguration(config);

capabilities.setWorkspaceSymbolProvider(true);
capabilities.setDocumentSymbolProvider(new DocumentSymbolOptions("NatLS"));
var hoverOptions = new HoverOptions();
Expand Down Expand Up @@ -124,7 +129,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params)
}

var startTime = System.currentTimeMillis();
progressMonitor.progress("Begin indexing", 5);
progressMonitor.progress("Begin Indexing", 10);
languageService.indexProject(Paths.get(URI.create(params.getRootUri())), progressMonitor);
workspaceService.setLanguageService(languageService);
documentService.setLanguageService(languageService);
Expand All @@ -141,6 +146,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params)
progressMonitor.progress("Initialization done in %dms".formatted(endTime - startTime), 100);
}

BackgroundTasks.initialize(client);
var lspName = App.class.getPackage().getImplementationTitle();
var lspVersion = App.class.getPackage().getImplementationVersion();
var initEnd = System.currentTimeMillis();
Expand All @@ -149,6 +155,33 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params)
});
}

@Override
public void initialized(InitializedParams params)
{
log.info("initialized() called");
if (NaturalLanguageService.getConfig().getInitialization().isAsync())
{
client.showMessage(ClientMessage.info("Background initialization started"));
var fileReferences = languageService.parseFileReferencesAsync();
var dataAreas = languageService.preparseDataAreasAsync();
CompletableFuture.allOf(fileReferences, dataAreas)
.whenComplete((v, error) ->
{
if (error == null)
{
client.showMessage(ClientMessage.info("Background initialization done"));
client.refreshCodeLenses();
}
else
{
client.showMessage(ClientMessage.error("Background initialization failed"));
}
languageService.setInitialized();
});
}
log.info("initialized() returned");
}

@Override
public CompletableFuture<Object> shutdown()
{
Expand Down Expand Up @@ -204,7 +237,7 @@ public CompletableFuture<Void> reparseReferences(Object params)
{
if (languageService.isInitialized())
{
return languageService.parseFileReferences();
languageService.parseFileReferencesAsync();
}

return CompletableFuture.completedFuture(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import org.amshove.natls.hover.HoverContext;
import org.amshove.natls.hover.HoverProvider;
import org.amshove.natls.inlayhints.InlayHintProvider;
import org.amshove.natls.progress.BackgroundTasks;
import org.amshove.natls.progress.IProgressMonitor;
import org.amshove.natls.progress.NullProgressMonitor;
import org.amshove.natls.progress.ProgressTasks;
import org.amshove.natls.project.LanguageServerFile;
import org.amshove.natls.project.LanguageServerProject;
Expand Down Expand Up @@ -54,10 +56,12 @@
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class NaturalLanguageService implements LanguageClientAware
{
private static final Logger log = Logger.getAnonymousLogger();
private static final CodeActionRegistry codeActionRegistry = CodeActionRegistry.INSTANCE;
private NaturalProject project; // TODO: Replace
private LanguageServerProject languageServerProject;
Expand All @@ -79,26 +83,33 @@ public class NaturalLanguageService implements LanguageClientAware
public void indexProject(Path workspaceRoot, IProgressMonitor progressMonitor)
{
this.workspaceRoot = workspaceRoot;
progressMonitor.progress("Reading project file", 20);
var projectFile = new ActualFilesystem().findNaturalProjectFile(workspaceRoot);
if (projectFile.isEmpty())
{
throw new LanguageServerException("Could not load Natural project. .natural or _naturalBuild not found");
}
var project = new BuildFileProjectReader().getNaturalProject(projectFile.get());
progressMonitor.progress("Parsing .editorconfig", 30);
var editorconfigPath = projectFile.get().getParent().resolve(".editorconfig");
if (editorconfigPath.toFile().exists())
{
loadEditorConfig(editorconfigPath);
}

progressMonitor.progress("Indexing Natural files", 40);
var indexer = new NaturalProjectFileIndexer();
indexer.indexProject(project);
this.project = project;
languageServerProject = LanguageServerProject.fromProject(project);
parseFileReferences(progressMonitor);
preParseDataAreas(progressMonitor);
initialized = true;
if (!getConfig().getInitialization().isAsync())
{
parseFileReferencesAsync(progressMonitor);
preParseDataAreas(progressMonitor);
initialized = true;
}
hoverProvider = new HoverProvider();
progressMonitor.progress("Initializing Services", 80);
completionProvider = new CompletionProvider(new SnippetEngine(languageServerProject), hoverProvider);
}

Expand Down Expand Up @@ -406,21 +417,31 @@ public void parseAll(IProgressMonitor monitor)
monitor.progress("Done", 100);
}

public CompletableFuture<Void> parseFileReferences()
public CompletableFuture<Void> parseFileReferencesAsync()
{
// BackgroundTasks can't have a ProgressMonitor, because the progress would spam the communication
// and make the client wait for finish of the progress before sending new requests.
return BackgroundTasks.enqueue(() -> parseFileReferencesAsync(new NullProgressMonitor()), "Parsing file references");
}

public CompletableFuture<Void> preparseDataAreasAsync()
{
return ProgressTasks.startNewVoid("Parsing file references", client, this::parseFileReferences);
// BackgroundTasks can't have a ProgressMonitor, because the progress would spam the communication
// and make the client wait for finish of the progress before sending new requests.
return BackgroundTasks.enqueue(() -> preParseDataAreas(new NullProgressMonitor()), "Parsing Data Areas");
}

private void preParseDataAreas(IProgressMonitor monitor)
{
monitor.progress("Preparsing data areas", 0);
languageServerProject.libraries().stream().flatMap(l -> l.files().stream().filter(f -> f.getType() == NaturalFileType.LDA || f.getType() == NaturalFileType.PDA))
.parallel()
.peek(f -> monitor.progress(f.getReferableName(), 0))
.peek(f -> monitor.progress("Parsing data areas %s".formatted(f.getReferableName())))
.forEach(f -> f.parse(ParseStrategy.WITHOUT_CALLERS));
log.info("preParseDataAreas done");
}

private void parseFileReferences(IProgressMonitor monitor)
private void parseFileReferencesAsync(IProgressMonitor monitor)
{
monitor.progress("Clearing current references", 0);
var parser = new ModuleReferenceParser();
Expand All @@ -440,7 +461,7 @@ private void parseFileReferences(IProgressMonitor monitor)
break;
}
var percentageDone = 100L * processedFiles / allFilesCount;
monitor.progress("Indexing %s.%s".formatted(library.name(), file.getReferableName()), (int) percentageDone);
monitor.progress("Parsing references %s.%s".formatted(library.name(), file.getReferableName()), (int) percentageDone);
switch (file.getType())
{
case PROGRAM, SUBPROGRAM, SUBROUTINE, FUNCTION, COPYCODE -> parser.parseReferences(file);
Expand All @@ -450,6 +471,7 @@ private void parseFileReferences(IProgressMonitor monitor)
processedFiles++;
}
}
log.info("parseFileReferences done");
}

public boolean isInitialized()
Expand Down Expand Up @@ -760,6 +782,11 @@ public LanguageServerProject getProject()
return languageServerProject;
}

public void setInitialized()
{
this.initialized = true;
}

private static <T> T extractJsonObject(Object obj, Class<T> clazz)
{
if (clazz.isInstance(obj))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void didChangeConfiguration(DidChangeConfigurationParams params)
var settings = (JsonObject) params.getSettings();
var jsonObject = settings.getAsJsonObject("natls");
var configuration = new Gson().fromJson(jsonObject, LSConfiguration.class);
languageService.setConfiguration(configuration);
NaturalLanguageService.setConfiguration(configuration);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.amshove.natls.progress;

import org.amshove.natls.languageserver.ClientMessage;
import org.eclipse.lsp4j.services.LanguageClient;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class BackgroundTasks
{
private static final Logger log = Logger.getAnonymousLogger();
private static final ExecutorService workpool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static LanguageClient client;

private BackgroundTasks()
{}

public static CompletableFuture<Void> enqueue(Runnable runnable, String description)
{
var future = new CompletableFuture<Void>();
workpool.submit(() ->
{
try
{
runnable.run();
future.complete(null);
}
catch (Exception e)
{
log.log(Level.SEVERE, "Background task <%s> threw an exception".formatted(description), e);
client.showMessage(ClientMessage.error("Background task <%s> failed".formatted(description)));
future.completeExceptionally(e);
}
});
return future;
}

public static void initialize(LanguageClient client)
{
BackgroundTasks.client = client;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

public interface IProgressMonitor
{
/**
* Sends a progress notification with the given percentage.
*/
void progress(String message, int percentage);

/**
* Increments the previous percentage (if below 100%) and sends a progress message.
*/
void progress(String message);

boolean isCancellationRequested();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ public class MessageProgressMonitor implements IProgressMonitor
private final LanguageClient client;

private int lastTenthPercentage = 0;
private int previousPercentage = 0;

public MessageProgressMonitor(LanguageClient client)
{
this.client = client;
}

@Override
public void progress(String message)
{
progress(message, ++previousPercentage);
}

@Override
public void progress(String message, int percentage)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ public class NullProgressMonitor implements IProgressMonitor
@Override
public void progress(String message, int percentage)
{
// intentionally empty
}

@Override
public void progress(String message)
{
// intentionally empty
}

@Override
Expand Down
Loading