From b834db0fd424e7ab02fcd5e509d855f2d97863bd Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 2 Oct 2021 02:11:31 +0200 Subject: perf(web): split off CodeMirror chunks Also optimizes statis asset caching. --- language-web/src/main/css/index.scss | 29 ++++++++- .../solver/language/web/CacheControlFilter.java | 55 ++++++++++++++++ .../viatra/solver/language/web/ServerLauncher.java | 3 + language-web/src/main/js/App.tsx | 6 +- language-web/src/main/js/editor/Editor.tsx | 20 ------ language-web/src/main/js/editor/EditorArea.tsx | 42 ++++++++++++ language-web/src/main/js/editor/EditorStore.ts | 75 ++++++++++++++++++---- language-web/src/main/js/editor/editor.ts | 18 ++++++ language-web/src/main/js/theme/ThemeStore.ts | 4 +- 9 files changed, 213 insertions(+), 39 deletions(-) create mode 100644 language-web/src/main/java/org/eclipse/viatra/solver/language/web/CacheControlFilter.java delete mode 100644 language-web/src/main/js/editor/Editor.tsx create mode 100644 language-web/src/main/js/editor/EditorArea.tsx create mode 100644 language-web/src/main/js/editor/editor.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 index 9d6e0f6a..54f3a654 100644 --- a/language-web/src/main/css/index.scss +++ b/language-web/src/main/css/index.scss @@ -30,12 +30,27 @@ body { height: 100%; } -.CodeMirror, .CodeMirror-hints { +.problem-fallback-editor { + display: block; + height: 100%; + width: 100%; + resize: none; + border: none; + outline: none; + padding: 4px 4px 4px 16px; + white-space: pre; + overflow-wrap: normal; + overflow: auto; +} + +.CodeMirror, .CodeMirror-hints, .problem-fallback-editor { font-size: 16px; font-family: 'JetBrains MonoVariable', 'JetBrains Mono', monospace; font-feature-settings: 'liga', 'calt'; font-weight: 400; text-rendering: optimizeLegibility; + line-height: 1.35; + letter-spacing: 0; } @each $themeName, $theme in $themes { @@ -45,6 +60,16 @@ body { color: map.get($theme, 'foreground'); } + &.problem-fallback-editor { + background: map.get($theme, 'background'); + color: map.get($theme, 'foreground'); + caret-color: map.get($theme, 'cursor'); + + &::selection { + background: map.get($theme, 'selection'); + } + } + .CodeMirror-gutters { background: map.get($theme, 'background'); border: none; @@ -183,11 +208,13 @@ li.CodeMirror-hint-active { .xtext-marker_read { background: rgba(128, 203, 196, 0.2); + display: inline-block; } .xtext-marker_write { background: rgba(255, 229, 100, 0.2); + display: inline-block; } .problem-abstract { diff --git a/language-web/src/main/java/org/eclipse/viatra/solver/language/web/CacheControlFilter.java b/language-web/src/main/java/org/eclipse/viatra/solver/language/web/CacheControlFilter.java new file mode 100644 index 00000000..41b8e5bf --- /dev/null +++ b/language-web/src/main/java/org/eclipse/viatra/solver/language/web/CacheControlFilter.java @@ -0,0 +1,55 @@ +package org.eclipse.viatra.solver.language.web; + +import java.io.IOException; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CacheControlFilter implements Filter { + + private static final String CACHE_CONTROL_HEADER = "Cache-Control"; + + private static final String EXPIRES_HEADER = "Expires"; + + private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2)"); + + private static final long EXPIRY = 31536000; + + private static final String CACHE_CONTROL_CACHE_VALUE = "public, max-age: " + EXPIRY + ", 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 && response instanceof HttpServletResponse) { + var httpRequest = (HttpServletRequest) request; + var httpResponse = (HttpServletResponse) response; + if (CACHE_URI_PATTERN.matcher(httpRequest.getRequestURI()).matches()) { + httpResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_CACHE_VALUE); + httpResponse.setDateHeader(EXPIRES_HEADER, System.currentTimeMillis() + EXPIRY * 1000L); + } else { + httpResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_NO_CACHE_VALUE); + httpResponse.setDateHeader(EXPIRES_HEADER, 0); + } + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // Nothing to dispose. + } +} diff --git a/language-web/src/main/java/org/eclipse/viatra/solver/language/web/ServerLauncher.java b/language-web/src/main/java/org/eclipse/viatra/solver/language/web/ServerLauncher.java index d92c7735..a6d58f95 100644 --- a/language-web/src/main/java/org/eclipse/viatra/solver/language/web/ServerLauncher.java +++ b/language-web/src/main/java/org/eclipse/viatra/solver/language/web/ServerLauncher.java @@ -8,8 +8,10 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.util.EnumSet; import java.util.Set; +import javax.servlet.DispatcherType; import javax.servlet.SessionTrackingMode; import org.eclipse.jetty.server.Server; @@ -42,6 +44,7 @@ public class ServerLauncher { handler.setWelcomeFiles(new String[] { "index.html" }); addDefaultServlet(handler); } + handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); server.setHandler(handler); } diff --git a/language-web/src/main/js/App.tsx b/language-web/src/main/js/App.tsx index 17d4f339..5cd157fa 100644 --- a/language-web/src/main/js/App.tsx +++ b/language-web/src/main/js/App.tsx @@ -9,12 +9,12 @@ import MenuIcon from '@material-ui/icons/Menu'; import PlayArrowIcon from '@material-ui/icons/PlayArrow'; import { makeStyles } from './makeStyles'; -import { Editor } from './editor/Editor'; +import { EditorArea } from './editor/EditorArea'; import { EditorButtons } from './editor/EditorButtons'; const useStyles = makeStyles()((theme) => ({ container: { - maxHeight: '100vh', + height: '100vh', }, menuButton: { marginRight: theme.spacing(2), @@ -85,7 +85,7 @@ export const App = (): JSX.Element => { flexShrink={1} className={cx(classes.editorBox)} > - + ); diff --git a/language-web/src/main/js/editor/Editor.tsx b/language-web/src/main/js/editor/Editor.tsx deleted file mode 100644 index 9badb6a3..00000000 --- a/language-web/src/main/js/editor/Editor.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import React from 'react'; -import { Controlled as CodeMirror } from 'react-codemirror2'; - -import { useRootStore } from '../RootStore'; - -export const Editor = observer(() => { - const { editorStore } = useRootStore(); - - return ( - editorStore.editorDidMount(editor)} - editorWillUnmount={() => editorStore.editorWillUnmount()} - onBeforeChange={(_editor, _data, value) => editorStore.updateValue(value)} - onChange={() => editorStore.reportChanged()} - /> - ); -}); diff --git a/language-web/src/main/js/editor/EditorArea.tsx b/language-web/src/main/js/editor/EditorArea.tsx new file mode 100644 index 00000000..f07a0ad8 --- /dev/null +++ b/language-web/src/main/js/editor/EditorArea.tsx @@ -0,0 +1,42 @@ +import { observer } from 'mobx-react-lite'; +import React, { useRef } from 'react'; + +import { useRootStore } from '../RootStore'; + +export const EditorArea = observer(() => { + const { editorStore } = useRootStore(); + const { CodeMirror } = editorStore.chunk || {}; + const fallbackTextarea = useRef(null); + + if (!CodeMirror) { + return ( + + ); + } + + const textarea = fallbackTextarea.current; + if (textarea) { + editorStore.setInitialSelection( + textarea.selectionStart, + textarea.selectionEnd, + document.activeElement === textarea, + ); + } + + return ( + editorStore.editorDidMount(editor)} + editorWillUnmount={() => editorStore.editorWillUnmount()} + onBeforeChange={(_editor, _data, value) => editorStore.updateValue(value)} + onChange={() => editorStore.reportChanged()} + /> + ); +}); diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index 5da45ac1..1ac2e79f 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts @@ -1,19 +1,14 @@ -import { Editor, EditorConfiguration } from 'codemirror'; -import 'codemirror/addon/selection/active-line'; +import type { Editor, EditorConfiguration } from 'codemirror'; import { createAtom, makeAutoObservable, observable, + runInAction, } from 'mobx'; -import 'mode-problem'; -import { - IXtextOptions, - IXtextServices, - createServices, - removeServices, -} from 'xtext/xtext-codemirror'; +import type { IXtextOptions, IXtextServices } from 'xtext/xtext-codemirror'; -import { ThemeStore } from '../theme/ThemeStore'; +import type { IEditorChunk } from './editor'; +import type { ThemeStore } from '../theme/ThemeStore'; const xtextLang = 'problem'; @@ -33,6 +28,8 @@ export class EditorStore { atom; + chunk?: IEditorChunk; + editor?: Editor; xtextServices?: IXtextServices; @@ -41,15 +38,56 @@ export class EditorStore { showLineNumbers = false; + initialSelection!: { start: number, end: number, focused: boolean }; + constructor(themeStore: ThemeStore) { this.themeStore = themeStore; this.atom = createAtom('EditorStore'); + this.resetInitialSelection(); makeAutoObservable(this, { themeStore: false, atom: false, + chunk: observable.ref, editor: observable.ref, xtextServices: observable.ref, + initialSelection: false, }); + import('./editor').then(({ editorChunk }) => { + runInAction(() => { + this.chunk = editorChunk; + }); + }).catch((error) => { + console.warn('Error while loading editor', error); + }); + } + + setInitialSelection(start: number, end: number, focused: boolean): void { + this.initialSelection = { start, end, focused }; + this.applyInitialSelectionToEditor(); + } + + private resetInitialSelection(): void { + this.initialSelection = { + start: 0, + end: 0, + focused: false, + }; + } + + private applyInitialSelectionToEditor(): void { + if (this.editor) { + const { start, end, focused } = this.initialSelection; + const doc = this.editor.getDoc(); + const startPos = doc.posFromIndex(start); + const endPos = doc.posFromIndex(end); + doc.setSelection(startPos, endPos, { + scroll: true, + }); + if (focused) { + this.editor.focus(); + } + this.resetInitialSelection(); + } } /** @@ -61,16 +99,23 @@ export class EditorStore { * @param newEditor The new CodeMirror instance */ editorDidMount(newEditor: Editor): void { + if (!this.chunk) { + throw new Error('Editor not loaded yet'); + } if (this.editor) { throw new Error('CoreMirror editor mounted before unmounting'); } this.editor = newEditor; - this.xtextServices = createServices(newEditor, xtextOptions); + this.xtextServices = this.chunk.createServices(newEditor, xtextOptions); + this.applyInitialSelectionToEditor(); } editorWillUnmount(): void { + if (!this.chunk) { + throw new Error('Editor not loaded yet'); + } if (this.editor) { - removeServices(this.editor); + this.chunk.removeServices(this.editor); } delete this.editor; delete this.xtextServices; @@ -93,10 +138,14 @@ export class EditorStore { this.atom.reportObserved(); } + get codeMirrorTheme(): string { + return `problem-${this.themeStore.className}`; + } + get codeMirrorOptions(): EditorConfiguration { return { ...codeMirrorGlobalOptions, - theme: this.themeStore.codeMirrorTheme, + theme: this.codeMirrorTheme, lineNumbers: this.showLineNumbers, }; } diff --git a/language-web/src/main/js/editor/editor.ts b/language-web/src/main/js/editor/editor.ts new file mode 100644 index 00000000..fbf8796b --- /dev/null +++ b/language-web/src/main/js/editor/editor.ts @@ -0,0 +1,18 @@ +import 'codemirror/addon/selection/active-line'; +import 'mode-problem'; +import { Controlled } from 'react-codemirror2'; +import { createServices, removeServices } from 'xtext/xtext-codemirror'; + +export interface IEditorChunk { + CodeMirror: typeof Controlled; + + createServices: typeof createServices; + + removeServices: typeof removeServices; +} + +export const editorChunk: IEditorChunk = { + CodeMirror: Controlled, + createServices, + removeServices, +}; diff --git a/language-web/src/main/js/theme/ThemeStore.ts b/language-web/src/main/js/theme/ThemeStore.ts index 0e4aeb23..2644a96a 100644 --- a/language-web/src/main/js/theme/ThemeStore.ts +++ b/language-web/src/main/js/theme/ThemeStore.ts @@ -51,7 +51,7 @@ export class ThemeStore { return responsiveFontSizes(materialUiTheme); } - get codeMirrorTheme(): string { - return `problem-${this.currentThemeData.className}`; + get className(): string { + return this.currentThemeData.className; } } -- cgit v1.2.3-70-g09d2