diff --git a/WebSocketClient/Dockerfile b/WebSocketClient/Dockerfile index e729244..b9e6f01 100644 --- a/WebSocketClient/Dockerfile +++ b/WebSocketClient/Dockerfile @@ -9,7 +9,9 @@ COPY . . RUN npm run build # Install Nginx -RUN apt-get update && apt-get install -y nginx +RUN apt-get update +RUN apt-get install -y nginx +RUN apt-get clean # Copy your static files into the Nginx document root directory RUN cp -r dist/* /var/www/html/ diff --git a/WebSocketClient/dark_mode_dev_env.sh b/WebSocketClient/dark_mode_dev_env.sh new file mode 100755 index 0000000..7d49484 --- /dev/null +++ b/WebSocketClient/dark_mode_dev_env.sh @@ -0,0 +1,6 @@ +cp -r ./dist/assets/dark_modern* ./node_modules/.vite/deps/dark_modern.json +cp -r ./dist/assets/dark_plus* ./node_modules/.vite/deps/dark_plus.json +cp -r ./dist/assets/dark_vs* ./node_modules/.vite/deps/dark_vs.json +cp -r ./dist/assets/light_modern* ./node_modules/.vite/deps/light_modern.json +cp -r ./dist/assets/light_plus* ./node_modules/.vite/deps/light_plus.json +cp -r ./dist/assets/light_vs* ./node_modules/.vite/deps/light_vs.json \ No newline at end of file diff --git a/WebSocketClient/index.html b/WebSocketClient/index.html index 12bd2fe..cb22d23 100644 --- a/WebSocketClient/index.html +++ b/WebSocketClient/index.html @@ -1,32 +1,382 @@ - - - - - UVL Playground - - -
-

UVL Playground

-

-
-
-
-
+ + + + + + + + + UVL Playground + + +
+

UVL Playground

+

+
+
?
+
?
+
+ +
+ +
+

Get the UVLS VSCode Extension

+

You are trying to access the Configuration Page for Feature Models. + As by now, the Web-Playground does not support this, but it is available in the VSCode Extension! + Here is a preview: +

+ Preview +

Get the extension here +

