From fc7e9312d00e60171ed77c477ed91231d3dbfff9 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 12 Dec 2021 17:48:47 +0100 Subject: build: move modules into subproject directory --- language-web/src/main/css/index.scss | 16 - .../src/main/css/themeVariables.module.scss | 9 - language-web/src/main/css/themes.scss | 38 --- language-web/src/main/html/index.html | 16 - .../refinery/language/web/CacheControlFilter.java | 52 --- .../refinery/language/web/ProblemWebModule.java | 35 -- .../refinery/language/web/ProblemWebSetup.java | 25 -- .../language/web/ProblemWebSocketServlet.java | 29 -- .../refinery/language/web/ServerLauncher.java | 192 ----------- .../web/occurrences/ProblemOccurrencesService.java | 16 - .../language/web/xtext/server/PongResult.java | 44 --- .../language/web/xtext/server/ResponseHandler.java | 8 - .../web/xtext/server/ResponseHandlerException.java | 14 - .../xtext/server/SubscribingServiceContext.java | 26 -- .../web/xtext/server/TransactionExecutor.java | 180 ---------- .../xtext/server/message/XtextWebErrorKind.java | 11 - .../server/message/XtextWebErrorResponse.java | 79 ----- .../xtext/server/message/XtextWebOkResponse.java | 72 ---- .../xtext/server/message/XtextWebPushMessage.java | 81 ----- .../web/xtext/server/message/XtextWebRequest.java | 57 ---- .../web/xtext/server/message/XtextWebResponse.java | 4 - .../xtext/server/push/PrecomputationListener.java | 15 - .../xtext/server/push/PushServiceDispatcher.java | 23 -- .../web/xtext/server/push/PushWebDocument.java | 89 ----- .../xtext/server/push/PushWebDocumentAccess.java | 68 ---- .../xtext/server/push/PushWebDocumentProvider.java | 33 -- .../web/xtext/servlet/SimpleServiceContext.java | 26 -- .../language/web/xtext/servlet/SimpleSession.java | 35 -- .../web/xtext/servlet/XtextStatusCode.java | 9 - .../language/web/xtext/servlet/XtextWebSocket.java | 133 -------- .../web/xtext/servlet/XtextWebSocketServlet.java | 83 ----- language-web/src/main/js/App.tsx | 60 ---- language-web/src/main/js/RootStore.tsx | 39 --- language-web/src/main/js/editor/EditorArea.tsx | 152 --------- language-web/src/main/js/editor/EditorButtons.tsx | 98 ------ language-web/src/main/js/editor/EditorParent.ts | 205 ------------ language-web/src/main/js/editor/EditorStore.ts | 289 ---------------- language-web/src/main/js/editor/GenerateButton.tsx | 44 --- .../src/main/js/editor/decorationSetExtension.ts | 39 --- language-web/src/main/js/editor/findOccurrences.ts | 35 -- .../src/main/js/editor/semanticHighlighting.ts | 24 -- language-web/src/main/js/global.d.ts | 11 - language-web/src/main/js/index.tsx | 69 ---- language-web/src/main/js/language/folding.ts | 115 ------- language-web/src/main/js/language/indentation.ts | 87 ----- language-web/src/main/js/language/problem.grammar | 149 --------- .../src/main/js/language/problemLanguageSupport.ts | 92 ------ language-web/src/main/js/language/props.ts | 7 - language-web/src/main/js/theme/EditorTheme.ts | 47 --- language-web/src/main/js/theme/ThemeProvider.tsx | 15 - language-web/src/main/js/theme/ThemeStore.ts | 64 ---- .../src/main/js/utils/ConditionVariable.ts | 64 ---- language-web/src/main/js/utils/PendingTask.ts | 60 ---- language-web/src/main/js/utils/Timer.ts | 33 -- language-web/src/main/js/utils/logger.ts | 49 --- .../src/main/js/xtext/ContentAssistService.ts | 219 ------------- .../src/main/js/xtext/HighlightingService.ts | 37 --- .../src/main/js/xtext/OccurrencesService.ts | 127 ------- language-web/src/main/js/xtext/UpdateService.ts | 363 --------------------- .../src/main/js/xtext/ValidationService.ts | 39 --- language-web/src/main/js/xtext/XtextClient.ts | 86 ----- .../src/main/js/xtext/XtextWebSocketClient.ts | 362 -------------------- language-web/src/main/js/xtext/xtextMessages.ts | 40 --- .../src/main/js/xtext/xtextServiceResults.ts | 112 ------- 64 files changed, 4750 deletions(-) delete mode 100644 language-web/src/main/css/index.scss delete mode 100644 language-web/src/main/css/themeVariables.module.scss delete mode 100644 language-web/src/main/css/themes.scss delete mode 100644 language-web/src/main/html/index.html delete mode 100644 language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java delete mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java delete mode 100644 language-web/src/main/js/App.tsx delete mode 100644 language-web/src/main/js/RootStore.tsx delete mode 100644 language-web/src/main/js/editor/EditorArea.tsx delete mode 100644 language-web/src/main/js/editor/EditorButtons.tsx delete mode 100644 language-web/src/main/js/editor/EditorParent.ts delete mode 100644 language-web/src/main/js/editor/EditorStore.ts delete mode 100644 language-web/src/main/js/editor/GenerateButton.tsx delete mode 100644 language-web/src/main/js/editor/decorationSetExtension.ts delete mode 100644 language-web/src/main/js/editor/findOccurrences.ts delete mode 100644 language-web/src/main/js/editor/semanticHighlighting.ts delete mode 100644 language-web/src/main/js/global.d.ts delete mode 100644 language-web/src/main/js/index.tsx delete mode 100644 language-web/src/main/js/language/folding.ts delete mode 100644 language-web/src/main/js/language/indentation.ts delete mode 100644 language-web/src/main/js/language/problem.grammar delete mode 100644 language-web/src/main/js/language/problemLanguageSupport.ts delete mode 100644 language-web/src/main/js/language/props.ts delete mode 100644 language-web/src/main/js/theme/EditorTheme.ts delete mode 100644 language-web/src/main/js/theme/ThemeProvider.tsx delete mode 100644 language-web/src/main/js/theme/ThemeStore.ts delete mode 100644 language-web/src/main/js/utils/ConditionVariable.ts delete mode 100644 language-web/src/main/js/utils/PendingTask.ts delete mode 100644 language-web/src/main/js/utils/Timer.ts delete mode 100644 language-web/src/main/js/utils/logger.ts delete mode 100644 language-web/src/main/js/xtext/ContentAssistService.ts delete mode 100644 language-web/src/main/js/xtext/HighlightingService.ts delete mode 100644 language-web/src/main/js/xtext/OccurrencesService.ts delete mode 100644 language-web/src/main/js/xtext/UpdateService.ts delete mode 100644 language-web/src/main/js/xtext/ValidationService.ts delete mode 100644 language-web/src/main/js/xtext/XtextClient.ts delete mode 100644 language-web/src/main/js/xtext/XtextWebSocketClient.ts delete mode 100644 language-web/src/main/js/xtext/xtextMessages.ts delete mode 100644 language-web/src/main/js/xtext/xtextServiceResults.ts (limited to 'language-web/src/main') diff --git a/language-web/src/main/css/index.scss b/language-web/src/main/css/index.scss deleted file mode 100644 index ad876aaf..00000000 --- a/language-web/src/main/css/index.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use '@fontsource/roboto/scss/mixins' as Roboto; -@use '@fontsource/jetbrains-mono/scss/mixins' as JetbrainsMono; - -$fontWeights: 300, 400, 500, 700; -@each $weight in $fontWeights { - @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight); - @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight, $style: italic); -} - -$monoFontWeights: 400, 700; -@each $weight in $monoFontWeights { - @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight); - @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight, $style: italic); -} -@include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable'); -@include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable', $style: italic); diff --git a/language-web/src/main/css/themeVariables.module.scss b/language-web/src/main/css/themeVariables.module.scss deleted file mode 100644 index 85af4219..00000000 --- a/language-web/src/main/css/themeVariables.module.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './themes'; - -:export { - @each $themeName, $theme in $themes { - @each $variable, $value in $theme { - #{$themeName}--#{$variable}: $value, - } - } -} diff --git a/language-web/src/main/css/themes.scss b/language-web/src/main/css/themes.scss deleted file mode 100644 index a30f1de3..00000000 --- a/language-web/src/main/css/themes.scss +++ /dev/null @@ -1,38 +0,0 @@ -$themes: ( - 'dark': ( - 'foreground': #abb2bf, - 'foregroundHighlight': #eeffff, - 'background': #212121, - 'primary': #56b6c2, - 'secondary': #ff5370, - 'keyword': #56b6c2, - 'predicate': #d6e9ff, - 'variable': #c8ae9d, - 'uniqueNode': #d6e9ff, - 'number': #6e88a6, - 'delimiter': #707787, - 'comment': #5c6370, - 'cursor': #56b6c2, - 'selection': #3e4452, - 'currentLine': rgba(0, 0, 0, 0.2), - 'lineNumber': #5c6370, - ), - 'light': ( - 'foreground': #abb2bf, - 'background': #282c34, - 'paper': #21252b, - 'primary': #56b6c2, - 'secondary': #ff5370, - 'keyword': #56b6c2, - 'predicate': #d6e9ff, - 'variable': #c8ae9d, - 'uniqueNode': #d6e9ff, - 'number': #6e88a6, - 'delimiter': #56606d, - 'comment': #55606d, - 'cursor': #f3efe7, - 'selection': #3e4452, - 'currentLine': #2c323c, - 'lineNumber': #5c6370, - ), -); diff --git a/language-web/src/main/html/index.html b/language-web/src/main/html/index.html deleted file mode 100644 index f404aa8a..00000000 --- a/language-web/src/main/html/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Refinery - - - -
- - diff --git a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java b/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java deleted file mode 100644 index b13ae95d..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java +++ /dev/null @@ -1,52 +0,0 @@ -package tools.refinery.language.web; - -import java.io.IOException; -import java.time.Duration; -import java.util.regex.Pattern; - -import org.eclipse.jetty.http.HttpHeader; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public class CacheControlFilter implements Filter { - private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2)"); - - private static final Duration EXPIRY = Duration.ofDays(365); - - private static final String CACHE_CONTROL_CACHE_VALUE = "public, max-age: " + EXPIRY.toSeconds() + ", immutable"; - - private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate"; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to initialize. - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) { - if (CACHE_URI_PATTERN.matcher(httpRequest.getRequestURI()).matches()) { - httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_CACHE_VALUE); - httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(), - System.currentTimeMillis() + EXPIRY.toMillis()); - } else { - httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_NO_CACHE_VALUE); - httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(), 0); - } - } - chain.doFilter(request, response); - } - - @Override - public void destroy() { - // Nothing to dispose. - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java deleted file mode 100644 index ec55036f..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * generated by Xtext 2.25.0 - */ -package tools.refinery.language.web; - -import org.eclipse.xtext.web.server.XtextServiceDispatcher; -import org.eclipse.xtext.web.server.model.IWebDocumentProvider; -import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; -import org.eclipse.xtext.web.server.occurrences.OccurrencesService; - -import tools.refinery.language.web.occurrences.ProblemOccurrencesService; -import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; -import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; -import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; - -/** - * Use this class to register additional components to be used within the web application. - */ -public class ProblemWebModule extends AbstractProblemWebModule { - public Class bindIWebDocumentProvider() { - return PushWebDocumentProvider.class; - } - - public Class bindXtextWebDocumentAccess() { - return PushWebDocumentAccess.class; - } - - public Class bindXtextServiceDispatcher() { - return PushServiceDispatcher.class; - } - - public Class bindOccurrencesService() { - return ProblemOccurrencesService.class; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java deleted file mode 100644 index 4738bc80..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * generated by Xtext 2.25.0 - */ -package tools.refinery.language.web; - -import org.eclipse.xtext.util.Modules2; - -import com.google.inject.Guice; -import com.google.inject.Injector; - -import tools.refinery.language.ProblemRuntimeModule; -import tools.refinery.language.ProblemStandaloneSetup; -import tools.refinery.language.ide.ProblemIdeModule; - -/** - * Initialization support for running Xtext languages in web applications. - */ -public class ProblemWebSetup extends ProblemStandaloneSetup { - - @Override - public Injector createInjector() { - return Guice.createInjector(Modules2.mixin(new ProblemRuntimeModule(), new ProblemIdeModule(), new ProblemWebModule())); - } - -} diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java deleted file mode 100644 index df67b521..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java +++ /dev/null @@ -1,29 +0,0 @@ -package tools.refinery.language.web; - -import org.eclipse.xtext.util.DisposableRegistry; - -import jakarta.servlet.ServletException; -import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; - -public class ProblemWebSocketServlet extends XtextWebSocketServlet { - - private static final long serialVersionUID = -7040955470384797008L; - - private transient DisposableRegistry disposableRegistry; - - @Override - public void init() throws ServletException { - super.init(); - var injector = new ProblemWebSetup().createInjectorAndDoEMFRegistration(); - this.disposableRegistry = injector.getInstance(DisposableRegistry.class); - } - - @Override - public void destroy() { - if (disposableRegistry != null) { - disposableRegistry.dispose(); - disposableRegistry = null; - } - super.destroy(); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java deleted file mode 100644 index ffd903d0..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * generated by Xtext 2.25.0 - */ -package tools.refinery.language.web; - -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jakarta.servlet.DispatcherType; -import jakarta.servlet.SessionTrackingMode; -import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; - -public class ServerLauncher { - public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; - - public static final int DEFAULT_LISTEN_PORT = 1312; - - public static final int DEFAULT_PUBLIC_PORT = 443; - - public static final int HTTP_DEFAULT_PORT = 80; - - public static final int HTTPS_DEFAULT_PORT = 443; - - public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; - - private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class); - - private final Server server; - - public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, Optional allowedOrigins) { - server = new Server(bindAddress); - var handler = new ServletContextHandler(); - addSessionHandler(handler); - addProblemServlet(handler, allowedOrigins); - if (baseResource != null) { - handler.setBaseResource(baseResource); - handler.setWelcomeFiles(new String[] { "index.html" }); - addDefaultServlet(handler); - } - handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); - server.setHandler(handler); - } - - private void addSessionHandler(ServletContextHandler handler) { - var sessionHandler = new SessionHandler(); - sessionHandler.setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE)); - handler.setSessionHandler(sessionHandler); - } - - private void addProblemServlet(ServletContextHandler handler, Optional allowedOrigins) { - var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class); - if (allowedOrigins.isEmpty()) { - LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!"); - } else { - var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR, - allowedOrigins.get()); - problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, - allowedOriginsString); - } - handler.addServlet(problemServletHolder, "/xtext-service"); - JettyWebSocketServletContainerInitializer.configure(handler, null); - } - - private void addDefaultServlet(ServletContextHandler handler) { - var defaultServletHolder = new ServletHolder(DefaultServlet.class); - var isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - // Avoid file locking on Windows: https://stackoverflow.com/a/4985717 - // See also the related Jetty ticket: - // https://github.com/eclipse/jetty.project/issues/2925 - defaultServletHolder.setInitParameter("useFileMappedBuffer", isWindows ? "false" : "true"); - handler.addServlet(defaultServletHolder, "/"); - } - - public void start() throws Exception { - server.start(); - LOG.info("Server started on {}", server.getURI()); - server.join(); - } - - public static void main(String[] args) { - try { - var bindAddress = getBindAddress(); - var baseResource = getBaseResource(); - var allowedOrigins = getAllowedOrigins(); - var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins); - serverLauncher.start(); - } catch (Exception exception) { - LOG.error("Fatal server error", exception); - System.exit(1); - } - } - - private static String getListenAddress() { - var listenAddress = System.getenv("LISTEN_ADDRESS"); - if (listenAddress == null) { - return DEFAULT_LISTEN_ADDRESS; - } - return listenAddress; - } - - private static int getListenPort() { - var portStr = System.getenv("LISTEN_PORT"); - if (portStr != null) { - return Integer.parseInt(portStr); - } - return DEFAULT_LISTEN_PORT; - } - - private static InetSocketAddress getBindAddress() { - var listenAddress = getListenAddress(); - var listenPort = getListenPort(); - return new InetSocketAddress(listenAddress, listenPort); - } - - private static Resource getBaseResource() throws IOException, URISyntaxException { - var baseResourceOverride = System.getenv("BASE_RESOURCE"); - if (baseResourceOverride != null) { - // If a user override is provided, use it. - return Resource.newResource(baseResourceOverride); - } - var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html"); - if (indexUrlInJar != null) { - // If the app is packaged in the jar, serve it. - var webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/")); - return Resource.newResource(webRootUri); - } - // Look for unpacked production artifacts (convenience for running from IDE). - var unpackedResourcePathComponents = new String[] { System.getProperty("user.dir"), "build", "webpack", - "production" }; - var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents)); - if (unpackedResourceDir.isDirectory()) { - return Resource.newResource(unpackedResourceDir); - } - // Fall back to just serving a 404. - return null; - } - - private static String getPublicHost() { - var publicHost = System.getenv("PUBLIC_HOST"); - if (publicHost != null) { - return publicHost.toLowerCase(); - } - return null; - } - - private static int getPublicPort() { - var portStr = System.getenv("PUBLIC_PORT"); - if (portStr != null) { - return Integer.parseInt(portStr); - } - return DEFAULT_LISTEN_PORT; - } - - private static Optional getAllowedOrigins() { - var allowedOrigins = System.getenv("ALLOWED_ORIGINS"); - if (allowedOrigins != null) { - return Optional.of(allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR)); - } - return getAllowedOriginsFromPublicHostAndPort(); - } - - private static Optional getAllowedOriginsFromPublicHostAndPort() { - var publicHost = getPublicHost(); - if (publicHost == null) { - return Optional.empty(); - } - int publicPort = getPublicPort(); - var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http"; - var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort); - if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) { - var urlWithoutPort = String.format("%s://%s", scheme, publicHost); - return Optional.of(new String[] { urlWithPort, urlWithoutPort }); - } - return Optional.of(new String[] { urlWithPort }); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java b/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java deleted file mode 100644 index d32bbb54..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java +++ /dev/null @@ -1,16 +0,0 @@ -package tools.refinery.language.web.occurrences; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.web.server.occurrences.OccurrencesService; - -import com.google.inject.Singleton; - -import tools.refinery.language.model.problem.NamedElement; - -@Singleton -public class ProblemOccurrencesService extends OccurrencesService { - @Override - protected boolean filter(EObject element) { - return super.filter(element) && element instanceof NamedElement; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java deleted file mode 100644 index fe510f51..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package tools.refinery.language.web.xtext.server; - -import java.util.Objects; - -import org.eclipse.xtext.web.server.IServiceResult; - -public class PongResult implements IServiceResult { - private String pong; - - public PongResult(String pong) { - super(); - this.pong = pong; - } - - public String getPong() { - return pong; - } - - public void setPong(String pong) { - this.pong = pong; - } - - @Override - public int hashCode() { - return Objects.hash(pong); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - PongResult other = (PongResult) obj; - return Objects.equals(pong, other.pong); - } - - @Override - public String toString() { - return "PongResult [pong=" + pong + "]"; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java deleted file mode 100644 index 2a85afe3..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package tools.refinery.language.web.xtext.server; - -import tools.refinery.language.web.xtext.server.message.XtextWebResponse; - -@FunctionalInterface -public interface ResponseHandler { - void onResponse(XtextWebResponse response) throws ResponseHandlerException; -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java deleted file mode 100644 index 34fcb546..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java +++ /dev/null @@ -1,14 +0,0 @@ -package tools.refinery.language.web.xtext.server; - -public class ResponseHandlerException extends Exception { - - private static final long serialVersionUID = 3589866922420268164L; - - public ResponseHandlerException(String message, Throwable cause) { - super(message, cause); - } - - public ResponseHandlerException(String message) { - super(message); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java deleted file mode 100644 index 78e00a9e..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java +++ /dev/null @@ -1,26 +0,0 @@ -package tools.refinery.language.web.xtext.server; - -import java.util.Set; - -import org.eclipse.xtext.web.server.IServiceContext; -import org.eclipse.xtext.web.server.ISession; - -import tools.refinery.language.web.xtext.server.push.PrecomputationListener; - -public record SubscribingServiceContext(IServiceContext delegate, PrecomputationListener subscriber) - implements IServiceContext { - @Override - public Set getParameterKeys() { - return delegate.getParameterKeys(); - } - - @Override - public String getParameter(String key) { - return delegate.getParameter(key); - } - - @Override - public ISession getSession() { - return delegate.getSession(); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java deleted file mode 100644 index 0b417b06..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java +++ /dev/null @@ -1,180 +0,0 @@ -package tools.refinery.language.web.xtext.server; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.eclipse.xtext.util.IDisposable; -import org.eclipse.xtext.web.server.IServiceContext; -import org.eclipse.xtext.web.server.IServiceResult; -import org.eclipse.xtext.web.server.ISession; -import org.eclipse.xtext.web.server.InvalidRequestException; -import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException; -import org.eclipse.xtext.web.server.XtextServiceDispatcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.base.Strings; -import com.google.inject.Injector; - -import tools.refinery.language.web.xtext.server.message.XtextWebErrorKind; -import tools.refinery.language.web.xtext.server.message.XtextWebErrorResponse; -import tools.refinery.language.web.xtext.server.message.XtextWebOkResponse; -import tools.refinery.language.web.xtext.server.message.XtextWebPushMessage; -import tools.refinery.language.web.xtext.server.message.XtextWebRequest; -import tools.refinery.language.web.xtext.server.push.PrecomputationListener; -import tools.refinery.language.web.xtext.server.push.PushWebDocument; -import tools.refinery.language.web.xtext.servlet.SimpleServiceContext; - -public class TransactionExecutor implements IDisposable, PrecomputationListener { - private static final Logger LOG = LoggerFactory.getLogger(TransactionExecutor.class); - - private final ISession session; - - private final IResourceServiceProvider.Registry resourceServiceProviderRegistry; - - private final Map> subscriptions = new HashMap<>(); - - private ResponseHandler responseHandler; - - private Object callPendingLock = new Object(); - - private boolean callPending; - - private List pendingPushMessages = new ArrayList<>(); - - public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { - this.session = session; - this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; - } - - public void setResponseHandler(ResponseHandler responseHandler) { - this.responseHandler = responseHandler; - } - - public void handleRequest(XtextWebRequest request) throws ResponseHandlerException { - var serviceContext = new SimpleServiceContext(session, request.getRequestData()); - var ping = serviceContext.getParameter("ping"); - if (ping != null) { - responseHandler.onResponse(new XtextWebOkResponse(request, new PongResult(ping))); - return; - } - synchronized (callPendingLock) { - if (callPending) { - LOG.error("Reentrant request detected"); - } - if (!pendingPushMessages.isEmpty()) { - LOG.error("{} push messages got stuck without a pending request", pendingPushMessages.size()); - } - callPending = true; - } - try { - var injector = getInjector(serviceContext); - var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); - var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this)); - var serviceResult = service.getService().apply(); - responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult)); - } catch (InvalidRequestException e) { - responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); - } catch (RuntimeException e) { - responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); - } finally { - synchronized (callPendingLock) { - for (var message : pendingPushMessages) { - try { - responseHandler.onResponse(message); - } catch (ResponseHandlerException | RuntimeException e) { - LOG.error("Error while flushing push message", e); - } - } - pendingPushMessages.clear(); - callPending = false; - } - } - } - - @Override - public void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName, - IServiceResult serviceResult) throws ResponseHandlerException { - var message = new XtextWebPushMessage(resourceId, stateId, serviceName, serviceResult); - synchronized (callPendingLock) { - // If we're currently responding to a call we must delay any push messages until - // the reply is sent, because push messages relating to the new state id must be - // sent after the response with the new state id so that the client knows about - // the new state when it receives the push message. - if (callPending) { - pendingPushMessages.add(message); - } else { - responseHandler.onResponse(message); - } - } - } - - @Override - public void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) { - PushWebDocument previousDocument = null; - var previousSubscription = subscriptions.get(resourceId); - if (previousSubscription != null) { - previousDocument = previousSubscription.get(); - } - if (previousDocument == document) { - return; - } - if (previousDocument != null) { - previousDocument.removePrecomputationListener(this); - } - subscriptions.put(resourceId, new WeakReference<>(document)); - } - - /** - * Get the injector to satisfy the request in the {@code serviceContext}. - * - * Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}. - * - * @param serviceContext the Xtext service context of the request - * @return the injector for the Xtext language in the request - * @throws UnknownLanguageException if the Xtext language cannot be determined - */ - protected Injector getInjector(IServiceContext context) { - IResourceServiceProvider resourceServiceProvider = null; - var resourceName = context.getParameter("resource"); - if (resourceName == null) { - resourceName = ""; - } - var emfURI = URI.createURI(resourceName); - var contentType = context.getParameter("contentType"); - if (Strings.isNullOrEmpty(contentType)) { - resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI); - if (resourceServiceProvider == null) { - if (emfURI.toString().isEmpty()) { - throw new UnknownLanguageException( - "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'."); - } else { - throw new UnknownLanguageException( - "Unable to identify the Xtext language for resource " + emfURI + "."); - } - } - } else { - resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType); - if (resourceServiceProvider == null) { - throw new UnknownLanguageException( - "Unable to identify the Xtext language for contentType " + contentType + "."); - } - } - return resourceServiceProvider.get(Injector.class); - } - - @Override - public void dispose() { - for (var subscription : subscriptions.values()) { - var document = subscription.get(); - if (document != null) { - document.removePrecomputationListener(this); - } - } - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java deleted file mode 100644 index f74bae74..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java +++ /dev/null @@ -1,11 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -import com.google.gson.annotations.SerializedName; - -public enum XtextWebErrorKind { - @SerializedName("request") - REQUEST_ERROR, - - @SerializedName("server") - SERVER_ERROR, -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java deleted file mode 100644 index 01d78c31..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java +++ /dev/null @@ -1,79 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -import java.util.Objects; - -import com.google.gson.annotations.SerializedName; - -public final class XtextWebErrorResponse implements XtextWebResponse { - private String id; - - @SerializedName("error") - private XtextWebErrorKind errorKind; - - @SerializedName("message") - private String errorMessage; - - public XtextWebErrorResponse(String id, XtextWebErrorKind errorKind, String errorMessage) { - super(); - this.id = id; - this.errorKind = errorKind; - this.errorMessage = errorMessage; - } - - public XtextWebErrorResponse(XtextWebRequest request, XtextWebErrorKind errorKind, - String errorMessage) { - this(request.getId(), errorKind, errorMessage); - } - - public XtextWebErrorResponse(XtextWebRequest request, XtextWebErrorKind errorKind, Throwable t) { - this(request, errorKind, t.getMessage()); - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public XtextWebErrorKind getErrorKind() { - return errorKind; - } - - public void setErrorKind(XtextWebErrorKind errorKind) { - this.errorKind = errorKind; - } - - public String getErrorMessage() { - return errorMessage; - } - - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public int hashCode() { - return Objects.hash(errorKind, errorMessage, id); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - XtextWebErrorResponse other = (XtextWebErrorResponse) obj; - return errorKind == other.errorKind && Objects.equals(errorMessage, other.errorMessage) - && Objects.equals(id, other.id); - } - - @Override - public String toString() { - return "XtextWebSocketErrorResponse [id=" + id + ", errorKind=" + errorKind + ", errorMessage=" + errorMessage - + "]"; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java deleted file mode 100644 index 8af27247..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java +++ /dev/null @@ -1,72 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -import java.util.Objects; - -import org.eclipse.xtext.web.server.IServiceResult; -import org.eclipse.xtext.web.server.IUnwrappableServiceResult; - -import com.google.gson.annotations.SerializedName; - -public final class XtextWebOkResponse implements XtextWebResponse { - private String id; - - @SerializedName("response") - private Object responseData; - - public XtextWebOkResponse(String id, Object responseData) { - super(); - this.id = id; - this.responseData = responseData; - } - - public XtextWebOkResponse(XtextWebRequest request, IServiceResult result) { - this(request.getId(), maybeUnwrap(result)); - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Object getResponseData() { - return responseData; - } - - public void setResponseData(Object responseData) { - this.responseData = responseData; - } - - @Override - public int hashCode() { - return Objects.hash(id, responseData); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - XtextWebOkResponse other = (XtextWebOkResponse) obj; - return Objects.equals(id, other.id) && Objects.equals(responseData, other.responseData); - } - - @Override - public String toString() { - return "XtextWebSocketOkResponse [id=" + id + ", responseData=" + responseData + "]"; - } - - private static Object maybeUnwrap(IServiceResult result) { - if (result instanceof IUnwrappableServiceResult unwrappableServiceResult - && unwrappableServiceResult.getContent() != null) { - return unwrappableServiceResult.getContent(); - } else { - return result; - } - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java deleted file mode 100644 index c9432e1c..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java +++ /dev/null @@ -1,81 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -import java.util.Objects; - -import com.google.gson.annotations.SerializedName; - -public final class XtextWebPushMessage implements XtextWebResponse { - @SerializedName("resource") - private String resourceId; - - private String stateId; - - private String service; - - @SerializedName("push") - private Object pushData; - - public XtextWebPushMessage(String resourceId, String stateId, String service, Object pushData) { - super(); - this.resourceId = resourceId; - this.stateId = stateId; - this.service = service; - this.pushData = pushData; - } - - public String getResourceId() { - return resourceId; - } - - public void setResourceId(String resourceId) { - this.resourceId = resourceId; - } - - public String getStateId() { - return stateId; - } - - public void setStateId(String stateId) { - this.stateId = stateId; - } - - public String getService() { - return service; - } - - public void setService(String service) { - this.service = service; - } - - public Object getPushData() { - return pushData; - } - - public void setPushData(Object pushData) { - this.pushData = pushData; - } - - @Override - public int hashCode() { - return Objects.hash(pushData, resourceId, service, stateId); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - XtextWebPushMessage other = (XtextWebPushMessage) obj; - return Objects.equals(pushData, other.pushData) && Objects.equals(resourceId, other.resourceId) - && Objects.equals(service, other.service) && Objects.equals(stateId, other.stateId); - } - - @Override - public String toString() { - return "XtextWebPushMessage [resourceId=" + resourceId + ", stateId=" + stateId + ", service=" + service - + ", pushData=" + pushData + "]"; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java deleted file mode 100644 index 959749f8..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -import java.util.Map; -import java.util.Objects; - -import com.google.gson.annotations.SerializedName; - -public class XtextWebRequest { - private String id; - - @SerializedName("request") - private Map requestData; - - public XtextWebRequest(String id, Map requestData) { - super(); - this.id = id; - this.requestData = requestData; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Map getRequestData() { - return requestData; - } - - public void setRequestData(Map requestData) { - this.requestData = requestData; - } - - @Override - public int hashCode() { - return Objects.hash(id, requestData); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - XtextWebRequest other = (XtextWebRequest) obj; - return Objects.equals(id, other.id) && Objects.equals(requestData, other.requestData); - } - - @Override - public String toString() { - return "XtextWebSocketRequest [id=" + id + ", requestData=" + requestData + "]"; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java deleted file mode 100644 index 3bd13047..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package tools.refinery.language.web.xtext.server.message; - -public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java deleted file mode 100644 index 79a284db..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package tools.refinery.language.web.xtext.server.push; - -import org.eclipse.xtext.web.server.IServiceResult; - -import tools.refinery.language.web.xtext.server.ResponseHandlerException; - -@FunctionalInterface -public interface PrecomputationListener { - void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName, IServiceResult serviceResult) - throws ResponseHandlerException; - - default void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) { - // Nothing to handle by default. - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java deleted file mode 100644 index c7b8108d..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java +++ /dev/null @@ -1,23 +0,0 @@ -package tools.refinery.language.web.xtext.server.push; - -import org.eclipse.xtext.web.server.IServiceContext; -import org.eclipse.xtext.web.server.XtextServiceDispatcher; -import org.eclipse.xtext.web.server.model.XtextWebDocument; - -import com.google.inject.Singleton; - -import tools.refinery.language.web.xtext.server.SubscribingServiceContext; - -@Singleton -public class PushServiceDispatcher extends XtextServiceDispatcher { - - @Override - protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) { - var document = super.getFullTextDocument(fullText, resourceId, context); - if (document instanceof PushWebDocument pushWebDocument - && context instanceof SubscribingServiceContext subscribingContext) { - pushWebDocument.addPrecomputationListener(subscribingContext.subscriber()); - } - return document; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java deleted file mode 100644 index 906b9e30..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java +++ /dev/null @@ -1,89 +0,0 @@ -package tools.refinery.language.web.xtext.server.push; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.eclipse.xtext.util.CancelIndicator; -import org.eclipse.xtext.web.server.IServiceResult; -import org.eclipse.xtext.web.server.model.AbstractCachedService; -import org.eclipse.xtext.web.server.model.DocumentSynchronizer; -import org.eclipse.xtext.web.server.model.XtextWebDocument; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.collect.ImmutableList; - -import tools.refinery.language.web.xtext.server.ResponseHandlerException; - -public class PushWebDocument extends XtextWebDocument { - private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); - - private final List precomputationListeners = new ArrayList<>(); - - private final Map, IServiceResult> precomputedServices = new HashMap<>(); - - public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) { - super(resourceId, synchronizer); - if (resourceId == null) { - throw new IllegalArgumentException("resourceId must not be null"); - } - } - - public boolean addPrecomputationListener(PrecomputationListener listener) { - synchronized (precomputationListeners) { - if (precomputationListeners.contains(listener)) { - return false; - } - precomputationListeners.add(listener); - listener.onSubscribeToPrecomputationEvents(getResourceId(), this); - return true; - } - } - - public boolean removePrecomputationListener(PrecomputationListener listener) { - synchronized (precomputationListeners) { - return precomputationListeners.remove(listener); - } - } - - public void precomputeServiceResult(AbstractCachedService service, String serviceName, - CancelIndicator cancelIndicator, boolean logCacheMiss) { - var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); - if (result == null) { - LOG.error("{} service returned null result", serviceName); - return; - } - var serviceClass = service.getClass(); - var previousResult = precomputedServices.get(serviceClass); - if (previousResult != null && previousResult.equals(result)) { - return; - } - precomputedServices.put(serviceClass, result); - notifyPrecomputationListeners(serviceName, result); - } - - private void notifyPrecomputationListeners(String serviceName, T result) { - var resourceId = getResourceId(); - var stateId = getStateId(); - List copyOfListeners; - synchronized (precomputationListeners) { - copyOfListeners = ImmutableList.copyOf(precomputationListeners); - } - var toRemove = new ArrayList(); - for (var listener : copyOfListeners) { - try { - listener.onPrecomputedServiceResult(resourceId, stateId, serviceName, result); - } catch (ResponseHandlerException e) { - LOG.error("Delivering precomputation push message failed", e); - toRemove.add(listener); - } - } - if (!toRemove.isEmpty()) { - synchronized (precomputationListeners) { - precomputationListeners.removeAll(toRemove); - } - } - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java deleted file mode 100644 index b3666a86..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java +++ /dev/null @@ -1,68 +0,0 @@ -package tools.refinery.language.web.xtext.server.push; - -import org.eclipse.xtext.service.OperationCanceledManager; -import org.eclipse.xtext.util.CancelIndicator; -import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork; -import org.eclipse.xtext.web.server.IServiceResult; -import org.eclipse.xtext.web.server.model.AbstractCachedService; -import org.eclipse.xtext.web.server.model.IXtextWebDocument; -import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; -import org.eclipse.xtext.web.server.model.XtextWebDocument; -import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; -import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingService; -import org.eclipse.xtext.web.server.validation.ValidationService; - -import com.google.inject.Inject; - -public class PushWebDocumentAccess extends XtextWebDocumentAccess { - - @Inject - private PrecomputedServiceRegistry preComputedServiceRegistry; - - @Inject - private OperationCanceledManager operationCanceledManager; - - private PushWebDocument pushDocument; - - @Override - protected void init(XtextWebDocument document, String requiredStateId, boolean skipAsyncWork) { - super.init(document, requiredStateId, skipAsyncWork); - if (document instanceof PushWebDocument newPushDocument) { - pushDocument = newPushDocument; - } - } - - @Override - protected void performPrecomputation(CancelIndicator cancelIndicator) { - if (pushDocument == null) { - super.performPrecomputation(cancelIndicator); - return; - } - for (AbstractCachedService service : preComputedServiceRegistry - .getPrecomputedServices()) { - operationCanceledManager.checkCanceled(cancelIndicator); - precomputeServiceResult(service, false); - } - } - - protected void precomputeServiceResult(AbstractCachedService service, boolean logCacheMiss) { - var serviceName = getPrecomputedServiceName(service); - readOnly(new CancelableUnitOfWork() { - @Override - public java.lang.Void exec(IXtextWebDocument d, CancelIndicator cancelIndicator) throws Exception { - pushDocument.precomputeServiceResult(service, serviceName, cancelIndicator, logCacheMiss); - return null; - } - }); - } - - protected String getPrecomputedServiceName(AbstractCachedService service) { - if (service instanceof ValidationService) { - return "validate"; - } - if (service instanceof HighlightingService) { - return "highlight"; - } - throw new IllegalArgumentException("Unknown precomputed service: " + service); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java deleted file mode 100644 index fc45f74a..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -package tools.refinery.language.web.xtext.server.push; - -import org.eclipse.xtext.web.server.IServiceContext; -import org.eclipse.xtext.web.server.model.DocumentSynchronizer; -import org.eclipse.xtext.web.server.model.IWebDocumentProvider; -import org.eclipse.xtext.web.server.model.XtextWebDocument; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; - -/** - * Based on - * {@link org.eclipse.xtext.web.server.model.IWebDocumentProvider.DefaultImpl}. - * - * @author Kristóf Marussy - */ -@Singleton -public class PushWebDocumentProvider implements IWebDocumentProvider { - @Inject - private Provider synchronizerProvider; - - @Override - public XtextWebDocument get(String resourceId, IServiceContext serviceContext) { - if (resourceId == null) { - return new XtextWebDocument(resourceId, synchronizerProvider.get()); - } else { - // We only need to send push messages if a resourceId is specified. - return new PushWebDocument(resourceId, - serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get())); - } - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java deleted file mode 100644 index 43e37160..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java +++ /dev/null @@ -1,26 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.util.Map; -import java.util.Set; - -import org.eclipse.xtext.web.server.IServiceContext; -import org.eclipse.xtext.web.server.ISession; - -import com.google.common.collect.ImmutableSet; - -public record SimpleServiceContext(ISession session, Map parameters) implements IServiceContext { - @Override - public Set getParameterKeys() { - return ImmutableSet.copyOf(parameters.keySet()); - } - - @Override - public String getParameter(String key) { - return parameters.get(key); - } - - @Override - public ISession getSession() { - return session; - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java deleted file mode 100644 index 09c055a2..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java +++ /dev/null @@ -1,35 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.xtext.web.server.ISession; -import org.eclipse.xtext.xbase.lib.Functions.Function0; - -public class SimpleSession implements ISession { - private Map map = new HashMap<>(); - - @Override - public T get(Object key) { - @SuppressWarnings("unchecked") - var value = (T) map.get(key); - return value; - } - - @Override - public T get(Object key, Function0 factory) { - @SuppressWarnings("unchecked") - var value = (T) map.computeIfAbsent(key, absentKey -> factory.apply()); - return value; - } - - @Override - public void put(Object key, Object value) { - map.put(key, value); - } - - @Override - public void remove(Object key) { - map.remove(key); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java deleted file mode 100644 index 0cd229e8..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java +++ /dev/null @@ -1,9 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -public final class XtextStatusCode { - public static final int INVALID_JSON = 4007; - - private XtextStatusCode() { - throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java deleted file mode 100644 index fd41f213..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java +++ /dev/null @@ -1,133 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.io.IOException; -import java.io.Reader; - -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.StatusCode; -import org.eclipse.jetty.websocket.api.WriteCallback; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.eclipse.xtext.web.server.ISession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonParseException; - -import tools.refinery.language.web.xtext.server.ResponseHandler; -import tools.refinery.language.web.xtext.server.ResponseHandlerException; -import tools.refinery.language.web.xtext.server.TransactionExecutor; -import tools.refinery.language.web.xtext.server.message.XtextWebRequest; -import tools.refinery.language.web.xtext.server.message.XtextWebResponse; - -@WebSocket -public class XtextWebSocket implements WriteCallback, ResponseHandler { - private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); - - private final Gson gson = new Gson(); - - private final TransactionExecutor executor; - - private Session webSocketSession; - - public XtextWebSocket(TransactionExecutor executor) { - this.executor = executor; - executor.setResponseHandler(this); - } - - public XtextWebSocket(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { - this(new TransactionExecutor(session, resourceServiceProviderRegistry)); - } - - @OnWebSocketConnect - public void onConnect(Session webSocketSession) { - if (this.webSocketSession != null) { - LOG.error("Websocket session onConnect when already connected"); - return; - } - LOG.debug("New websocket connection from {}", webSocketSession.getRemoteAddress()); - this.webSocketSession = webSocketSession; - } - - @OnWebSocketClose - public void onClose(int statusCode, String reason) { - executor.dispose(); - if (webSocketSession == null) { - return; - } - if (statusCode == StatusCode.NORMAL || statusCode == StatusCode.SHUTDOWN) { - LOG.debug("{} closed connection normally: {}", webSocketSession.getRemoteAddress(), reason); - } else { - LOG.warn("{} closed connection with status code {}: {}", webSocketSession.getRemoteAddress(), statusCode, - reason); - } - webSocketSession = null; - } - - @OnWebSocketError - public void onError(Throwable error) { - if (webSocketSession == null) { - return; - } - LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteAddress(), error); - } - - @OnWebSocketMessage - public void onMessage(Reader reader) { - if (webSocketSession == null) { - LOG.error("Trying to receive message when websocket is disconnected"); - return; - } - XtextWebRequest request; - try { - request = gson.fromJson(reader, XtextWebRequest.class); - } catch (JsonIOException e) { - LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteAddress(), e); - if (webSocketSession.isOpen()) { - webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload"); - } - return; - } catch (JsonParseException e) { - LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteAddress(), e); - webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload"); - return; - } - try { - executor.handleRequest(request); - } catch (ResponseHandlerException e) { - LOG.warn("Cannot write websocket response", e); - if (webSocketSession.isOpen()) { - webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response"); - } - } - } - - @Override - public void onResponse(XtextWebResponse response) throws ResponseHandlerException { - if (webSocketSession == null) { - throw new ResponseHandlerException("Trying to send message when websocket is disconnected"); - } - var responseString = gson.toJson(response); - try { - webSocketSession.getRemote().sendPartialString(responseString, true, this); - } catch (IOException e) { - throw new ResponseHandlerException( - "Cannot initiaite async write to websocket " + webSocketSession.getRemoteAddress(), e); - } - } - - @Override - public void writeFailed(Throwable x) { - if (webSocketSession == null) { - LOG.error("Cannot complete async write to disconnected websocket", x); - return; - } - LOG.warn("Cannot complete async write to websocket " + webSocketSession.getRemoteAddress(), x); - } -} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java deleted file mode 100644 index 942ca380..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java +++ /dev/null @@ -1,83 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.io.IOException; -import java.time.Duration; -import java.util.Set; - -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; -import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; -import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; -import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; - -public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implements JettyWebSocketCreator { - - private static final long serialVersionUID = -3772740838165122685L; - - public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; - - public static final String ALLOWED_ORIGINS_INIT_PARAM = "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin"; - - public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1"; - - /** - * Maximum message size should be large enough to upload a full model file. - */ - private static final long MAX_FRAME_SIZE = 4L * 1024L * 1024L; - - private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30); - - private transient Logger log = LoggerFactory.getLogger(getClass()); - - private transient Set allowedOrigins = null; - - @Override - public void init(ServletConfig config) throws ServletException { - var allowedOriginsStr = config.getInitParameter(ALLOWED_ORIGINS_INIT_PARAM); - if (allowedOriginsStr == null) { - log.warn("All WebSocket origins are allowed! This setting should not be used in production!"); - } else { - allowedOrigins = Set.of(allowedOriginsStr.split(ALLOWED_ORIGINS_SEPARATOR)); - log.info("Allowed origins: {}", allowedOrigins); - } - super.init(config); - } - - @Override - protected void configure(JettyWebSocketServletFactory factory) { - factory.setMaxFrameSize(MAX_FRAME_SIZE); - factory.setIdleTimeout(IDLE_TIMEOUT); - factory.addMapping("/", this); - } - - @Override - public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { - if (allowedOrigins != null) { - var origin = req.getOrigin(); - if (origin == null || !allowedOrigins.contains(origin.toLowerCase())) { - log.error("Connection from {} from forbidden origin {}", req.getRemoteSocketAddress(), origin); - try { - resp.sendForbidden("Origin not allowed"); - } catch (IOException e) { - log.error("Cannot send forbidden origin error", e); - } - return null; - } - } - if (req.getSubProtocols().contains(XTEXT_SUBPROTOCOL_V1)) { - resp.setAcceptedSubProtocol(XTEXT_SUBPROTOCOL_V1); - } else { - log.error("None of the subprotocols {} offered by {} are supported", req.getSubProtocols(), - req.getRemoteSocketAddress()); - resp.setAcceptedSubProtocol(null); - } - var session = new SimpleSession(); - return new XtextWebSocket(session, IResourceServiceProvider.Registry.INSTANCE); - } -} diff --git a/language-web/src/main/js/App.tsx b/language-web/src/main/js/App.tsx deleted file mode 100644 index 54f92f9a..00000000 --- a/language-web/src/main/js/App.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import AppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; -import MenuIcon from '@mui/icons-material/Menu'; -import React from 'react'; - -import { EditorArea } from './editor/EditorArea'; -import { EditorButtons } from './editor/EditorButtons'; -import { GenerateButton } from './editor/GenerateButton'; - -export function App(): JSX.Element { - return ( - - - - - - - - Refinery - - - - - - - - - - - - ); -} diff --git a/language-web/src/main/js/RootStore.tsx b/language-web/src/main/js/RootStore.tsx deleted file mode 100644 index baf0b61e..00000000 --- a/language-web/src/main/js/RootStore.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { createContext, useContext } from 'react'; - -import { EditorStore } from './editor/EditorStore'; -import { ThemeStore } from './theme/ThemeStore'; - -export class RootStore { - editorStore; - - themeStore; - - constructor(initialValue: string) { - this.themeStore = new ThemeStore(); - this.editorStore = new EditorStore(initialValue, this.themeStore); - } -} - -const StoreContext = createContext(undefined); - -export interface RootStoreProviderProps { - children: JSX.Element; - - rootStore: RootStore; -} - -export function RootStoreProvider({ children, rootStore }: RootStoreProviderProps): JSX.Element { - return ( - - {children} - - ); -} - -export const useRootStore = (): RootStore => { - const rootStore = useContext(StoreContext); - if (!rootStore) { - throw new Error('useRootStore must be used within RootStoreProvider'); - } - return rootStore; -}; diff --git a/language-web/src/main/js/editor/EditorArea.tsx b/language-web/src/main/js/editor/EditorArea.tsx deleted file mode 100644 index dba20f6e..00000000 --- a/language-web/src/main/js/editor/EditorArea.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { Command, EditorView } from '@codemirror/view'; -import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; -import { closeLintPanel, openLintPanel } from '@codemirror/lint'; -import { observer } from 'mobx-react-lite'; -import React, { - useCallback, - useEffect, - useRef, - useState, -} from 'react'; - -import { EditorParent } from './EditorParent'; -import { useRootStore } from '../RootStore'; -import { getLogger } from '../utils/logger'; - -const log = getLogger('editor.EditorArea'); - -function usePanel( - panelId: string, - stateToSet: boolean, - editorView: EditorView | null, - openCommand: Command, - closeCommand: Command, - closeCallback: () => void, -) { - const [cachedViewState, setCachedViewState] = useState(false); - useEffect(() => { - if (editorView === null || cachedViewState === stateToSet) { - return; - } - if (stateToSet) { - openCommand(editorView); - const buttonQuery = `.cm-${panelId}.cm-panel button[name="close"]`; - const closeButton = editorView.dom.querySelector(buttonQuery); - if (closeButton) { - log.debug('Addig close button callback to', panelId, 'panel'); - // We must remove the event listener added by CodeMirror from the button - // that dispatches a transaction without going through `EditorStorre`. - // Cloning a DOM node removes event listeners, - // see https://stackoverflow.com/a/9251864 - const closeButtonWithoutListeners = closeButton.cloneNode(true); - closeButtonWithoutListeners.addEventListener('click', (event) => { - closeCallback(); - event.preventDefault(); - }); - closeButton.replaceWith(closeButtonWithoutListeners); - } else { - log.error('Opened', panelId, 'panel has no close button'); - } - } else { - closeCommand(editorView); - } - setCachedViewState(stateToSet); - }, [ - stateToSet, - editorView, - cachedViewState, - panelId, - openCommand, - closeCommand, - closeCallback, - ]); - return setCachedViewState; -} - -function fixCodeMirrorAccessibility(editorView: EditorView) { - // Reported by Lighthouse 8.3.0. - const { contentDOM } = editorView; - contentDOM.removeAttribute('aria-expanded'); - contentDOM.setAttribute('aria-label', 'Code editor'); -} - -export const EditorArea = observer(() => { - const { editorStore } = useRootStore(); - const editorParentRef = useRef(null); - const [editorViewState, setEditorViewState] = useState(null); - - const setSearchPanelOpen = usePanel( - 'search', - editorStore.showSearchPanel, - editorViewState, - openSearchPanel, - closeSearchPanel, - useCallback(() => editorStore.setSearchPanelOpen(false), [editorStore]), - ); - - const setLintPanelOpen = usePanel( - 'panel-lint', - editorStore.showLintPanel, - editorViewState, - openLintPanel, - closeLintPanel, - useCallback(() => editorStore.setLintPanelOpen(false), [editorStore]), - ); - - useEffect(() => { - if (editorParentRef.current === null) { - return () => { - // Nothing to clean up. - }; - } - - const editorView = new EditorView({ - state: editorStore.state, - parent: editorParentRef.current, - dispatch: (transaction) => { - editorStore.onTransaction(transaction); - editorView.update([transaction]); - if (editorView.state !== editorStore.state) { - log.error( - 'Failed to synchronize editor state - store state:', - editorStore.state, - 'view state:', - editorView.state, - ); - } - }, - }); - fixCodeMirrorAccessibility(editorView); - setEditorViewState(editorView); - setSearchPanelOpen(false); - setLintPanelOpen(false); - // `dispatch` is bound to the view instance, - // so it does not have to be called as a method. - // eslint-disable-next-line @typescript-eslint/unbound-method - editorStore.updateDispatcher(editorView.dispatch); - log.info('Editor created'); - - return () => { - editorStore.updateDispatcher(null); - editorView.destroy(); - log.info('Editor destroyed'); - }; - }, [ - editorParentRef, - editorStore, - setSearchPanelOpen, - setLintPanelOpen, - ]); - - return ( - - ); -}); diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx deleted file mode 100644 index 150aa00d..00000000 --- a/language-web/src/main/js/editor/EditorButtons.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { Diagnostic } from '@codemirror/lint'; -import { observer } from 'mobx-react-lite'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import CheckIcon from '@mui/icons-material/Check'; -import ErrorIcon from '@mui/icons-material/Error'; -import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; -import FormatPaint from '@mui/icons-material/FormatPaint'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import RedoIcon from '@mui/icons-material/Redo'; -import SearchIcon from '@mui/icons-material/Search'; -import UndoIcon from '@mui/icons-material/Undo'; -import WarningIcon from '@mui/icons-material/Warning'; -import React from 'react'; - -import { useRootStore } from '../RootStore'; - -// Exhastive switch as proven by TypeScript. -// eslint-disable-next-line consistent-return -function getLintIcon(severity: Diagnostic['severity'] | null) { - switch (severity) { - case 'error': - return ; - case 'warning': - return ; - case 'info': - return ; - case null: - return ; - } -} - -export const EditorButtons = observer(() => { - const { editorStore } = useRootStore(); - - return ( - - - editorStore.undo()} - aria-label="Undo" - > - - - editorStore.redo()} - aria-label="Redo" - > - - - - - editorStore.toggleLineNumbers()} - aria-label="Show line numbers" - value="show-line-numbers" - > - - - editorStore.toggleSearchPanel()} - aria-label="Show find/replace" - value="show-search-panel" - > - - - editorStore.toggleLintPanel()} - aria-label="Show diagnostics panel" - value="show-lint-panel" - > - {getLintIcon(editorStore.highestDiagnosticLevel)} - - - editorStore.formatText()} - aria-label="Automatic format" - > - - - - ); -}); diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts deleted file mode 100644 index 94ca24ea..00000000 --- a/language-web/src/main/js/editor/EditorParent.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { styled } from '@mui/material/styles'; - -/** - * Returns a squiggly underline background image encoded as a CSS `url()` data URI with Base64. - * - * Based on - * https://github.com/codemirror/lint/blob/f524b4a53b0183bb343ac1e32b228d28030d17af/src/lint.ts#L501 - * - * @param color the color of the underline - * @returns the CSS `url()` - */ -function underline(color: string) { - const svg = ` - - `; - const svgBase64 = window.btoa(svg); - return `url('data:image/svg+xml;base64,${svgBase64}')`; -} - -export const EditorParent = styled('div')(({ theme }) => { - const codeMirrorLintStyle: Record = {}; - (['error', 'warning', 'info'] as const).forEach((severity) => { - const color = theme.palette[severity].main; - codeMirrorLintStyle[`.cm-diagnostic-${severity}`] = { - borderLeftColor: color, - }; - codeMirrorLintStyle[`.cm-lintRange-${severity}`] = { - backgroundImage: underline(color), - }; - }); - - return { - background: theme.palette.background.default, - '&, .cm-editor': { - height: '100%', - }, - '.cm-content': { - padding: 0, - }, - '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { - fontSize: 16, - fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', - fontFeatureSettings: '"liga", "calt"', - fontWeight: 400, - letterSpacing: 0, - textRendering: 'optimizeLegibility', - }, - '.cm-scroller': { - color: theme.palette.text.secondary, - }, - '.cm-gutters': { - background: 'rgba(255, 255, 255, 0.1)', - color: theme.palette.text.disabled, - border: 'none', - }, - '.cm-specialChar': { - color: theme.palette.secondary.main, - }, - '.cm-activeLine': { - background: 'rgba(0, 0, 0, 0.3)', - }, - '.cm-activeLineGutter': { - background: 'transparent', - }, - '.cm-lineNumbers .cm-activeLineGutter': { - color: theme.palette.text.primary, - }, - '.cm-cursor, .cm-cursor-primary': { - borderColor: theme.palette.primary.main, - background: theme.palette.common.black, - }, - '.cm-selectionBackground': { - background: '#3e4453', - }, - '.cm-focused': { - outline: 'none', - '.cm-selectionBackground': { - background: '#3e4453', - }, - }, - '.cm-panels-top': { - color: theme.palette.text.secondary, - }, - '.cm-panel': { - '&, & button, & input': { - fontFamily: '"Roboto","Helvetica","Arial",sans-serif', - }, - background: theme.palette.background.paper, - borderTop: `1px solid ${theme.palette.divider}`, - 'button[name="close"]': { - background: 'transparent', - color: theme.palette.text.secondary, - cursor: 'pointer', - }, - }, - '.cm-panel.cm-panel-lint': { - 'button[name="close"]': { - // Close button interferes with scrollbar, so we better hide it. - // The panel can still be closed from the toolbar. - display: 'none', - }, - ul: { - li: { - borderBottom: `1px solid ${theme.palette.divider}`, - cursor: 'pointer', - }, - '[aria-selected]': { - background: '#3e4453', - color: theme.palette.text.primary, - }, - '&:focus [aria-selected]': { - background: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - }, - }, - '.cm-foldPlaceholder': { - background: theme.palette.background.paper, - borderColor: theme.palette.text.disabled, - color: theme.palette.text.secondary, - }, - '.cmt-comment': { - fontStyle: 'italic', - color: theme.palette.text.disabled, - }, - '.cmt-number': { - color: '#6188a6', - }, - '.cmt-string': { - color: theme.palette.secondary.dark, - }, - '.cmt-keyword': { - color: theme.palette.primary.main, - }, - '.cmt-typeName, .cmt-macroName, .cmt-atom': { - color: theme.palette.text.primary, - }, - '.cmt-variableName': { - color: '#c8ae9d', - }, - '.cmt-problem-node': { - '&, & .cmt-variableName': { - color: theme.palette.text.secondary, - }, - }, - '.cmt-problem-individual': { - '&, & .cmt-variableName': { - color: theme.palette.text.primary, - }, - }, - '.cmt-problem-abstract, .cmt-problem-new': { - fontStyle: 'italic', - }, - '.cmt-problem-containment': { - fontWeight: 700, - }, - '.cmt-problem-error': { - '&, & .cmt-typeName': { - color: theme.palette.error.main, - }, - }, - '.cmt-problem-builtin': { - '&, & .cmt-typeName, & .cmt-atom, & .cmt-variableName': { - color: theme.palette.primary.main, - fontWeight: 400, - fontStyle: 'normal', - }, - }, - '.cm-tooltip-autocomplete': { - background: theme.palette.background.paper, - boxShadow: `0px 2px 4px -1px rgb(0 0 0 / 20%), - 0px 4px 5px 0px rgb(0 0 0 / 14%), - 0px 1px 10px 0px rgb(0 0 0 / 12%)`, - '.cm-completionIcon': { - color: theme.palette.text.secondary, - }, - '.cm-completionLabel': { - color: theme.palette.text.primary, - }, - '.cm-completionDetail': { - color: theme.palette.text.secondary, - fontStyle: 'normal', - }, - '[aria-selected]': { - background: `${theme.palette.primary.main} !important`, - '.cm-completionIcon, .cm-completionLabel, .cm-completionDetail': { - color: theme.palette.primary.contrastText, - }, - }, - }, - '.cm-completionIcon': { - width: 16, - padding: 0, - marginRight: '0.5em', - textAlign: 'center', - }, - ...codeMirrorLintStyle, - '.cm-problem-write': { - background: 'rgba(255, 255, 128, 0.3)', - }, - '.cm-problem-read': { - background: 'rgba(255, 255, 255, 0.15)', - }, - }; -}); diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts deleted file mode 100644 index 5760de28..00000000 --- a/language-web/src/main/js/editor/EditorStore.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; -import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets'; -import { defaultKeymap, indentWithTab } from '@codemirror/commands'; -import { commentKeymap } from '@codemirror/comment'; -import { foldGutter, foldKeymap } from '@codemirror/fold'; -import { highlightActiveLineGutter, lineNumbers } from '@codemirror/gutter'; -import { classHighlightStyle } from '@codemirror/highlight'; -import { - history, - historyKeymap, - redo, - redoDepth, - undo, - undoDepth, -} from '@codemirror/history'; -import { indentOnInput } from '@codemirror/language'; -import { - Diagnostic, - lintKeymap, - setDiagnostics, -} from '@codemirror/lint'; -import { bracketMatching } from '@codemirror/matchbrackets'; -import { rectangularSelection } from '@codemirror/rectangular-selection'; -import { searchConfig, searchKeymap } from '@codemirror/search'; -import { - EditorState, - StateCommand, - StateEffect, - Transaction, - TransactionSpec, -} from '@codemirror/state'; -import { - drawSelection, - EditorView, - highlightActiveLine, - highlightSpecialChars, - keymap, -} from '@codemirror/view'; -import { - makeAutoObservable, - observable, - reaction, -} from 'mobx'; - -import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences'; -import { problemLanguageSupport } from '../language/problemLanguageSupport'; -import { - IHighlightRange, - semanticHighlighting, - setSemanticHighlighting, -} from './semanticHighlighting'; -import type { ThemeStore } from '../theme/ThemeStore'; -import { getLogger } from '../utils/logger'; -import { XtextClient } from '../xtext/XtextClient'; - -const log = getLogger('editor.EditorStore'); - -export class EditorStore { - private readonly themeStore; - - state: EditorState; - - private readonly client: XtextClient; - - showLineNumbers = false; - - showSearchPanel = false; - - showLintPanel = false; - - errorCount = 0; - - warningCount = 0; - - infoCount = 0; - - private readonly defaultDispatcher = (tr: Transaction): void => { - this.onTransaction(tr); - }; - - private dispatcher = this.defaultDispatcher; - - constructor(initialValue: string, themeStore: ThemeStore) { - this.themeStore = themeStore; - this.state = EditorState.create({ - doc: initialValue, - extensions: [ - autocompletion({ - activateOnTyping: true, - override: [ - (context) => this.client.contentAssist(context), - ], - }), - classHighlightStyle.extension, - closeBrackets(), - bracketMatching(), - drawSelection(), - EditorState.allowMultipleSelections.of(true), - EditorView.theme({}, { - dark: this.themeStore.darkMode, - }), - findOccurrences, - highlightActiveLine(), - highlightActiveLineGutter(), - highlightSpecialChars(), - history(), - indentOnInput(), - rectangularSelection(), - searchConfig({ - top: true, - matchCase: true, - }), - semanticHighlighting, - // We add the gutters to `extensions` in the order we want them to appear. - lineNumbers(), - foldGutter(), - keymap.of([ - { key: 'Mod-Shift-f', run: () => this.formatText() }, - ...closeBracketsKeymap, - ...commentKeymap, - ...completionKeymap, - ...foldKeymap, - ...historyKeymap, - indentWithTab, - // Override keys in `lintKeymap` to go through the `EditorStore`. - { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, - ...lintKeymap, - // Override keys in `searchKeymap` to go through the `EditorStore`. - { key: 'Mod-f', run: () => this.setSearchPanelOpen(true), scope: 'editor search-panel' }, - { key: 'Escape', run: () => this.setSearchPanelOpen(false), scope: 'editor search-panel' }, - ...searchKeymap, - ...defaultKeymap, - ]), - problemLanguageSupport(), - ], - }); - this.client = new XtextClient(this); - reaction( - () => this.themeStore.darkMode, - (darkMode) => { - log.debug('Update editor dark mode', darkMode); - this.dispatch({ - effects: [ - StateEffect.appendConfig.of(EditorView.theme({}, { - dark: darkMode, - })), - ], - }); - }, - ); - makeAutoObservable(this, { - state: observable.ref, - }); - } - - updateDispatcher(newDispatcher: ((tr: Transaction) => void) | null): void { - this.dispatcher = newDispatcher || this.defaultDispatcher; - } - - onTransaction(tr: Transaction): void { - log.trace('Editor transaction', tr); - this.state = tr.state; - this.client.onTransaction(tr); - } - - dispatch(...specs: readonly TransactionSpec[]): void { - this.dispatcher(this.state.update(...specs)); - } - - doStateCommand(command: StateCommand): boolean { - return command({ - state: this.state, - dispatch: this.dispatcher, - }); - } - - updateDiagnostics(diagnostics: Diagnostic[]): void { - this.dispatch(setDiagnostics(this.state, diagnostics)); - this.errorCount = 0; - this.warningCount = 0; - this.infoCount = 0; - diagnostics.forEach(({ severity }) => { - switch (severity) { - case 'error': - this.errorCount += 1; - break; - case 'warning': - this.warningCount += 1; - break; - case 'info': - this.infoCount += 1; - break; - } - }); - } - - get highestDiagnosticLevel(): Diagnostic['severity'] | null { - if (this.errorCount > 0) { - return 'error'; - } - if (this.warningCount > 0) { - return 'warning'; - } - if (this.infoCount > 0) { - return 'info'; - } - return null; - } - - updateSemanticHighlighting(ranges: IHighlightRange[]): void { - this.dispatch(setSemanticHighlighting(ranges)); - } - - updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void { - this.dispatch(setOccurrences(write, read)); - } - - /** - * @returns `true` if there is history to undo - */ - get canUndo(): boolean { - return undoDepth(this.state) > 0; - } - - // eslint-disable-next-line class-methods-use-this - undo(): void { - log.debug('Undo', this.doStateCommand(undo)); - } - - /** - * @returns `true` if there is history to redo - */ - get canRedo(): boolean { - return redoDepth(this.state) > 0; - } - - // eslint-disable-next-line class-methods-use-this - redo(): void { - log.debug('Redo', this.doStateCommand(redo)); - } - - toggleLineNumbers(): void { - this.showLineNumbers = !this.showLineNumbers; - log.debug('Show line numbers', this.showLineNumbers); - } - - /** - * Sets whether the CodeMirror search panel should be open. - * - * This method can be used as a CodeMirror command, - * because it returns `false` if it didn't execute, - * allowing other commands for the same keybind to run instead. - * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` - * commands from `'@codemirror/search'`. - * - * @param newShosSearchPanel whether we should show the search panel - * @returns `true` if the state was changed, `false` otherwise - */ - setSearchPanelOpen(newShowSearchPanel: boolean): boolean { - if (this.showSearchPanel === newShowSearchPanel) { - return false; - } - this.showSearchPanel = newShowSearchPanel; - log.debug('Show search panel', this.showSearchPanel); - return true; - } - - toggleSearchPanel(): void { - this.setSearchPanelOpen(!this.showSearchPanel); - } - - setLintPanelOpen(newShowLintPanel: boolean): boolean { - if (this.showLintPanel === newShowLintPanel) { - return false; - } - this.showLintPanel = newShowLintPanel; - log.debug('Show lint panel', this.showLintPanel); - return true; - } - - toggleLintPanel(): void { - this.setLintPanelOpen(!this.showLintPanel); - } - - formatText(): boolean { - this.client.formatText(); - return true; - } -} diff --git a/language-web/src/main/js/editor/GenerateButton.tsx b/language-web/src/main/js/editor/GenerateButton.tsx deleted file mode 100644 index 3834cec4..00000000 --- a/language-web/src/main/js/editor/GenerateButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import Button from '@mui/material/Button'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import React from 'react'; - -import { useRootStore } from '../RootStore'; - -const GENERATE_LABEL = 'Generate'; - -export const GenerateButton = observer(() => { - const { editorStore } = useRootStore(); - const { errorCount, warningCount } = editorStore; - - const diagnostics: string[] = []; - if (errorCount > 0) { - diagnostics.push(`${errorCount} error${errorCount === 1 ? '' : 's'}`); - } - if (warningCount > 0) { - diagnostics.push(`${warningCount} warning${warningCount === 1 ? '' : 's'}`); - } - const summary = diagnostics.join(' and '); - - if (errorCount > 0) { - return ( - - ); - } - - return ( - - ); -}); diff --git a/language-web/src/main/js/editor/decorationSetExtension.ts b/language-web/src/main/js/editor/decorationSetExtension.ts deleted file mode 100644 index 2d630c20..00000000 --- a/language-web/src/main/js/editor/decorationSetExtension.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; -import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; - -export type TransactionSpecFactory = (decorations: DecorationSet) => TransactionSpec; - -export function decorationSetExtension(): [TransactionSpecFactory, StateField] { - const setEffect = StateEffect.define(); - const field = StateField.define({ - create() { - return Decoration.none; - }, - update(currentDecorations, transaction) { - let newDecorations: DecorationSet | null = null; - transaction.effects.forEach((effect) => { - if (effect.is(setEffect)) { - newDecorations = effect.value; - } - }); - if (newDecorations === null) { - if (transaction.docChanged) { - return currentDecorations.map(transaction.changes); - } - return currentDecorations; - } - return newDecorations; - }, - provide: (f) => EditorView.decorations.from(f), - }); - - function transactionSpecFactory(decorations: DecorationSet) { - return { - effects: [ - setEffect.of(decorations), - ], - }; - } - - return [transactionSpecFactory, field]; -} diff --git a/language-web/src/main/js/editor/findOccurrences.ts b/language-web/src/main/js/editor/findOccurrences.ts deleted file mode 100644 index 92102746..00000000 --- a/language-web/src/main/js/editor/findOccurrences.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Range, RangeSet } from '@codemirror/rangeset'; -import type { TransactionSpec } from '@codemirror/state'; -import { Decoration } from '@codemirror/view'; - -import { decorationSetExtension } from './decorationSetExtension'; - -export interface IOccurrence { - from: number; - - to: number; -} - -const [setOccurrencesInteral, findOccurrences] = decorationSetExtension(); - -const writeDecoration = Decoration.mark({ - class: 'cm-problem-write', -}); - -const readDecoration = Decoration.mark({ - class: 'cm-problem-read', -}); - -export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): TransactionSpec { - const decorations: Range[] = []; - write.forEach(({ from, to }) => { - decorations.push(writeDecoration.range(from, to)); - }); - read.forEach(({ from, to }) => { - decorations.push(readDecoration.range(from, to)); - }); - const rangeSet = RangeSet.of(decorations, true); - return setOccurrencesInteral(rangeSet); -} - -export { findOccurrences }; diff --git a/language-web/src/main/js/editor/semanticHighlighting.ts b/language-web/src/main/js/editor/semanticHighlighting.ts deleted file mode 100644 index 2aed421b..00000000 --- a/language-web/src/main/js/editor/semanticHighlighting.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RangeSet } from '@codemirror/rangeset'; -import type { TransactionSpec } from '@codemirror/state'; -import { Decoration } from '@codemirror/view'; - -import { decorationSetExtension } from './decorationSetExtension'; - -export interface IHighlightRange { - from: number; - - to: number; - - classes: string[]; -} - -const [setSemanticHighlightingInternal, semanticHighlighting] = decorationSetExtension(); - -export function setSemanticHighlighting(ranges: IHighlightRange[]): TransactionSpec { - const rangeSet = RangeSet.of(ranges.map(({ from, to, classes }) => Decoration.mark({ - class: classes.map((c) => `cmt-problem-${c}`).join(' '), - }).range(from, to)), true); - return setSemanticHighlightingInternal(rangeSet); -} - -export { semanticHighlighting }; diff --git a/language-web/src/main/js/global.d.ts b/language-web/src/main/js/global.d.ts deleted file mode 100644 index 0533a46e..00000000 --- a/language-web/src/main/js/global.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare const DEBUG: boolean; - -declare const PACKAGE_NAME: string; - -declare const PACKAGE_VERSION: string; - -declare module '*.module.scss' { - const cssVariables: { [key in string]?: string }; - // eslint-disable-next-line import/no-default-export - export default cssVariables; -} diff --git a/language-web/src/main/js/index.tsx b/language-web/src/main/js/index.tsx deleted file mode 100644 index d368c9ba..00000000 --- a/language-web/src/main/js/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { render } from 'react-dom'; -import CssBaseline from '@mui/material/CssBaseline'; - -import { App } from './App'; -import { RootStore, RootStoreProvider } from './RootStore'; -import { ThemeProvider } from './theme/ThemeProvider'; - -import '../css/index.scss'; - -const initialValue = `class Family { - contains Person[] members -} - -class Person { - Person[] children opposite parent - Person[0..1] parent opposite children - int age - TaxStatus taxStatus -} - -enum TaxStatus { - child, student, adult, retired -} - -% A child cannot have any dependents. -pred invalidTaxStatus(Person p) <-> - taxStatus(p, child), - children(p, _q) - ; taxStatus(p, retired), - parent(p, q), - !taxStatus(q, retired). - -direct rule createChild(p): - children(p, newPerson) = unknown, - equals(newPerson, newPerson) = unknown - ~> new q, - children(p, q) = true, - taxStatus(q, child) = true. - -indiv family. -Family(family). -members(family, anne). -members(family, bob). -members(family, ciri). -children(anne, ciri). -?children(bob, ciri). -default children(ciri, *): false. -taxStatus(anne, adult). -age(anne, 35). -bobAge: 27. -age(bob, bobAge). -!age(ciri, bobAge). - -scope Family = 1, Person += 5..10. -`; - -const rootStore = new RootStore(initialValue); - -const app = ( - - - - - - -); - -render(app, document.getElementById('app')); diff --git a/language-web/src/main/js/language/folding.ts b/language-web/src/main/js/language/folding.ts deleted file mode 100644 index 5d51f796..00000000 --- a/language-web/src/main/js/language/folding.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { EditorState } from '@codemirror/state'; -import type { SyntaxNode } from '@lezer/common'; - -export type FoldRange = { from: number, to: number }; - -/** - * Folds a block comment between its delimiters. - * - * @param node the node to fold - * @returns the folding range or `null` is there is nothing to fold - */ -export function foldBlockComment(node: SyntaxNode): FoldRange { - return { - from: node.from + 2, - to: node.to - 2, - }; -} - -/** - * Folds a declaration after the first element if it appears on the opening line, - * otherwise folds after the opening keyword. - * - * @example - * First element on the opening line: - * ``` - * scope Family = 1, - * Person += 5..10. - * ``` - * becomes - * ``` - * scope Family = 1,[...]. - * ``` - * - * @example - * First element not on the opening line: - * ``` - * scope Family - * = 1, - * Person += 5..10. - * ``` - * becomes - * ``` - * scope [...]. - * ``` - * - * @param node the node to fold - * @param state the editor state - * @returns the folding range or `null` is there is nothing to fold - */ -export function foldDeclaration(node: SyntaxNode, state: EditorState): FoldRange | null { - const { firstChild: open, lastChild: close } = node; - if (open === null || close === null) { - return null; - } - const { cursor } = open; - const lineEnd = state.doc.lineAt(open.from).to; - let foldFrom = open.to; - while (cursor.next() && cursor.from < lineEnd) { - if (cursor.type.name === ',') { - foldFrom = cursor.to; - break; - } - } - return { - from: foldFrom, - to: close.from, - }; -} - -/** - * Folds a node only if it has at least one sibling of the same type. - * - * The folding range will be the entire `node`. - * - * @param node the node to fold - * @returns the folding range or `null` is there is nothing to fold - */ -function foldWithSibling(node: SyntaxNode): FoldRange | null { - const { parent } = node; - if (parent === null) { - return null; - } - const { firstChild } = parent; - if (firstChild === null) { - return null; - } - const { cursor } = firstChild; - let nSiblings = 0; - while (cursor.nextSibling()) { - if (cursor.type === node.type) { - nSiblings += 1; - } - if (nSiblings >= 2) { - return { - from: node.from, - to: node.to, - }; - } - } - return null; -} - -export function foldWholeNode(node: SyntaxNode): FoldRange { - return { - from: node.from, - to: node.to, - }; -} - -export function foldConjunction(node: SyntaxNode): FoldRange | null { - if (node.parent?.type?.name === 'PredicateBody') { - return foldWithSibling(node); - } - return foldWholeNode(node); -} diff --git a/language-web/src/main/js/language/indentation.ts b/language-web/src/main/js/language/indentation.ts deleted file mode 100644 index 6d36ed3b..00000000 --- a/language-web/src/main/js/language/indentation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { TreeIndentContext } from '@codemirror/language'; - -/** - * Finds the `from` of first non-skipped token, if any, - * after the opening keyword in the first line of the declaration. - * - * Based on - * https://github.com/codemirror/language/blob/cd7f7e66fa51ddbce96cf9396b1b6127d0ca4c94/src/indent.ts#L246 - * - * @param context the indentation context - * @returns the alignment or `null` if there is no token after the opening keyword - */ -function findAlignmentAfterOpening(context: TreeIndentContext): number | null { - const { - node: tree, - simulatedBreak, - } = context; - const openingToken = tree.childAfter(tree.from); - if (openingToken === null) { - return null; - } - const openingLine = context.state.doc.lineAt(openingToken.from); - const lineEnd = simulatedBreak == null || simulatedBreak <= openingLine.from - ? openingLine.to - : Math.min(openingLine.to, simulatedBreak); - const { cursor } = openingToken; - while (cursor.next() && cursor.from < lineEnd) { - if (!cursor.type.isSkipped) { - return cursor.from; - } - } - return null; -} - -/** - * Indents text after declarations by a single unit if it begins on a new line, - * otherwise it aligns with the text after the declaration. - * - * Based on - * https://github.com/codemirror/language/blob/cd7f7e66fa51ddbce96cf9396b1b6127d0ca4c94/src/indent.ts#L275 - * - * @example - * Result with no hanging indent (indent unit = 2 spaces, units = 1): - * ``` - * scope - * Family = 1, - * Person += 5..10. - * ``` - * - * @example - * Result with hanging indent: - * ``` - * scope Family = 1, - * Person += 5..10. - * ``` - * - * @param context the indentation context - * @param units the number of units to indent - * @returns the desired indentation level - */ -function indentDeclarationStrategy(context: TreeIndentContext, units: number): number { - const alignment = findAlignmentAfterOpening(context); - if (alignment !== null) { - return context.column(alignment); - } - return context.baseIndent + units * context.unit; -} - -export function indentBlockComment(): number { - // Do not indent. - return -1; -} - -export function indentDeclaration(context: TreeIndentContext): number { - return indentDeclarationStrategy(context, 1); -} - -export function indentPredicateOrRule(context: TreeIndentContext): number { - const clauseIndent = indentDeclarationStrategy(context, 1); - if (/^\s+[;.]/.exec(context.textAfter) !== null) { - return clauseIndent - 2; - } - if (/^\s+(~>)/.exec(context.textAfter) !== null) { - return clauseIndent - 3; - } - return clauseIndent; -} diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar deleted file mode 100644 index bccc2e31..00000000 --- a/language-web/src/main/js/language/problem.grammar +++ /dev/null @@ -1,149 +0,0 @@ -@detectDelim - -@external prop implicitCompletion from '../../../../src/main/js/language/props.ts' - -@top Problem { statement* } - -statement { - ProblemDeclaration { - ckw<"problem"> QualifiedName "." - } | - ClassDefinition { - ckw<"abstract">? ckw<"class"> RelationName - (ckw<"extends"> sep<",", RelationName>)? - (ClassBody { "{" ReferenceDeclaration* "}" } | ".") - } | - EnumDefinition { - ckw<"enum"> RelationName - (EnumBody { "{" sep<",", IndividualNodeName> "}" } | ".") - } | - PredicateDefinition { - (ckw<"error"> ckw<"pred">? | ckw<"direct">? ckw<"pred">) - RelationName ParameterList? - PredicateBody { ("<->" sep)? "." } - } | - RuleDefinition { - ckw<"direct">? ckw<"rule"> - RuleName ParameterList? - RuleBody { ":" sep "~>" sep "." } - } | - Assertion { - kw<"default">? (NotOp | UnknownOp)? RelationName - ParameterList (":" LogicValue)? "." - } | - NodeValueAssertion { - IndividualNodeName ":" Constant "." - } | - IndividualDeclaration { - ckw<"indiv"> sep<",", IndividualNodeName> "." - } | - ScopeDeclaration { - kw<"scope"> sep<",", ScopeElement> "." - } -} - -ReferenceDeclaration { - (kw<"refers"> | kw<"contains">)? - RelationName - RelationName - ( "[" Multiplicity? "]" )? - (kw<"opposite"> RelationName)? - ";"? -} - -Parameter { RelationName? VariableName } - -Conjunction { ("," | Literal)+ } - -OrOp { ";" } - -Literal { NotOp? Atom (("=" | ":") sep1<"|", LogicValue>)? } - -Atom { RelationName "+"? ParameterList } - -Action { ("," | ActionLiteral)+ } - -ActionLiteral { - ckw<"new"> VariableName | - ckw<"delete"> VariableName | - Literal -} - -Argument { VariableName | Constant } - -AssertionArgument { NodeName | StarArgument | Constant } - -Constant { Real | String } - -LogicValue { - ckw<"true"> | ckw<"false"> | ckw<"unknown"> | ckw<"error"> -} - -ScopeElement { RelationName ("=" | "+=") Multiplicity } - -Multiplicity { (IntMult "..")? (IntMult | StarMult)} - -RelationName { QualifiedName } - -RuleName { QualifiedName } - -IndividualNodeName { QualifiedName } - -VariableName { QualifiedName } - -NodeName { QualifiedName } - -QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* } - -kw { @specialize[@name={term},implicitCompletion=true] } - -ckw { @extend[@name={term},implicitCompletion=true] } - -ParameterList { "(" sep<",", content> ")" } - -sep { sep1? } - -sep1 { content (separator content)* } - -@skip { LineComment | BlockComment | whitespace } - -@tokens { - whitespace { std.whitespace+ } - - LineComment { ("//" | "%") ![\n]* } - - BlockComment { "/*" blockCommentRest } - - blockCommentRest { ![*] blockCommentRest | "*" blockCommentAfterStar } - - blockCommentAfterStar { "/" | "*" blockCommentAfterStar | ![/*] blockCommentRest } - - @precedence { BlockComment, LineComment } - - identifier { $[A-Za-z_] $[a-zA-Z0-9_]* } - - int { $[0-9]+ } - - IntMult { int } - - StarMult { "*" } - - Real { "-"? (exponential | int ("." (int | exponential))?) } - - exponential { int ("e" | "E") ("+" | "-")? int } - - String { - "'" (![\\'\n] | "\\" ![\n] | "\\\n")+ "'" | - "\"" (![\\"\n] | "\\" (![\n] | "\n"))* "\"" - } - - NotOp { "!" } - - UnknownOp { "?" } - - StarArgument { "*" } - - "{" "}" "(" ")" "[" "]" "." ".." "," ":" "<->" "~>" -} - -@detectDelim diff --git a/language-web/src/main/js/language/problemLanguageSupport.ts b/language-web/src/main/js/language/problemLanguageSupport.ts deleted file mode 100644 index 6508a2c0..00000000 --- a/language-web/src/main/js/language/problemLanguageSupport.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { styleTags, tags as t } from '@codemirror/highlight'; -import { - foldInside, - foldNodeProp, - indentNodeProp, - indentUnit, - LanguageSupport, - LRLanguage, -} from '@codemirror/language'; -import { LRParser } from '@lezer/lr'; - -import { parser } from '../../../../build/generated/sources/lezer/problem'; -import { - foldBlockComment, - foldConjunction, - foldDeclaration, - foldWholeNode, -} from './folding'; -import { - indentBlockComment, - indentDeclaration, - indentPredicateOrRule, -} from './indentation'; - -const parserWithMetadata = (parser as LRParser).configure({ - props: [ - styleTags({ - LineComment: t.lineComment, - BlockComment: t.blockComment, - 'problem class enum pred rule indiv scope': t.definitionKeyword, - 'abstract extends refers contains opposite error direct default': t.modifier, - 'true false unknown error': t.keyword, - 'new delete': t.operatorKeyword, - NotOp: t.keyword, - UnknownOp: t.keyword, - OrOp: t.keyword, - StarArgument: t.keyword, - 'IntMult StarMult Real': t.number, - StarMult: t.number, - String: t.string, - 'RelationName/QualifiedName': t.typeName, - 'RuleName/QualifiedName': t.macroName, - 'IndividualNodeName/QualifiedName': t.atom, - 'VariableName/QualifiedName': t.variableName, - '{ }': t.brace, - '( )': t.paren, - '[ ]': t.squareBracket, - '. .. , :': t.separator, - '<-> ~>': t.definitionOperator, - }), - indentNodeProp.add({ - ProblemDeclaration: indentDeclaration, - UniqueDeclaration: indentDeclaration, - ScopeDeclaration: indentDeclaration, - PredicateBody: indentPredicateOrRule, - RuleBody: indentPredicateOrRule, - BlockComment: indentBlockComment, - }), - foldNodeProp.add({ - ClassBody: foldInside, - EnumBody: foldInside, - ParameterList: foldInside, - PredicateBody: foldInside, - RuleBody: foldInside, - Conjunction: foldConjunction, - Action: foldWholeNode, - UniqueDeclaration: foldDeclaration, - ScopeDeclaration: foldDeclaration, - BlockComment: foldBlockComment, - }), - ], -}); - -const problemLanguage = LRLanguage.define({ - parser: parserWithMetadata, - languageData: { - commentTokens: { - block: { - open: '/*', - close: '*/', - }, - line: '%', - }, - indentOnInput: /^\s*(?:\{|\}|\(|\)|;|\.|~>)$/, - }, -}); - -export function problemLanguageSupport(): LanguageSupport { - return new LanguageSupport(problemLanguage, [ - indentUnit.of(' '), - ]); -} diff --git a/language-web/src/main/js/language/props.ts b/language-web/src/main/js/language/props.ts deleted file mode 100644 index 8e488bf5..00000000 --- a/language-web/src/main/js/language/props.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NodeProp } from '@lezer/common'; - -export const implicitCompletion = new NodeProp({ - deserialize(s: string) { - return s === 'true'; - }, -}); diff --git a/language-web/src/main/js/theme/EditorTheme.ts b/language-web/src/main/js/theme/EditorTheme.ts deleted file mode 100644 index 1b0dd5de..00000000 --- a/language-web/src/main/js/theme/EditorTheme.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { PaletteMode } from '@mui/material'; - -import cssVariables from '../../css/themeVariables.module.scss'; - -export enum EditorTheme { - Light, - Dark, -} - -export class EditorThemeData { - className: string; - - paletteMode: PaletteMode; - - toggleDarkMode: EditorTheme; - - foreground!: string; - - foregroundHighlight!: string; - - background!: string; - - primary!: string; - - secondary!: string; - - constructor(className: string, paletteMode: PaletteMode, toggleDarkMode: EditorTheme) { - this.className = className; - this.paletteMode = paletteMode; - this.toggleDarkMode = toggleDarkMode; - Reflect.ownKeys(this).forEach((key) => { - if (!Reflect.get(this, key)) { - const cssKey = `${this.className}--${key.toString()}`; - if (cssKey in cssVariables) { - Reflect.set(this, key, cssVariables[cssKey]); - } - } - }); - } -} - -export const DEFAULT_THEME = EditorTheme.Dark; - -export const EDITOR_THEMES: { [key in EditorTheme]: EditorThemeData } = { - [EditorTheme.Light]: new EditorThemeData('light', 'light', EditorTheme.Dark), - [EditorTheme.Dark]: new EditorThemeData('dark', 'dark', EditorTheme.Light), -}; diff --git a/language-web/src/main/js/theme/ThemeProvider.tsx b/language-web/src/main/js/theme/ThemeProvider.tsx deleted file mode 100644 index f5b50be1..00000000 --- a/language-web/src/main/js/theme/ThemeProvider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { ThemeProvider as MaterialUiThemeProvider } from '@mui/material/styles'; -import React from 'react'; - -import { useRootStore } from '../RootStore'; - -export const ThemeProvider: React.FC = observer(({ children }) => { - const { themeStore } = useRootStore(); - - return ( - - {children} - - ); -}); diff --git a/language-web/src/main/js/theme/ThemeStore.ts b/language-web/src/main/js/theme/ThemeStore.ts deleted file mode 100644 index ffaf6dde..00000000 --- a/language-web/src/main/js/theme/ThemeStore.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { makeAutoObservable } from 'mobx'; -import { - Theme, - createTheme, - responsiveFontSizes, -} from '@mui/material/styles'; - -import { - EditorTheme, - EditorThemeData, - DEFAULT_THEME, - EDITOR_THEMES, -} from './EditorTheme'; - -export class ThemeStore { - currentTheme: EditorTheme = DEFAULT_THEME; - - constructor() { - makeAutoObservable(this); - } - - toggleDarkMode(): void { - this.currentTheme = this.currentThemeData.toggleDarkMode; - } - - private get currentThemeData(): EditorThemeData { - return EDITOR_THEMES[this.currentTheme]; - } - - get materialUiTheme(): Theme { - const themeData = this.currentThemeData; - const materialUiTheme = createTheme({ - palette: { - mode: themeData.paletteMode, - background: { - default: themeData.background, - paper: themeData.background, - }, - primary: { - main: themeData.primary, - }, - secondary: { - main: themeData.secondary, - }, - error: { - main: themeData.secondary, - }, - text: { - primary: themeData.foregroundHighlight, - secondary: themeData.foreground, - }, - }, - }); - return responsiveFontSizes(materialUiTheme); - } - - get darkMode(): boolean { - return this.currentThemeData.paletteMode === 'dark'; - } - - get className(): string { - return this.currentThemeData.className; - } -} diff --git a/language-web/src/main/js/utils/ConditionVariable.ts b/language-web/src/main/js/utils/ConditionVariable.ts deleted file mode 100644 index 0910dfa6..00000000 --- a/language-web/src/main/js/utils/ConditionVariable.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { getLogger } from './logger'; -import { PendingTask } from './PendingTask'; - -const log = getLogger('utils.ConditionVariable'); - -export type Condition = () => boolean; - -export class ConditionVariable { - condition: Condition; - - defaultTimeout: number; - - listeners: PendingTask[] = []; - - constructor(condition: Condition, defaultTimeout = 0) { - this.condition = condition; - this.defaultTimeout = defaultTimeout; - } - - async waitFor(timeoutMs: number | null = null): Promise { - if (this.condition()) { - return; - } - const timeoutOrDefault = timeoutMs || this.defaultTimeout; - let nowMs = Date.now(); - const endMs = nowMs + timeoutOrDefault; - while (!this.condition() && nowMs < endMs) { - const remainingMs = endMs - nowMs; - const promise = new Promise((resolve, reject) => { - if (this.condition()) { - resolve(); - return; - } - const task = new PendingTask(resolve, reject, remainingMs); - this.listeners.push(task); - }); - // We must keep waiting until the update has completed, - // so the tasks can't be started in parallel. - // eslint-disable-next-line no-await-in-loop - await promise; - nowMs = Date.now(); - } - if (!this.condition()) { - log.error('Condition still does not hold after', timeoutOrDefault, 'ms'); - throw new Error('Failed to wait for condition'); - } - } - - notifyAll(): void { - this.clearListenersWith((listener) => listener.resolve()); - } - - rejectAll(error: unknown): void { - this.clearListenersWith((listener) => listener.reject(error)); - } - - private clearListenersWith(callback: (listener: PendingTask) => void) { - // Copy `listeners` so that we don't get into a race condition - // if one of the listeners adds another listener. - const { listeners } = this; - this.listeners = []; - listeners.forEach(callback); - } -} diff --git a/language-web/src/main/js/utils/PendingTask.ts b/language-web/src/main/js/utils/PendingTask.ts deleted file mode 100644 index 51b79fb0..00000000 --- a/language-web/src/main/js/utils/PendingTask.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getLogger } from './logger'; - -const log = getLogger('utils.PendingTask'); - -export class PendingTask { - private readonly resolveCallback: (value: T) => void; - - private readonly rejectCallback: (reason?: unknown) => void; - - private resolved = false; - - private timeout: number | null; - - constructor( - resolveCallback: (value: T) => void, - rejectCallback: (reason?: unknown) => void, - timeoutMs?: number, - timeoutCallback?: () => void, - ) { - this.resolveCallback = resolveCallback; - this.rejectCallback = rejectCallback; - if (timeoutMs) { - this.timeout = setTimeout(() => { - if (!this.resolved) { - this.reject(new Error('Request timed out')); - if (timeoutCallback) { - timeoutCallback(); - } - } - }, timeoutMs); - } else { - this.timeout = null; - } - } - - resolve(value: T): void { - if (this.resolved) { - log.warn('Trying to resolve already resolved promise'); - return; - } - this.markResolved(); - this.resolveCallback(value); - } - - reject(reason?: unknown): void { - if (this.resolved) { - log.warn('Trying to reject already resolved promise'); - return; - } - this.markResolved(); - this.rejectCallback(reason); - } - - private markResolved() { - this.resolved = true; - if (this.timeout !== null) { - clearTimeout(this.timeout); - } - } -} diff --git a/language-web/src/main/js/utils/Timer.ts b/language-web/src/main/js/utils/Timer.ts deleted file mode 100644 index 8f653070..00000000 --- a/language-web/src/main/js/utils/Timer.ts +++ /dev/null @@ -1,33 +0,0 @@ -export class Timer { - readonly callback: () => void; - - readonly defaultTimeout: number; - - timeout: number | null = null; - - constructor(callback: () => void, defaultTimeout = 0) { - this.callback = () => { - this.timeout = null; - callback(); - }; - this.defaultTimeout = defaultTimeout; - } - - schedule(timeout: number | null = null): void { - if (this.timeout === null) { - this.timeout = setTimeout(this.callback, timeout || this.defaultTimeout); - } - } - - reschedule(timeout: number | null = null): void { - this.cancel(); - this.schedule(timeout); - } - - cancel(): void { - if (this.timeout !== null) { - clearTimeout(this.timeout); - this.timeout = null; - } - } -} diff --git a/language-web/src/main/js/utils/logger.ts b/language-web/src/main/js/utils/logger.ts deleted file mode 100644 index 306d122c..00000000 --- a/language-web/src/main/js/utils/logger.ts +++ /dev/null @@ -1,49 +0,0 @@ -import styles, { CSPair } from 'ansi-styles'; -import log from 'loglevel'; -import * as prefix from 'loglevel-plugin-prefix'; - -const colors: Partial> = { - TRACE: styles.magenta, - DEBUG: styles.cyan, - INFO: styles.blue, - WARN: styles.yellow, - ERROR: styles.red, -}; - -prefix.reg(log); - -if (DEBUG) { - log.setLevel(log.levels.DEBUG); -} else { - log.setLevel(log.levels.WARN); -} - -if ('chrome' in window) { - // Only Chromium supports console ANSI escape sequences. - prefix.apply(log, { - format(level, name, timestamp) { - const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${styles.gray.close}`; - const levelColor = colors[level.toUpperCase()] || styles.red; - const formattedLevel = `${levelColor.open}${level}${levelColor.close}`; - const formattedName = `${styles.green.open}(${name || 'root'})${styles.green.close}`; - return `${formattedTimestamp} ${formattedLevel} ${formattedName}`; - }, - }); -} else { - prefix.apply(log, { - template: '[%t] %l (%n)', - }); -} - -const appLogger = log.getLogger(PACKAGE_NAME); - -appLogger.info('Version:', PACKAGE_NAME, PACKAGE_VERSION); -appLogger.info('Debug mode:', DEBUG); - -export function getLoggerFromRoot(name: string | symbol): log.Logger { - return log.getLogger(name); -} - -export function getLogger(name: string | symbol): log.Logger { - return getLoggerFromRoot(`${PACKAGE_NAME}.${name.toString()}`); -} diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts deleted file mode 100644 index 8b872e06..00000000 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { - Completion, - CompletionContext, - CompletionResult, -} from '@codemirror/autocomplete'; -import { syntaxTree } from '@codemirror/language'; -import type { Transaction } from '@codemirror/state'; -import escapeStringRegexp from 'escape-string-regexp'; - -import { implicitCompletion } from '../language/props'; -import type { UpdateService } from './UpdateService'; -import { getLogger } from '../utils/logger'; -import type { ContentAssistEntry } from './xtextServiceResults'; - -const PROPOSALS_LIMIT = 1000; - -const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; - -const HIGH_PRIORITY_KEYWORDS = ['<->', '~>']; - -const log = getLogger('xtext.ContentAssistService'); - -interface IFoundToken { - from: number; - - to: number; - - implicitCompletion: boolean; - - text: string; -} - -function findToken({ pos, state }: CompletionContext): IFoundToken | null { - const token = syntaxTree(state).resolveInner(pos, -1); - if (token === null) { - return null; - } - if (token.firstChild !== null) { - // We only autocomplete terminal nodes. If the current node is nonterminal, - // returning `null` makes us autocomplete with the empty prefix instead. - return null; - } - return { - from: token.from, - to: token.to, - implicitCompletion: token.type.prop(implicitCompletion) || false, - text: state.sliceDoc(token.from, token.to), - }; -} - -function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { - return token !== null - && token.implicitCompletion - && context.pos - token.from >= 2; -} - -function computeSpan(prefix: string, entryCount: number): RegExp { - const escapedPrefix = escapeStringRegexp(prefix); - if (entryCount < PROPOSALS_LIMIT) { - // Proposals with the current prefix fit the proposals limit. - // We can filter client side as long as the current prefix is preserved. - return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); - } - // The current prefix overflows the proposals limits, - // so we have to fetch the completions again on the next keypress. - // Hopefully, it'll return a shorter list and we'll be able to filter client side. - return new RegExp(`^${escapedPrefix}$`); -} - -function createCompletion(entry: ContentAssistEntry): Completion { - let boost: number; - switch (entry.kind) { - case 'KEYWORD': - // Some hard-to-type operators should be on top. - boost = HIGH_PRIORITY_KEYWORDS.includes(entry.proposal) ? 10 : -99; - break; - case 'TEXT': - case 'SNIPPET': - boost = -90; - break; - default: { - // Penalize qualified names (vs available unqualified names). - const extraSegments = entry.proposal.match(/::/g)?.length || 0; - boost = Math.max(-5 * extraSegments, -50); - } - break; - } - return { - label: entry.proposal, - detail: entry.description, - info: entry.documentation, - type: entry.kind?.toLowerCase(), - boost, - }; -} - -export class ContentAssistService { - private readonly updateService: UpdateService; - - private lastCompletion: CompletionResult | null = null; - - constructor(updateService: UpdateService) { - this.updateService = updateService; - } - - onTransaction(transaction: Transaction): void { - if (this.shouldInvalidateCachedCompletion(transaction)) { - this.lastCompletion = null; - } - } - - async contentAssist(context: CompletionContext): Promise { - const tokenBefore = findToken(context); - if (!context.explicit && !shouldCompleteImplicitly(tokenBefore, context)) { - return { - from: context.pos, - options: [], - }; - } - let range: { from: number, to: number }; - let prefix = ''; - if (tokenBefore === null) { - range = { - from: context.pos, - to: context.pos, - }; - prefix = ''; - } else { - range = { - from: tokenBefore.from, - to: tokenBefore.to, - }; - const prefixLength = context.pos - tokenBefore.from; - if (prefixLength > 0) { - prefix = tokenBefore.text.substring(0, context.pos - tokenBefore.from); - } - } - if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) { - log.trace('Returning cached completion result'); - // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` - return { - ...this.lastCompletion as CompletionResult, - ...range, - }; - } - this.lastCompletion = null; - const entries = await this.updateService.fetchContentAssist({ - resource: this.updateService.resourceName, - serviceType: 'assist', - caretOffset: context.pos, - proposalsLimit: PROPOSALS_LIMIT, - }, context); - if (context.aborted) { - return { - ...range, - options: [], - }; - } - const options: Completion[] = []; - entries.forEach((entry) => { - if (prefix === entry.prefix) { - // Xtext will generate completions that do not complete the current token, - // e.g., `(` after trying to complete an indetifier, - // but we ignore those, since CodeMirror won't filter for them anyways. - options.push(createCompletion(entry)); - } - }); - log.debug('Fetched', options.length, 'completions from server'); - this.lastCompletion = { - ...range, - options, - span: computeSpan(prefix, entries.length), - }; - return this.lastCompletion; - } - - private shouldReturnCachedCompletion( - token: { from: number, to: number, text: string } | null, - ): boolean { - if (token === null || this.lastCompletion === null) { - return false; - } - const { from, to, text } = token; - const { from: lastFrom, to: lastTo, span } = this.lastCompletion; - if (!lastTo) { - return true; - } - const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); - return from >= transformedFrom - && to <= transformedTo - && typeof span !== 'undefined' - && span.exec(text) !== null; - } - - private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { - if (!transaction.docChanged || this.lastCompletion === null) { - return false; - } - const { from: lastFrom, to: lastTo } = this.lastCompletion; - if (!lastTo) { - return true; - } - const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); - let invalidate = false; - transaction.changes.iterChangedRanges((fromA, toA) => { - if (fromA < transformedFrom || toA > transformedTo) { - invalidate = true; - } - }); - return invalidate; - } - - private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] { - const changes = this.updateService.computeChangesSinceLastUpdate(); - const transformedFrom = changes.mapPos(lastFrom); - const transformedTo = changes.mapPos(lastTo, 1); - return [transformedFrom, transformedTo]; - } -} diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts deleted file mode 100644 index dfbb4a19..00000000 --- a/language-web/src/main/js/xtext/HighlightingService.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { EditorStore } from '../editor/EditorStore'; -import type { IHighlightRange } from '../editor/semanticHighlighting'; -import type { UpdateService } from './UpdateService'; -import { highlightingResult } from './xtextServiceResults'; - -export class HighlightingService { - private readonly store: EditorStore; - - private readonly updateService: UpdateService; - - constructor(store: EditorStore, updateService: UpdateService) { - this.store = store; - this.updateService = updateService; - } - - onPush(push: unknown): void { - const { regions } = highlightingResult.parse(push); - const allChanges = this.updateService.computeChangesSinceLastUpdate(); - const ranges: IHighlightRange[] = []; - regions.forEach(({ offset, length, styleClasses }) => { - if (styleClasses.length === 0) { - return; - } - const from = allChanges.mapPos(offset); - const to = allChanges.mapPos(offset + length); - if (to <= from) { - return; - } - ranges.push({ - from, - to, - classes: styleClasses, - }); - }); - this.store.updateSemanticHighlighting(ranges); - } -} diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts deleted file mode 100644 index bc865537..00000000 --- a/language-web/src/main/js/xtext/OccurrencesService.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Transaction } from '@codemirror/state'; - -import type { EditorStore } from '../editor/EditorStore'; -import type { IOccurrence } from '../editor/findOccurrences'; -import type { UpdateService } from './UpdateService'; -import { getLogger } from '../utils/logger'; -import { Timer } from '../utils/Timer'; -import { XtextWebSocketClient } from './XtextWebSocketClient'; -import { - isConflictResult, - occurrencesResult, - TextRegion, -} from './xtextServiceResults'; - -const FIND_OCCURRENCES_TIMEOUT_MS = 1000; - -// Must clear occurrences asynchronously from `onTransaction`, -// because we must not emit a conflicting transaction when handling the pending transaction. -const CLEAR_OCCURRENCES_TIMEOUT_MS = 10; - -const log = getLogger('xtext.OccurrencesService'); - -function transformOccurrences(regions: TextRegion[]): IOccurrence[] { - const occurrences: IOccurrence[] = []; - regions.forEach(({ offset, length }) => { - if (length > 0) { - occurrences.push({ - from: offset, - to: offset + length, - }); - } - }); - return occurrences; -} - -export class OccurrencesService { - private readonly store: EditorStore; - - private readonly webSocketClient: XtextWebSocketClient; - - private readonly updateService: UpdateService; - - private hasOccurrences = false; - - private readonly findOccurrencesTimer = new Timer(() => { - this.handleFindOccurrences(); - }, FIND_OCCURRENCES_TIMEOUT_MS); - - private readonly clearOccurrencesTimer = new Timer(() => { - this.clearOccurrences(); - }, CLEAR_OCCURRENCES_TIMEOUT_MS); - - constructor( - store: EditorStore, - webSocketClient: XtextWebSocketClient, - updateService: UpdateService, - ) { - this.store = store; - this.webSocketClient = webSocketClient; - this.updateService = updateService; - } - - onTransaction(transaction: Transaction): void { - if (transaction.docChanged) { - this.clearOccurrencesTimer.schedule(); - this.findOccurrencesTimer.reschedule(); - } - if (transaction.isUserEvent('select')) { - this.findOccurrencesTimer.reschedule(); - } - } - - private handleFindOccurrences() { - this.clearOccurrencesTimer.cancel(); - this.updateOccurrences().catch((error) => { - log.error('Unexpected error while updating occurrences', error); - this.clearOccurrences(); - }); - } - - private async updateOccurrences() { - await this.updateService.update(); - const result = await this.webSocketClient.send({ - resource: this.updateService.resourceName, - serviceType: 'occurrences', - expectedStateId: this.updateService.xtextStateId, - caretOffset: this.store.state.selection.main.head, - }); - const allChanges = this.updateService.computeChangesSinceLastUpdate(); - if (!allChanges.empty || isConflictResult(result, 'canceled')) { - // Stale occurrences result, the user already made some changes. - // We can safely ignore the occurrences and schedule a new find occurrences call. - this.clearOccurrences(); - this.findOccurrencesTimer.schedule(); - return; - } - const parsedOccurrencesResult = occurrencesResult.safeParse(result); - if (!parsedOccurrencesResult.success) { - log.error( - 'Unexpected occurences result', - result, - 'not an OccurrencesResult: ', - parsedOccurrencesResult.error, - ); - this.clearOccurrences(); - return; - } - const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; - if (stateId !== this.updateService.xtextStateId) { - log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId); - this.clearOccurrences(); - return; - } - const write = transformOccurrences(writeRegions); - const read = transformOccurrences(readRegions); - this.hasOccurrences = write.length > 0 || read.length > 0; - log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); - this.store.updateOccurrences(write, read); - } - - private clearOccurrences() { - if (this.hasOccurrences) { - this.store.updateOccurrences([], []); - this.hasOccurrences = false; - } - } -} diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts deleted file mode 100644 index e78944a9..00000000 --- a/language-web/src/main/js/xtext/UpdateService.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { - ChangeDesc, - ChangeSet, - ChangeSpec, - StateEffect, - Transaction, -} from '@codemirror/state'; -import { nanoid } from 'nanoid'; - -import type { EditorStore } from '../editor/EditorStore'; -import type { XtextWebSocketClient } from './XtextWebSocketClient'; -import { ConditionVariable } from '../utils/ConditionVariable'; -import { getLogger } from '../utils/logger'; -import { Timer } from '../utils/Timer'; -import { - ContentAssistEntry, - contentAssistResult, - documentStateResult, - formattingResult, - isConflictResult, -} from './xtextServiceResults'; - -const UPDATE_TIMEOUT_MS = 500; - -const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; - -const log = getLogger('xtext.UpdateService'); - -const setDirtyChanges = StateEffect.define(); - -export interface IAbortSignal { - aborted: boolean; -} - -export class UpdateService { - resourceName: string; - - xtextStateId: string | null = null; - - private readonly store: EditorStore; - - /** - * The changes being synchronized to the server if a full or delta text update is running, - * `null` otherwise. - */ - private pendingUpdate: ChangeSet | null = null; - - /** - * Local changes not yet sychronized to the server and not part of the running update, if any. - */ - private dirtyChanges: ChangeSet; - - private readonly webSocketClient: XtextWebSocketClient; - - private readonly updatedCondition = new ConditionVariable( - () => this.pendingUpdate === null && this.xtextStateId !== null, - WAIT_FOR_UPDATE_TIMEOUT_MS, - ); - - private readonly idleUpdateTimer = new Timer(() => { - this.handleIdleUpdate(); - }, UPDATE_TIMEOUT_MS); - - constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { - this.resourceName = `${nanoid(7)}.problem`; - this.store = store; - this.dirtyChanges = this.newEmptyChangeSet(); - this.webSocketClient = webSocketClient; - } - - onReconnect(): void { - this.xtextStateId = null; - this.updateFullText().catch((error) => { - log.error('Unexpected error during initial update', error); - }); - } - - onTransaction(transaction: Transaction): void { - const setDirtyChangesEffect = transaction.effects.find( - (effect) => effect.is(setDirtyChanges), - ) as StateEffect | undefined; - if (setDirtyChangesEffect) { - const { value } = setDirtyChangesEffect; - if (this.pendingUpdate !== null) { - this.pendingUpdate = ChangeSet.empty(value.length); - } - this.dirtyChanges = value; - return; - } - if (transaction.docChanged) { - this.dirtyChanges = this.dirtyChanges.compose(transaction.changes); - this.idleUpdateTimer.reschedule(); - } - } - - /** - * Computes the summary of any changes happened since the last complete update. - * - * The result reflects any changes that happened since the `xtextStateId` - * version was uploaded to the server. - * - * @return the summary of changes since the last update - */ - computeChangesSinceLastUpdate(): ChangeDesc { - return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc; - } - - private handleIdleUpdate() { - if (!this.webSocketClient.isOpen || this.dirtyChanges.empty) { - return; - } - if (this.pendingUpdate === null) { - this.update().catch((error) => { - log.error('Unexpected error during scheduled update', error); - }); - } - this.idleUpdateTimer.reschedule(); - } - - private newEmptyChangeSet() { - return ChangeSet.of([], this.store.state.doc.length); - } - - async updateFullText(): Promise { - await this.withUpdate(() => this.doUpdateFullText()); - } - - private async doUpdateFullText(): Promise<[string, void]> { - const result = await this.webSocketClient.send({ - resource: this.resourceName, - serviceType: 'update', - fullText: this.store.state.doc.sliceString(0), - }); - const { stateId } = documentStateResult.parse(result); - return [stateId, undefined]; - } - - /** - * Makes sure that the document state on the server reflects recent - * local changes. - * - * Performs either an update with delta text or a full text update if needed. - * If there are not local dirty changes, the promise resolves immediately. - * - * @return a promise resolving when the update is completed - */ - async update(): Promise { - await this.prepareForDeltaUpdate(); - const delta = this.computeDelta(); - if (delta === null) { - return; - } - log.trace('Editor delta', delta); - await this.withUpdate(async () => { - const result = await this.webSocketClient.send({ - resource: this.resourceName, - serviceType: 'update', - requiredStateId: this.xtextStateId, - ...delta, - }); - const parsedDocumentStateResult = documentStateResult.safeParse(result); - if (parsedDocumentStateResult.success) { - return [parsedDocumentStateResult.data.stateId, undefined]; - } - if (isConflictResult(result, 'invalidStateId')) { - return this.doFallbackToUpdateFullText(); - } - throw parsedDocumentStateResult.error; - }); - } - - private doFallbackToUpdateFullText() { - if (this.pendingUpdate === null) { - throw new Error('Only a pending update can be extended'); - } - log.warn('Delta update failed, performing full text update'); - this.xtextStateId = null; - this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges); - this.dirtyChanges = this.newEmptyChangeSet(); - return this.doUpdateFullText(); - } - - async fetchContentAssist( - params: Record, - signal: IAbortSignal, - ): Promise { - await this.prepareForDeltaUpdate(); - if (signal.aborted) { - return []; - } - const delta = this.computeDelta(); - if (delta !== null) { - log.trace('Editor delta', delta); - const entries = await this.withUpdate(async () => { - const result = await this.webSocketClient.send({ - ...params, - requiredStateId: this.xtextStateId, - ...delta, - }); - const parsedContentAssistResult = contentAssistResult.safeParse(result); - if (parsedContentAssistResult.success) { - const { stateId, entries: resultEntries } = parsedContentAssistResult.data; - return [stateId, resultEntries]; - } - if (isConflictResult(result, 'invalidStateId')) { - log.warn('Server state invalid during content assist'); - const [newStateId] = await this.doFallbackToUpdateFullText(); - // We must finish this state update transaction to prepare for any push events - // before querying for content assist, so we just return `null` and will query - // the content assist service later. - return [newStateId, null]; - } - throw parsedContentAssistResult.error; - }); - if (entries !== null) { - return entries; - } - if (signal.aborted) { - return []; - } - } - // Poscondition of `prepareForDeltaUpdate`: `xtextStateId !== null` - return this.doFetchContentAssist(params, this.xtextStateId as string); - } - - private async doFetchContentAssist(params: Record, expectedStateId: string) { - const result = await this.webSocketClient.send({ - ...params, - requiredStateId: expectedStateId, - }); - const { stateId, entries } = contentAssistResult.parse(result); - if (stateId !== expectedStateId) { - throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); - } - return entries; - } - - async formatText(): Promise { - await this.update(); - let { from, to } = this.store.state.selection.main; - if (to <= from) { - from = 0; - to = this.store.state.doc.length; - } - log.debug('Formatting from', from, 'to', to); - await this.withUpdate(async () => { - const result = await this.webSocketClient.send({ - resource: this.resourceName, - serviceType: 'format', - selectionStart: from, - selectionEnd: to, - }); - const { stateId, formattedText } = formattingResult.parse(result); - this.applyBeforeDirtyChanges({ - from, - to, - insert: formattedText, - }); - return [stateId, null]; - }); - } - - private computeDelta() { - if (this.dirtyChanges.empty) { - return null; - } - let minFromA = Number.MAX_SAFE_INTEGER; - let maxToA = 0; - let minFromB = Number.MAX_SAFE_INTEGER; - let maxToB = 0; - this.dirtyChanges.iterChangedRanges((fromA, toA, fromB, toB) => { - minFromA = Math.min(minFromA, fromA); - maxToA = Math.max(maxToA, toA); - minFromB = Math.min(minFromB, fromB); - maxToB = Math.max(maxToB, toB); - }); - return { - deltaOffset: minFromA, - deltaReplaceLength: maxToA - minFromA, - deltaText: this.store.state.doc.sliceString(minFromB, maxToB), - }; - } - - private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { - const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; - const revertChanges = pendingChanges.invert(this.store.state.doc); - const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); - const redoChanges = pendingChanges.map(applyBefore.desc); - const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); - this.store.dispatch({ - changes: changeSet, - effects: [ - setDirtyChanges.of(redoChanges), - ], - }); - } - - /** - * Executes an asynchronous callback that updates the state on the server. - * - * Ensures that updates happen sequentially and manages `pendingUpdate` - * and `dirtyChanges` to reflect changes being synchronized to the server - * and not yet synchronized to the server, respectively. - * - * Optionally, `callback` may return a second value that is retured by this function. - * - * Once the remote procedure call to update the server state finishes - * and returns the new `stateId`, `callback` must return _immediately_ - * to ensure that the local `stateId` is updated likewise to be able to handle - * push messages referring to the new `stateId` from the server. - * If additional work is needed to compute the second value in some cases, - * use `T | null` instead of `T` as a return type and signal the need for additional - * computations by returning `null`. Thus additional computations can be performed - * outside of the critical section. - * - * @param callback the asynchronous callback that updates the server state - * @return a promise resolving to the second value returned by `callback` - */ - private async withUpdate(callback: () => Promise<[string, T]>): Promise { - if (this.pendingUpdate !== null) { - throw new Error('Another update is pending, will not perform update'); - } - this.pendingUpdate = this.dirtyChanges; - this.dirtyChanges = this.newEmptyChangeSet(); - let newStateId: string | null = null; - try { - let result: T; - [newStateId, result] = await callback(); - this.xtextStateId = newStateId; - this.pendingUpdate = null; - this.updatedCondition.notifyAll(); - return result; - } catch (e) { - log.error('Error while update', e); - if (this.pendingUpdate === null) { - log.error('pendingUpdate was cleared during update'); - } else { - this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges); - } - this.pendingUpdate = null; - this.webSocketClient.forceReconnectOnError(); - this.updatedCondition.rejectAll(e); - throw e; - } - } - - /** - * Ensures that there is some state available on the server (`xtextStateId`) - * and that there is not pending update. - * - * After this function resolves, a delta text update is possible. - * - * @return a promise resolving when there is a valid state id but no pending update - */ - private async prepareForDeltaUpdate() { - // If no update is pending, but the full text hasn't been uploaded to the server yet, - // we must start a full text upload. - if (this.pendingUpdate === null && this.xtextStateId === null) { - await this.updateFullText(); - } - await this.updatedCondition.waitFor(); - } -} diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts deleted file mode 100644 index ff7d3700..00000000 --- a/language-web/src/main/js/xtext/ValidationService.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Diagnostic } from '@codemirror/lint'; - -import type { EditorStore } from '../editor/EditorStore'; -import type { UpdateService } from './UpdateService'; -import { validationResult } from './xtextServiceResults'; - -export class ValidationService { - private readonly store: EditorStore; - - private readonly updateService: UpdateService; - - constructor(store: EditorStore, updateService: UpdateService) { - this.store = store; - this.updateService = updateService; - } - - onPush(push: unknown): void { - const { issues } = validationResult.parse(push); - const allChanges = this.updateService.computeChangesSinceLastUpdate(); - const diagnostics: Diagnostic[] = []; - issues.forEach(({ - offset, - length, - severity, - description, - }) => { - if (severity === 'ignore') { - return; - } - diagnostics.push({ - from: allChanges.mapPos(offset), - to: allChanges.mapPos(offset + length), - severity, - message: description, - }); - }); - this.store.updateDiagnostics(diagnostics); - } -} diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts deleted file mode 100644 index 0898e725..00000000 --- a/language-web/src/main/js/xtext/XtextClient.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { - CompletionContext, - CompletionResult, -} from '@codemirror/autocomplete'; -import type { Transaction } from '@codemirror/state'; - -import type { EditorStore } from '../editor/EditorStore'; -import { ContentAssistService } from './ContentAssistService'; -import { HighlightingService } from './HighlightingService'; -import { OccurrencesService } from './OccurrencesService'; -import { UpdateService } from './UpdateService'; -import { getLogger } from '../utils/logger'; -import { ValidationService } from './ValidationService'; -import { XtextWebSocketClient } from './XtextWebSocketClient'; -import { XtextWebPushService } from './xtextMessages'; - -const log = getLogger('xtext.XtextClient'); - -export class XtextClient { - private readonly webSocketClient: XtextWebSocketClient; - - private readonly updateService: UpdateService; - - private readonly contentAssistService: ContentAssistService; - - private readonly highlightingService: HighlightingService; - - private readonly validationService: ValidationService; - - private readonly occurrencesService: OccurrencesService; - - constructor(store: EditorStore) { - this.webSocketClient = new XtextWebSocketClient( - () => this.updateService.onReconnect(), - (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), - ); - this.updateService = new UpdateService(store, this.webSocketClient); - this.contentAssistService = new ContentAssistService(this.updateService); - this.highlightingService = new HighlightingService(store, this.updateService); - this.validationService = new ValidationService(store, this.updateService); - this.occurrencesService = new OccurrencesService( - store, - this.webSocketClient, - this.updateService, - ); - } - - onTransaction(transaction: Transaction): void { - // `ContentAssistService.prototype.onTransaction` needs the dirty change desc - // _before_ the current edit, so we call it before `updateService`. - this.contentAssistService.onTransaction(transaction); - this.updateService.onTransaction(transaction); - this.occurrencesService.onTransaction(transaction); - } - - private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) { - const { resourceName, xtextStateId } = this.updateService; - if (resource !== resourceName) { - log.error('Unknown resource name: expected:', resourceName, 'got:', resource); - return; - } - if (stateId !== xtextStateId) { - log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', stateId); - // The current push message might be stale (referring to a previous state), - // so this is not neccessarily an error and there is no need to force-reconnect. - return; - } - switch (service) { - case 'highlight': - this.highlightingService.onPush(push); - return; - case 'validate': - this.validationService.onPush(push); - } - } - - contentAssist(context: CompletionContext): Promise { - return this.contentAssistService.contentAssist(context); - } - - formatText(): void { - this.updateService.formatText().catch((e) => { - log.error('Error while formatting text', e); - }); - } -} diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts deleted file mode 100644 index 2ce20a54..00000000 --- a/language-web/src/main/js/xtext/XtextWebSocketClient.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { nanoid } from 'nanoid'; - -import { getLogger } from '../utils/logger'; -import { PendingTask } from '../utils/PendingTask'; -import { Timer } from '../utils/Timer'; -import { - xtextWebErrorResponse, - XtextWebRequest, - xtextWebOkResponse, - xtextWebPushMessage, - XtextWebPushService, -} from './xtextMessages'; -import { pongResult } from './xtextServiceResults'; - -const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; - -const WEBSOCKET_CLOSE_OK = 1000; - -const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; - -const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; - -const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; - -const PING_TIMEOUT_MS = 10 * 1000; - -const REQUEST_TIMEOUT_MS = 1000; - -const log = getLogger('xtext.XtextWebSocketClient'); - -export type ReconnectHandler = () => void; - -export type PushHandler = ( - resourceId: string, - stateId: string, - service: XtextWebPushService, - data: unknown, -) => void; - -enum State { - Initial, - Opening, - TabVisible, - TabHiddenIdle, - TabHiddenWaiting, - Error, - TimedOut, -} - -export class XtextWebSocketClient { - private nextMessageId = 0; - - private connection!: WebSocket; - - private readonly pendingRequests = new Map>(); - - private readonly onReconnect: ReconnectHandler; - - private readonly onPush: PushHandler; - - private state = State.Initial; - - private reconnectTryCount = 0; - - private readonly idleTimer = new Timer(() => { - this.handleIdleTimeout(); - }, BACKGROUND_IDLE_TIMEOUT_MS); - - private readonly pingTimer = new Timer(() => { - this.sendPing(); - }, PING_TIMEOUT_MS); - - private readonly reconnectTimer = new Timer(() => { - this.handleReconnect(); - }); - - constructor(onReconnect: ReconnectHandler, onPush: PushHandler) { - this.onReconnect = onReconnect; - this.onPush = onPush; - document.addEventListener('visibilitychange', () => { - this.handleVisibilityChange(); - }); - this.reconnect(); - } - - private get isLogicallyClosed(): boolean { - return this.state === State.Error || this.state === State.TimedOut; - } - - get isOpen(): boolean { - return this.state === State.TabVisible - || this.state === State.TabHiddenIdle - || this.state === State.TabHiddenWaiting; - } - - private reconnect() { - if (this.isOpen || this.state === State.Opening) { - log.error('Trying to reconnect from', this.state); - return; - } - this.state = State.Opening; - const webSocketServer = window.origin.replace(/^http/, 'ws'); - const webSocketUrl = `${webSocketServer}/xtext-service`; - this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); - this.connection.addEventListener('open', () => { - if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { - log.error('Unknown subprotocol', this.connection.protocol, 'selected by server'); - this.forceReconnectOnError(); - } - if (document.visibilityState === 'hidden') { - this.handleTabHidden(); - } else { - this.handleTabVisibleConnected(); - } - log.info('Connected to websocket'); - this.nextMessageId = 0; - this.reconnectTryCount = 0; - this.pingTimer.schedule(); - this.onReconnect(); - }); - this.connection.addEventListener('error', (event) => { - log.error('Unexpected websocket error', event); - this.forceReconnectOnError(); - }); - this.connection.addEventListener('message', (event) => { - this.handleMessage(event.data); - }); - this.connection.addEventListener('close', (event) => { - if (this.isLogicallyClosed && event.code === WEBSOCKET_CLOSE_OK - && this.pendingRequests.size === 0) { - log.info('Websocket closed'); - return; - } - log.error('Websocket closed unexpectedly', event.code, event.reason); - this.forceReconnectOnError(); - }); - } - - private handleVisibilityChange() { - if (document.visibilityState === 'hidden') { - if (this.state === State.TabVisible) { - this.handleTabHidden(); - } - return; - } - this.idleTimer.cancel(); - if (this.state === State.TabHiddenIdle || this.state === State.TabHiddenWaiting) { - this.handleTabVisibleConnected(); - return; - } - if (this.state === State.TimedOut) { - this.reconnect(); - } - } - - private handleTabHidden() { - log.debug('Tab hidden while websocket is connected'); - this.state = State.TabHiddenIdle; - this.idleTimer.schedule(); - } - - private handleTabVisibleConnected() { - log.debug('Tab visible while websocket is connected'); - this.state = State.TabVisible; - } - - private handleIdleTimeout() { - log.trace('Waiting for pending tasks before disconnect'); - if (this.state === State.TabHiddenIdle) { - this.state = State.TabHiddenWaiting; - this.handleWaitingForDisconnect(); - } - } - - private handleWaitingForDisconnect() { - if (this.state !== State.TabHiddenWaiting) { - return; - } - const pending = this.pendingRequests.size; - if (pending === 0) { - log.info('Closing idle websocket'); - this.state = State.TimedOut; - this.closeConnection(1000, 'idle timeout'); - return; - } - log.info('Waiting for', pending, 'pending requests before closing websocket'); - } - - private sendPing() { - if (!this.isOpen) { - return; - } - const ping = nanoid(); - log.trace('Ping', ping); - this.send({ ping }).then((result) => { - const parsedPongResult = pongResult.safeParse(result); - if (parsedPongResult.success && parsedPongResult.data.pong === ping) { - log.trace('Pong', ping); - this.pingTimer.schedule(); - } else { - log.error('Invalid pong:', parsedPongResult, 'expected:', ping); - this.forceReconnectOnError(); - } - }).catch((error) => { - log.error('Error while waiting for ping', error); - this.forceReconnectOnError(); - }); - } - - send(request: unknown): Promise { - if (!this.isOpen) { - throw new Error('Not open'); - } - const messageId = this.nextMessageId.toString(16); - if (messageId in this.pendingRequests) { - log.error('Message id wraparound still pending', messageId); - this.rejectRequest(messageId, new Error('Message id wraparound')); - } - if (this.nextMessageId >= Number.MAX_SAFE_INTEGER) { - this.nextMessageId = 0; - } else { - this.nextMessageId += 1; - } - const message = JSON.stringify({ - id: messageId, - request, - } as XtextWebRequest); - log.trace('Sending message', message); - return new Promise((resolve, reject) => { - const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => { - this.removePendingRequest(messageId); - }); - this.pendingRequests.set(messageId, task); - this.connection.send(message); - }); - } - - private handleMessage(messageStr: unknown) { - if (typeof messageStr !== 'string') { - log.error('Unexpected binary message', messageStr); - this.forceReconnectOnError(); - return; - } - log.trace('Incoming websocket message', messageStr); - let message: unknown; - try { - message = JSON.parse(messageStr); - } catch (error) { - log.error('Json parse error', error); - this.forceReconnectOnError(); - return; - } - const okResponse = xtextWebOkResponse.safeParse(message); - if (okResponse.success) { - const { id, response } = okResponse.data; - this.resolveRequest(id, response); - return; - } - const errorResponse = xtextWebErrorResponse.safeParse(message); - if (errorResponse.success) { - const { id, error, message: errorMessage } = errorResponse.data; - this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); - if (error === 'server') { - log.error('Reconnecting due to server error: ', errorMessage); - this.forceReconnectOnError(); - } - return; - } - const pushMessage = xtextWebPushMessage.safeParse(message); - if (pushMessage.success) { - const { - resource, - stateId, - service, - push, - } = pushMessage.data; - this.onPush(resource, stateId, service, push); - } else { - log.error( - 'Unexpected websocket message:', - message, - 'not ok response because:', - okResponse.error, - 'not error response because:', - errorResponse.error, - 'not push message because:', - pushMessage.error, - ); - this.forceReconnectOnError(); - } - } - - private resolveRequest(messageId: string, value: unknown) { - const pendingRequest = this.pendingRequests.get(messageId); - if (pendingRequest) { - pendingRequest.resolve(value); - this.removePendingRequest(messageId); - return; - } - log.error('Trying to resolve unknown request', messageId, 'with', value); - } - - private rejectRequest(messageId: string, reason?: unknown) { - const pendingRequest = this.pendingRequests.get(messageId); - if (pendingRequest) { - pendingRequest.reject(reason); - this.removePendingRequest(messageId); - return; - } - log.error('Trying to reject unknown request', messageId, 'with', reason); - } - - private removePendingRequest(messageId: string) { - this.pendingRequests.delete(messageId); - this.handleWaitingForDisconnect(); - } - - forceReconnectOnError(): void { - if (this.isLogicallyClosed) { - return; - } - this.abortPendingRequests(); - this.closeConnection(1000, 'reconnecting due to error'); - log.error('Reconnecting after delay due to error'); - this.handleErrorState(); - } - - private abortPendingRequests() { - this.pendingRequests.forEach((request) => { - request.reject(new Error('Websocket disconnect')); - }); - this.pendingRequests.clear(); - } - - private closeConnection(code: number, reason: string) { - this.pingTimer.cancel(); - const { readyState } = this.connection; - if (readyState !== WebSocket.CLOSING && readyState !== WebSocket.CLOSED) { - this.connection.close(code, reason); - } - } - - private handleErrorState() { - this.state = State.Error; - this.reconnectTryCount += 1; - const delay = RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; - log.info('Reconnecting in', delay, 'ms'); - this.reconnectTimer.schedule(delay); - } - - private handleReconnect() { - if (this.state !== State.Error) { - log.error('Unexpected reconnect in', this.state); - return; - } - if (document.visibilityState === 'hidden') { - this.state = State.TimedOut; - } else { - this.reconnect(); - } - } -} diff --git a/language-web/src/main/js/xtext/xtextMessages.ts b/language-web/src/main/js/xtext/xtextMessages.ts deleted file mode 100644 index c4305fcf..00000000 --- a/language-web/src/main/js/xtext/xtextMessages.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from 'zod'; - -export const xtextWebRequest = z.object({ - id: z.string().nonempty(), - request: z.unknown(), -}); - -export type XtextWebRequest = z.infer; - -export const xtextWebOkResponse = z.object({ - id: z.string().nonempty(), - response: z.unknown(), -}); - -export type XtextWebOkResponse = z.infer; - -export const xtextWebErrorKind = z.enum(['request', 'server']); - -export type XtextWebErrorKind = z.infer; - -export const xtextWebErrorResponse = z.object({ - id: z.string().nonempty(), - error: xtextWebErrorKind, - message: z.string(), -}); - -export type XtextWebErrorResponse = z.infer; - -export const xtextWebPushService = z.enum(['highlight', 'validate']); - -export type XtextWebPushService = z.infer; - -export const xtextWebPushMessage = z.object({ - resource: z.string().nonempty(), - stateId: z.string().nonempty(), - service: xtextWebPushService, - push: z.unknown(), -}); - -export type XtextWebPushMessage = z.infer; diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts deleted file mode 100644 index f79b059c..00000000 --- a/language-web/src/main/js/xtext/xtextServiceResults.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { z } from 'zod'; - -export const pongResult = z.object({ - pong: z.string().nonempty(), -}); - -export type PongResult = z.infer; - -export const documentStateResult = z.object({ - stateId: z.string().nonempty(), -}); - -export type DocumentStateResult = z.infer; - -export const conflict = z.enum(['invalidStateId', 'canceled']); - -export type Conflict = z.infer; - -export const serviceConflictResult = z.object({ - conflict, -}); - -export type ServiceConflictResult = z.infer; - -export function isConflictResult(result: unknown, conflictType: Conflict): boolean { - const parsedConflictResult = serviceConflictResult.safeParse(result); - return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; -} - -export const severity = z.enum(['error', 'warning', 'info', 'ignore']); - -export type Severity = z.infer; - -export const issue = z.object({ - description: z.string().nonempty(), - severity, - line: z.number().int(), - column: z.number().int().nonnegative(), - offset: z.number().int().nonnegative(), - length: z.number().int().nonnegative(), -}); - -export type Issue = z.infer; - -export const validationResult = z.object({ - issues: issue.array(), -}); - -export type ValidationResult = z.infer; - -export const replaceRegion = z.object({ - offset: z.number().int().nonnegative(), - length: z.number().int().nonnegative(), - text: z.string(), -}); - -export type ReplaceRegion = z.infer; - -export const textRegion = z.object({ - offset: z.number().int().nonnegative(), - length: z.number().int().nonnegative(), -}); - -export type TextRegion = z.infer; - -export const contentAssistEntry = z.object({ - prefix: z.string(), - proposal: z.string().nonempty(), - label: z.string().optional(), - description: z.string().nonempty().optional(), - documentation: z.string().nonempty().optional(), - escapePosition: z.number().int().nonnegative().optional(), - textReplacements: replaceRegion.array(), - editPositions: textRegion.array(), - kind: z.string().nonempty(), -}); - -export type ContentAssistEntry = z.infer; - -export const contentAssistResult = documentStateResult.extend({ - entries: contentAssistEntry.array(), -}); - -export type ContentAssistResult = z.infer; - -export const highlightingRegion = z.object({ - offset: z.number().int().nonnegative(), - length: z.number().int().nonnegative(), - styleClasses: z.string().nonempty().array(), -}); - -export type HighlightingRegion = z.infer; - -export const highlightingResult = z.object({ - regions: highlightingRegion.array(), -}); - -export type HighlightingResult = z.infer; - -export const occurrencesResult = documentStateResult.extend({ - writeRegions: textRegion.array(), - readRegions: textRegion.array(), -}); - -export type OccurrencesResult = z.infer; - -export const formattingResult = documentStateResult.extend({ - formattedText: z.string(), - replaceRegion: textRegion, -}); - -export type FormattingResult = z.infer; -- cgit v1.2.3-70-g09d2