+
+
+
+
+
+
+
+
+ +
+
+
+
- - - + + + + + diff --git a/WebSocketClient/package-lock.json b/WebSocketClient/package-lock.json index 41ef745..42def1f 100644 --- a/WebSocketClient/package-lock.json +++ b/WebSocketClient/package-lock.json @@ -19,6 +19,7 @@ "@codingame/monaco-vscode-theme-service-override": "~1.83.2", "@viz-js/viz": "^3.2.3", "dotenv": "^16.3.1", + "intro.js": "^7.2.0", "lodash": "^4.17.21", "monaco-editor": "^0.44.0", "monaco-editor-workers": "~0.44.0", @@ -594,6 +595,11 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/intro.js": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz", + "integrity": "sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==" + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", diff --git a/WebSocketClient/package.json b/WebSocketClient/package.json index 9115e1c..de17250 100644 --- a/WebSocketClient/package.json +++ b/WebSocketClient/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "./dark_mode_dev_env.sh && vite dev", "build": "tsc && vite build", "preview": "vite preview" }, @@ -28,6 +28,7 @@ "@codingame/monaco-vscode-theme-service-override": "~1.83.2", "@viz-js/viz": "^3.2.3", "dotenv": "^16.3.1", + "intro.js": "^7.2.0", "lodash": "^4.17.21", "monaco-editor": "^0.44.0", "monaco-editor-workers": "~0.44.0", diff --git a/WebSocketClient/split.css b/WebSocketClient/split.css new file mode 100644 index 0000000..1bf50c4 --- /dev/null +++ b/WebSocketClient/split.css @@ -0,0 +1,35 @@ +.splitter { + width: 100%; + height: 80vh; + display: flex; + +} + +#separator { + cursor: col-resize; + background-color: #aaa; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: center; + width: 10px; + height: 100%; + + /* Prevent the browser's built-in drag from interfering */ + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +#first { + background-color: rgb(255, 255, 255); + width: 100%; + height: 100%; + min-width: 10px; +} + +#second { + background-color: rgb(255, 255, 255); + width: 0%; + height: 100%; + min-width: 10px; +} \ No newline at end of file diff --git a/WebSocketClient/src/config.ts b/WebSocketClient/src/config.ts index 95bab79..445cd1a 100644 --- a/WebSocketClient/src/config.ts +++ b/WebSocketClient/src/config.ts @@ -2,4 +2,6 @@ export default { port: 30000, languageServerHostName: "localhost", debug: true, + MAX_NUMBER_LINES: 100, + MAX_NUMBER_CHARACTERS: 10000, }; \ No newline at end of file diff --git a/WebSocketClient/src/intro.ts b/WebSocketClient/src/intro.ts new file mode 100644 index 0000000..e8f348c --- /dev/null +++ b/WebSocketClient/src/intro.ts @@ -0,0 +1,49 @@ +import introJs from "intro.js"; +import { sendGenerateGraphCommand } from "./main"; + + +export const initIntroJS = () => { + + + var intro = introJs(); + + intro.setOptions({ + steps: [{ + element: '#container', intro: 'This is the texteditor where you write and edit your uvl feature model.', + }, { + element: '.codelens-decoration', intro: 'You can use the buttons to activate and deactivate functionality.' + }, { + element: "[id='1']", + intro: 'Click here to visualize your feature model und click on it again to hide it again.' + }, { + element: "#separator", intro: 'You can change the size of the editor and the visualization with your mouse.' + }], + }); + + const button = document.getElementById("tutorialButton"); + + intro.onchange(function(targetElement) { + if(targetElement.id === "1"){ + sendGenerateGraphCommand(); + } + + }); + + button?.addEventListener("click", () => { + intro.setOption("exitOnOverlayClick", false); + intro.setOption("overlayOpacity", 0); + intro.setOption("disableInteraction", true); + intro.setOption("highlightClass", "tmpClass"); + intro.start(); + + var helperLayer = document.getElementsByClassName("tmpClass"); + setTimeout(() => { + if(helperLayer[0] instanceof HTMLElement){ + helperLayer[0].style["box-shadow"] = "rgb(255, 255, 255) 0px 0px 1px 2px, rgba(230, 230, 230, 0.44) 0px 0px 0px 5000px"; + } + }, 100); + + + }); + +} \ No newline at end of file diff --git a/WebSocketClient/src/main.ts b/WebSocketClient/src/main.ts index 53f9457..1d81a01 100644 --- a/WebSocketClient/src/main.ts +++ b/WebSocketClient/src/main.ts @@ -1,31 +1,38 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) 2018-2022 TypeFox GmbH (http://www.typefox.io). All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - import * as monaco from 'monaco-editor'; +import {editor} from 'monaco-editor'; import * as vscode from 'vscode'; -import { whenReady } from '@codingame/monaco-vscode-theme-defaults-default-extension'; +import {LogLevel, Uri} from 'vscode'; +import {whenReady} from '@codingame/monaco-vscode-theme-defaults-default-extension'; import '@codingame/monaco-vscode-python-default-extension'; -import { createConfiguredEditor, createModelReference } from 'vscode/monaco'; -import { ExtensionHostKind, registerExtension } from 'vscode/extensions'; -import getConfigurationServiceOverride, { updateUserConfiguration } from '@codingame/monaco-vscode-configuration-service-override'; +import {createConfiguredEditor, createModelReference} from 'vscode/monaco'; +import {ExtensionHostKind, registerExtension} from 'vscode/extensions'; +import getConfigurationServiceOverride, { + updateUserConfiguration +} from '@codingame/monaco-vscode-configuration-service-override'; import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override'; import getThemeServiceOverride from '@codingame/monaco-vscode-theme-service-override'; import getTextmateServiceOverride from '@codingame/monaco-vscode-textmate-service-override'; -import { initServices, MonacoLanguageClient } from 'monaco-languageclient'; -import { CloseAction, ErrorAction, MessageTransports } from 'vscode-languageclient'; -import { WebSocketMessageReader, WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc'; -import { RegisteredFileSystemProvider, registerFileSystemOverlay, RegisteredMemoryFile } from 'vscode/service-override/files'; -import {LogLevel, Uri} from 'vscode'; +import {initServices, MonacoLanguageClient} from 'monaco-languageclient'; +import {CloseAction, ErrorAction, MessageTransports} from 'vscode-languageclient'; +import {toSocket, WebSocketMessageReader, WebSocketMessageWriter} from 'vscode-ws-jsonrpc'; +import { + RegisteredFileSystemProvider, + RegisteredMemoryFile, + registerFileSystemOverlay +} from 'vscode/service-override/files'; import config from './config.js'; -import { instance } from "@viz-js/viz"; -import { Message } from 'vscode-jsonrpc'; -import { v4 as uuidv4 } from 'uuid'; +import {instance} from "@viz-js/viz"; +import {Message} from 'vscode-jsonrpc'; +import {v4 as uuidv4} from 'uuid'; import lodash from 'lodash'; -import { ExecuteCommandRequest } from 'vscode-languageserver-protocol' +import {ExecuteCommandRequest} from 'vscode-languageserver-protocol' + +import {buildWorkerDefinition} from 'monaco-editor-workers'; +import {initIntroJS} from "./intro.ts"; +import IOverlayWidget = editor.IOverlayWidget; +import IContentWidget = editor.IContentWidget; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; -import { buildWorkerDefinition } from 'monaco-editor-workers'; buildWorkerDefinition('./node_modules/monaco-editor-workers/dist/workers', new URL('', window.location.href).href, false); const languageId = 'uvls'; @@ -33,138 +40,158 @@ let languageClient: MonacoLanguageClient; let fileID; let model; const connectionText = document.getElementById("connection"); +let debounceGenGraph; +let updateGraph = false; -const createUrl = (hostname: string, port: number, path: string, searchParams: Record = {}, secure: boolean): string => { +const createUrl = (hostname: string, port: number, path: string, secure: boolean): string => { const protocol = secure ? 'wss' : 'ws'; const url = new URL(`${protocol}://${hostname}:${port}${path}`); - for (let [key, value] of Object.entries(searchParams)) { - if (value instanceof Array) { - value = value.join(','); - } - if (value) { - url.searchParams.set(key, value); - } - } return url.toString(); }; const createWebSocket = (url: string): WebSocket => { const webSocket = new WebSocket(url); + webSocket.onerror = () => { - if(connectionText){ - connectionText.textContent = "Could not connect to language server. Reconnecting ..."; + if (connectionText) { + displayEditorError("Could not connect to language server. Reconnecting ..."); } setTimeout(() => { createWebSocket(url); - }, 1000); + }, 500); }; - webSocket.onopen = async () => { - if(connectionText){ - connectionText.textContent = ""; + webSocket.onopen = () => { + if (connectionText) { + displayEditorError(""); } const socket = toSocket(webSocket); const reader = new WebSocketMessageReader(socket); const writer = new WebSocketMessageWriter(socket); languageClient = createLanguageClient({ - reader, - writer + reader, writer }); - await languageClient.start(); + + languageClient.start(); reader.onClose(() => { - languageClient.stop(); - createWebSocket(url); + setTimeout(() => { + createWebSocket(url); + }, 500); }); }; + return webSocket; }; const createLanguageClient = (transports: MessageTransports): MonacoLanguageClient => { const client = new MonacoLanguageClient({ - name: 'UVL Language Client', - clientOptions: { + name: 'UVL Language Client', clientOptions: { // use a language id as a document selector - documentSelector: [languageId], - // disable the default error handler + documentSelector: [languageId], // disable the default error handler errorHandler: { - error: () => ({ action: ErrorAction.Continue }), - closed: () => ({ action: CloseAction.DoNotRestart }) - }, - // pyright requires a workspace folder to be present, otherwise it will not work + error: () => ({action: ErrorAction.Continue}), closed: () => ({action: CloseAction.DoNotRestart}) + }, // pyright requires a workspace folder to be present, otherwise it will not work workspaceFolder: { - index: 0, - name: 'workspace', - uri: monaco.Uri.parse('/workspace') - }, - synchronize: { + index: 0, name: 'workspace', uri: monaco.Uri.parse('/workspace') + }, synchronize: { fileEvents: [vscode.workspace.createFileSystemWatcher('**')] - }, - connectionOptions: { + }, connectionOptions: { // This construct can be used to filter the messages we are receiving from the language server messageStrategy: { handleMessage(message: Message, next: (message: Message) => void) { - if(Message.isRequest(message)){ + if (Message.isRequest(message)) { // Filters requests send by uvls -> Anti-Pattern in our opinion - } - else if(Message.isResponse(message)){ + } else if (Message.isResponse(message)) { // Filters responses send by uvls - } - else if(Message.isNotification(message)){ + } else if (Message.isNotification(message)) { // Filters Notification messages following json-rpc spec } // "next" is the default behaviour next(message); } } - }, - // The Middleware allows us to intercept all messages that would be sent to the language server + }, // The Middleware allows us to intercept all messages that would be sent to the language server middleware: { executeCommand(command, args, next) { const information = {command: command, arguments: args}; - if(command === "uvls/open_config") { + debounceGenGraph = lodash.debounce(() => { + client?.sendRequest(ExecuteCommandRequest.type, information).then((res) => { + createDiagramFromDot(res as string); + }); + }, 500); + console.log("command: " + command); + console.log("args: " + args); + + if (command === "uvls/open_config") { + const dialog: HTMLDialogElement | null = document.querySelector("#dialog") + const modalClose: HTMLButtonElement | null = document.querySelector('#modalClose'); + if (modalClose) { + modalClose.onclick = () => dialog?.close(); + } + dialog?.showModal(); //we do not support config view return; - } - else if(command === "uvls/generate_diagram") { + } else if (command === "uvls/generate_diagram") { client?.sendRequest(ExecuteCommandRequest.type, information).then((res) => { createDiagramFromDot(res as string); }); - } - else { + + if (!updateGraph) { + updateGraph = true; + + const firstPane = document.getElementById("first"); + const secondPane = document.getElementById("second"); + if (firstPane && secondPane) { + firstPane.style.width = "50%"; + secondPane.style.width = "50%"; + } + + } else { + updateGraph = false; + const div = document.getElementsByClassName("graph"); + const firstPane = document.getElementById("first"); + while (div[0].firstChild) { + div[0].removeChild(div[0].firstChild); + } + const secondPane = document.getElementById("second"); + if (firstPane && secondPane) { + firstPane.style.width = "100%"; + secondPane.style.width = "0%"; + } + } + + } else { next(command, args); } }, } - }, - // create a language client connection from the JSON RPC connection on demand + }, // create a language client connection from the JSON RPC connection on demand connectionProvider: { get: () => { return Promise.resolve(transports); } } }); + return client; }; function createDiagramFromDot(res: string): void { instance().then(viz => { const div = document.getElementsByClassName("graph"); - div[0].replaceChildren(viz.renderSVGElement(res!)); + let svg = viz.renderSVGElement(res!); + svg.id = "SVGGraph"; + div[0].replaceChildren(svg); }); } -export const startPythonClient = async () => { +export const startUvlClient = async () => { // init vscode-api const useDebugLogging = config.debug ? LogLevel.Debug : LogLevel.Off; await initServices({ userServices: { - ...getThemeServiceOverride(), - ...getTextmateServiceOverride(), - ...getConfigurationServiceOverride(Uri.file('/workspace')), - ...getKeybindingsServiceOverride(), - }, - debugLogging: config.debug, - logLevel: useDebugLogging, + ...getThemeServiceOverride(), ...getTextmateServiceOverride(), ...getConfigurationServiceOverride(Uri.file('/workspace')), ...getKeybindingsServiceOverride(), + }, debugLogging: config.debug, logLevel: useDebugLogging, }); await whenReady(); @@ -172,21 +199,11 @@ export const startPythonClient = async () => { // https://github.com/microsoft/pyright/blob/main/packages/vscode-pyright/package.json // only a minimum is required to get pyright working const extension = { - name: 'uvl-client', - publisher: 'monaco-languageclient-project', - version: '1.0.0', - engines: { + name: 'uvl-client', publisher: 'monaco-languageclient-project', version: '1.0.0', engines: { vscode: '^1.78.0' - }, - contributes: { + }, contributes: { languages: [{ - id: languageId, - aliases: [ - 'UVL' - ], - extensions: [ - '.uvl', - ] + id: languageId, aliases: ['UVL'], extensions: ['.uvl',] }] } }; @@ -204,46 +221,165 @@ export const startPythonClient = async () => { registerFileSystemOverlay(1, fileSystemProvider); // create the web socket and configure to start the language client on open, can add extra parameters to the url if needed. - createWebSocket(createUrl(config.languageServerHostName,config.port,'/', { - // Used to parse an auth token or additional parameters such as import IDs to the language server - authorization: 'UserAuth' - // By commenting above line out and commenting below line in, connection to language server will be denied. - // authorization: 'FailedUserAuth' - }, location.protocol === 'https:')); - + createWebSocket(createUrl(config.languageServerHostName, config.port, '/', location.protocol === 'https:')); // use the file create before const modelRef = await createModelReference(monaco.Uri.file(`/workspace/${fileID}.uvl`)); model = modelRef.object; + modelRef.object.setLanguageId(languageId); - const debouncedSave = lodash.debounce(saveFm, 1000); - modelRef.object.onDidChangeContent(() => { - debouncedSave(); + // create monaco editor + const editor = createConfiguredEditor(document.getElementById('container')!, { + model: modelRef.object.textEditorModel, automaticLayout: true }); + window["editor"] = editor; + editor.onDidChangeModelContent(() => { + const model = editor.getModel(); + if (!model) { + return; + } + const lineCount = model?.getLineCount(); + let numberCharacters = aggregateCharacters(model); - modelRef.object.setLanguageId(languageId); + if (lineCount && lineCount > config.MAX_NUMBER_LINES) { + if (lineCount > config.MAX_NUMBER_LINES + 1) { + vscode.commands.executeCommand("undo"); + } else { + vscode.commands.executeCommand("deleteLeft"); + } + if (connectionText) { + displayEditorErrorAtContent(editor, `The Editor only allows content up to ${config.MAX_NUMBER_LINES} Lines!`); + } + } else if (numberCharacters > config.MAX_NUMBER_CHARACTERS) { + if (numberCharacters > config.MAX_NUMBER_CHARACTERS + 1) { + vscode.commands.executeCommand("undo"); + } + else { + vscode.commands.executeCommand("deleteLeft"); + } + if (connectionText) { + displayEditorErrorAtContent(editor, `The Editor only allows content up to ${config.MAX_NUMBER_CHARACTERS} Characters!`); + } + } + debouncedSave(); + if (updateGraph && debounceGenGraph !== undefined) { + debounceGenGraph(); + } + }) - // create monaco editor - createConfiguredEditor(document.getElementById('container')!, { - model: modelRef.object.textEditorModel, - automaticLayout: true - }); + initIntroJS(); + const debouncedSave = lodash.debounce(saveFm, 1000); + + globalEditor = editor; }; -function getInitialFm(){ +let globalEditor: IStandaloneCodeEditor | null; +let currentWidget: IOverlayWidget | null; + + +function displayEditorError(msg: string) { + if (!globalEditor) { + return; + } + const overlayWidget: IOverlayWidget = { + getId(): string { + return 'myCustomWidget'; + }, getPosition(): editor.IOverlayWidgetPosition | null { + return { + preference: monaco.editor.OverlayWidgetPositionPreference.TOP_CENTER + } + }, getDomNode(): HTMLElement { + const node = document.createElement('div'); + const span = document.createElement('span'); + span.textContent = msg; + span.className = "top-error"; + node.replaceChildren(span); + return node; + } + } + if (currentWidget) { + globalEditor.removeOverlayWidget(currentWidget); + } + currentWidget = overlayWidget; + globalEditor.addOverlayWidget(overlayWidget); + // setTimeout(() => { + // if(!globalEditor) return; + // globalEditor.removeOverlayWidget(overlayWidget); + // }, 2000); +} + +let currentContentWidget: IContentWidget | null; + +function displayEditorErrorAtContent(editor: editor.IStandaloneCodeEditor, msg: string) { + + const selection = editor.getSelection(); + const contentWidget: IContentWidget = { + getId(): string { + return 'myCustomWidget'; + }, getPosition(): editor.IContentWidgetPosition | null { + if (selection) { + return { + position: selection.getStartPosition(), + preference: [monaco.editor.ContentWidgetPositionPreference.BELOW] + } + } + return { + position: {lineNumber: 1, column: 1}, preference: [monaco.editor.ContentWidgetPositionPreference.BELOW] + } + }, getDomNode(): HTMLElement { + const node = document.createElement('div'); + const span = document.createElement('span'); + node.className = "uvl-tooltip"; + span.className = "tooltip-text"; + span.textContent = msg; + node.replaceChildren(span); + return node; + } + } + if (currentContentWidget) { + editor.removeContentWidget(currentContentWidget); + } + currentContentWidget = contentWidget; + editor.addContentWidget(contentWidget); + + debouceRemoveWidget(editor); +} + +const debouceRemoveWidget = lodash.debounce(removeWidget, 2000); + +function removeWidget(editor: IStandaloneCodeEditor) { + console.log("Editor: ", editor); + if (currentContentWidget) { + editor.removeContentWidget(currentContentWidget); + } + currentContentWidget = null; +} + +function aggregateCharacters(model: editor.ITextModel): number { + let addReducer = (previousValue: number, currentValue: string) => { + return previousValue + currentValue.length + }; + return model?.getLinesContent().reduce(addReducer, 0); +} + +function getInitialFm() { let initialFm = "features\n\tfeature1\n\t\tor\n\t\t\tfeature2\n\t\t\tfeature3\n\nconstraints\n\tfeature1"; const storedFm = window.localStorage.getItem("fm"); - if(storedFm !== null){ + if (storedFm !== null) { initialFm = storedFm; } return initialFm; } -function saveFm(){ - if(model !== undefined){ +function saveFm() { + if (model !== undefined) { const content = model.textEditorModel?.getValue(); window.localStorage.setItem("fm", content); } +} + +export function sendGenerateGraphCommand() { + vscode.commands.executeCommand("uvls/generate_diagram", `file:///workspace/${fileID}.uvl`); } \ No newline at end of file diff --git a/WebSocketClient/style.css b/WebSocketClient/style.css index 768b2cf..aee7f15 100644 --- a/WebSocketClient/style.css +++ b/WebSocketClient/style.css @@ -1,81 +1,203 @@ /* Reset default browser styles */ body, h1, p { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } body { - font-family: Arial, sans-serif; - background-color: #1e1e1e; /* Dark background color */ - color: #ffffff; /* Light text color */ + font-family: Arial, sans-serif; + background-color: #1e1e1e; /* Dark background color */ } .container { - display: flex; - width: 50%; - height: 100% + display: flex; + width: 50%; + height: 100% } .header { - text-align: center; + text-align: center; + color: #ffffff; /* Light text color */ } .footer { - text-align: center; + text-align: center; + color: #ffffff; /* Light text color */ } -iframe { - border: none; +.graph { + overflow: scroll; + background: #1e1e1e; + height: 100%; } -.graph { - padding: 10px; - overflow: scroll; - text-align: center; +#SVGGraph { + margin: 30vh auto; + display: block; +} + +.tooltip * { + background-color: #1e1e1e; + color: white; + + & button { + border: 2px solid red; + } } -.flex-container { - display: flex; - height: 75%; - justify-content: space-around; +.tooltip { + & button { + border: 2px solid red; + } } -iframe { - flex-grow: 1; - max-width: 50%; +::backdrop { + background-image: linear-gradient( + 45deg, + darkgray, + black + ); + opacity: 0.75; } +#dialog { + background: #1e1e1e; + color: white; + width: 50vw; + height: 70vh; + + #gif_in_dialog { + display: block; + margin: 10px auto; + max-width: 40vw; + min-width: 20vw; + } + + & div { + text-align: right; + + & button { + color: white; + background: #1e1e1e; + border: none; + font-size: 20pt; + } + } +} + + h1 { - font-size: 30px; - margin-bottom: 20px; - text-align: center; + font-size: 30px; + margin: 20px; + text-align: center; } .editor { background-color: #2d2d2d; - padding: 20px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); min-width: 50%; max-width: 100%; - height:80ch; + height:80vh; border:1px solid grey; - margin-left: 10px; - margin-right: 10px; + + & .uvl-tooltip { + width: max-content; + background-color: #2e2e2e; + border: 1px solid black; + + & .tooltip-text { + color: #bb0000; + margin: 10px; + } + } } p { - font-size: 20px; - line-height: 1.5; - margin-top: 20px; - width: 80%; - hyphens: auto; - text-align: justify; - margin-left: auto; - margin-right: auto; -} - -a:link { color: lightskyblue; } -a:visited { color: rgb(73, 185, 255); } -a:hover { color: #ffffff; } -a:active { color: #ff4040; text-decoration:none; font-weight:normal; } \ No newline at end of file + font-size: 20px; + line-height: 1.5; + margin-top: 20px; + width: 80%; + hyphens: auto; + text-align: justify; + margin-left: auto; + margin-right: auto; +} + +a:link { + color: lightskyblue; +} + +a:visited { + color: rgb(73, 185, 255); +} + +a:hover { + color: #ffffff; +} + +a:active { + color: #ff4040; + text-decoration: none; + font-weight: normal; +} + +.tutorial-button { + position: fixed; + top: 10px; + right: 10px; + background-color: #1e1e1e; /* Light gray color for dark mode */ + color: white; /* Dark text color for dark mode */ + border: 2px solid #fff; + border-radius: 50%; + padding: 10px; + + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease; + width: 20px; /* Adjust the size as needed */ + height: 20px; + display: flex; + align-items: center; + justify-content: center; + } + + .tutorial-button-text { + font-size: 20px; + } + + .tutorial-button:hover { + background-color: #95a5a6; /* Darker gray on hover for dark mode */ + } + + .uvl-tutorial-button { + position: fixed; + top: 10px; + right: 60px; + background-color: #1e1e1e; /* Light gray color for dark mode */ + color: white; /* Dark text color for dark mode */ + border: 2px solid #fff; + border-radius: 50%; + padding: 10px; + + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease; + width: 20px; /* Adjust the size as needed */ + height: 20px; + display: flex; + align-items: center; + justify-content: center; + } + + .uvl-tutorial-button-text { + font-size: 20px; + } + + .uvl-tutorial-button:hover { + background-color: #95a5a6; /* Darker gray on hover for dark mode */ + } + + .tutorial { + color: white; + } \ No newline at end of file diff --git a/WebSocketClient/uvl-icon.PNG b/WebSocketClient/uvl-icon.PNG new file mode 100644 index 0000000..054db41 Binary files /dev/null and b/WebSocketClient/uvl-icon.PNG differ diff --git a/WebSocketLanguageServer/Dockerfile b/WebSocketLanguageServer/Dockerfile index 8d180d3..94ef769 100644 --- a/WebSocketLanguageServer/Dockerfile +++ b/WebSocketLanguageServer/Dockerfile @@ -3,7 +3,8 @@ FROM node:18 WORKDIR /usr/src/app RUN apt-get update -RUN apt-get install z3 +RUN apt-get install -y z3 +RUN apt-get clean COPY package*.json ./ diff --git a/WebSocketLanguageServer/lib/uvls b/WebSocketLanguageServer/lib/uvls index bf9a583..b8d7fc6 100755 Binary files a/WebSocketLanguageServer/lib/uvls and b/WebSocketLanguageServer/lib/uvls differ diff --git a/WebSocketLanguageServer/package-lock.json b/WebSocketLanguageServer/package-lock.json index 4b0cb4c..01101b1 100644 --- a/WebSocketLanguageServer/package-lock.json +++ b/WebSocketLanguageServer/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@types/express": "^4.17.20", "@types/ws": "^8.5.7", - "bidirectional-map": "^1.1.1", "express": "^4.18.2", "vscode-languageserver": "^9.0.1", "vscode-ws-jsonrpc": "^3.0.0", @@ -159,11 +158,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/bidirectional-map": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bidirectional-map/-/bidirectional-map-1.1.1.tgz", - "integrity": "sha512-qfpqHKQbSpc5Eev0W35FZoPnoNUvItDrHbVU0YYKkAOBCZ4Dp3CYi9tjAQcdmqN6ui/9+ioIkb9m0uz8EZqPkA==" - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", diff --git a/WebSocketLanguageServer/src/index.ts b/WebSocketLanguageServer/src/index.ts index 384382d..98ab47f 100644 --- a/WebSocketLanguageServer/src/index.ts +++ b/WebSocketLanguageServer/src/index.ts @@ -13,6 +13,8 @@ import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrp import { Message, InitializeRequest, InitializeParams } from 'vscode-languageserver'; import config from './config.js'; +const MAX_MESSAGE_SIZE = 11000; + const launchLanguageServer = (socket: IWebSocket) => { const serverName: string = 'UVLS'; const ls = config.languageServerBinary; @@ -22,20 +24,31 @@ const launchLanguageServer = (socket: IWebSocket) => { const writer = new WebSocketMessageWriter(socket); const socketConnection = createConnection(reader, writer, () => socket.dispose()); if (serverConnection) { + forward(socketConnection, serverConnection, message => { + if(JSON.stringify(message).length > MAX_MESSAGE_SIZE){ + console.log(`blocked message larger than ${MAX_MESSAGE_SIZE}`); + message = {jsonrpc: "2.0"}; + socketConnection.dispose(); + return message; + } if (Message.isRequest(message)) { console.log(`${serverName} Server received:`); - console.log(message); + logObjectRecursively(message); if (message.method === InitializeRequest.type.method) { const initializeParams = message.params as InitializeParams; initializeParams.processId = process.pid; } } + if(Message.isNotification(message)){ + logObjectRecursively(message); + } if (Message.isResponse(message)) { console.log(`${serverName} Server sent:`); logObjectRecursively(message); } return message; + }); } }; diff --git a/docker-compose.yaml b/docker-compose.yaml index 14eff74..0378073 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,23 +4,20 @@ services: traefik: image: traefik:v3.0 ports: - - 80:80 - - 443:443 - - 30000:30000 - - 3000:3000 + - "80:80" + - "443:443" + - "30000:30000" + - "3000:3000" networks: - proxy volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - letsencrypt:/letsencrypt - #- /var/log:/var/log command: - --api.dashboard=true - --api.insecure=true - --log.level=DEBUG - #- --log.filepath=/var/log/traefik.log - --accesslog=true - #- --accesslog.filepath=/var/log/traefik-access.log - --providers.docker.network=proxy - --providers.docker.exposedByDefault=false - --entrypoints.web.address=:80 @@ -37,11 +34,11 @@ services: - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json - --certificatesResolvers.myresolver.acme.caServer=https://acme-v02.api.letsencrypt.org/directory labels: - - traefik.enable=true - - traefik.http.routers.mydashboard.rule=Host(`${HOSTNAME}`) && (PathPrefix(`/dashboard/`) || PathPrefix(`/api`)) - - traefik.http.routers.mydashboard.service=api@internal - - traefik.http.routers.mydashboard.middlewares=myauth - - traefik.http.middlewares.myauth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/ + - "traefik.enable=true" + - "traefik.http.routers.mydashboard.rule=Host(`${HOSTNAME}`) && (PathPrefix(`/dashboard/`) || PathPrefix(`/api`))" + - "traefik.http.routers.mydashboard.service=api@internal" + - "traefik.http.routers.mydashboard.middlewares=myauth" + - "traefik.http.middlewares.myauth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/" webserver: build: @@ -56,8 +53,8 @@ services: - "traefik.http.services.webserver.loadbalancer.server.port=80" - traefik.http.middlewares.mywwwredirect.redirectregex.regex=^https://www\.(.*) - - traefik.http.middlewares.mywwwredirect.redirectregex.replacement=https://$${1} - - traefik.http.routers.webserver.middlewares=mywwwredirect + - "traefik.http.middlewares.mywwwredirect.redirectregex.replacement=https://$${1}" + - "traefik.http.routers.webserver.middlewares=mywwwredirect" languageserver: build: diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..bdb469a --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1 @@ +sonar.projectKey=UVLP \ No newline at end of file