diff --git a/.eslintignore b/.eslintignore index cf64a52..0693334 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ .eslintrc.cjs +esbuild.mjs diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 0000000..db5e75b --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,62 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import { build, context} from 'esbuild'; + +/** @type BuildOptions */ +const baseConfig = { + bundle: true, + minify: process.env.NODE_ENV === 'production', + sourcemap: process.env.NODE_ENV !== 'production' +}; + +// Config for extension source code (to be run in a Node-based context) +/** @type BuildOptions */ +const extensionConfig = { + ...baseConfig, + platform: 'node', + mainFields: ['module', 'main'], + tsconfig: './tsconfig.json', + format: 'cjs', + entryPoints: ['./src/extension.ts'], + outfile: './out/extension.js', + external: ['vscode'] +}; + +// Config for webview source code (to be run in a web-based context) +/** @type BuildOptions */ +const webviewConfig = { + ...baseConfig, + target: 'es2020', + format: 'esm', + entryPoints: ['./src/editors/ui/webview-ui/main.ts'], + outfile: './out/editors/ui/webview-ui/main.js' +}; + +// Build script +(async () => { + const args = process.argv.slice(2); + try { + if (args.includes('--watch')) { + const extCtx = await context({ + ...extensionConfig + }); + await extCtx.watch(); + await extCtx.dispose(); + const webCtx = await context({ + ...webviewConfig + }); + await webCtx.watch(); + await webCtx.dispose(); + console.log('[watch] build finished'); + } else { + // Build extension and webview code + await build(extensionConfig); + await build(webviewConfig); + console.log('build complete'); + } + } catch (err) { + process.stderr.write(err.stderr); + process.exit(1); + } +})(); diff --git a/package-lock.json b/package-lock.json index 0c8ce4e..25ce29b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,9 @@ "": { "name": "qt", "version": "0.0.1", - "hasInstallScript": true, "dependencies": { "@vscode/l10n": "^0.0.16", + "@vscode/webview-ui-toolkit": "^1.4.0", "command-exists": "^1.2.3", "ts-sinon": "^2.0.2", "typescript": "^5.2.2" @@ -631,6 +631,47 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@microsoft/fast-element": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.12.0.tgz", + "integrity": "sha512-gQutuDHPKNxUEcQ4pypZT4Wmrbapus+P9s3bR/SEOLsMbNqNoXigGImITygI5zhb+aA5rzflM6O8YWkmRbGkPA==" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.49.5", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.49.5.tgz", + "integrity": "sha512-3PpG1BNmZ5kUM1goYU3SsxjsM2H7Rk0ZmpDJ7mnRhWDgKiM5SzJ02KvALRUqDrJQoeDnkW0Q2Q+r9SkEd68Gpg==", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-foundation/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.3.23.tgz", + "integrity": "sha512-iuL+J2AFKJ1mwUBxSp+bqzt4X93kQwj1jpVgHgw2VRzCOTl7wzta6X+lvRIVg4eoyLfmeVSMkB+q3PD87T/MyQ==", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.5" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1144,6 +1185,20 @@ "node": "*" } }, + "node_modules/@vscode/webview-ui-toolkit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz", + "integrity": "sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.4", + "@microsoft/fast-react-wrapper": "^0.3.22", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2310,6 +2365,11 @@ "node": ">=0.10.0" } }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2964,6 +3024,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3190,6 +3256,18 @@ "node": ">=8" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -4023,6 +4101,18 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -4504,6 +4594,11 @@ "node": ">=4" } }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -4659,6 +4754,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index fe74594..32754a8 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "Other" ], "activationEvents": [ - "workspaceContains:*.pro", "workspaceContains:*.qrc", "workspaceContains:*.qdoc", + "workspaceContains:*.ui", "onCommand:cmake.activeFolderName", "onCommand:cmake.activeFolderPath", "onCommand:cmake.activeConfigurePresetName", @@ -164,6 +164,21 @@ ] } ], + "customEditors": [ + { + "viewType": "qt.uiEditor", + "displayName": "Qt UI Editor", + "extensions": [ + ".ui" + ], + "priority": "default", + "selector": [ + { + "filenamePattern": "*.ui" + } + ] + } + ], "languages": [ { "id": "pro", @@ -176,13 +191,18 @@ }, { "id": "qrc", + "configuration": "./res/lang/qrc/language-configuration.json", "extensions": [ ".qrc", ".qrc.cmakein" ], "aliases": [ "Qrc" - ] + ], + "icon": { + "light": "res/icons/qrc.png", + "dark": "res/icons/qrc.png" + } }, { "id": "qdoc", @@ -200,15 +220,16 @@ }, { "id": "ui", + "configuration": "./res/lang/ui/language-configuration.json", "extensions": [ ".ui" ], "aliases": [ - "Ui" + "ui" ], "icon": { - "light": "res/icons/qt.svg", - "dark": "res/icons/qt.svg" + "light": "res/icons/ui.svg", + "dark": "res/icons/ui.svg" } } ], @@ -244,6 +265,16 @@ "language": "qdoc", "scopeName": "source.qdoc", "path": "./res/lang/qdoc/qdoc.tmLanguage.json" + }, + { + "language": "ui", + "scopeName": "source.ui", + "path": "./res/lang/ui/ui.tmLanguage.json" + }, + { + "language": "qrc", + "scopeName": "source.qrc", + "path": "./res/lang/qrc/qrc.tmLanguage.json" } ], "configuration": { @@ -269,25 +300,24 @@ "ms-vscode.cmake-tools" ], "scripts": { - "vscode:prepublish": "npm run esbuild-base -- --minify", - "esbuild-base": "esbuild ./src/extension.ts --bundle --tsconfig=./tsconfig.json --outfile=out/extension.js --external:vscode --format=cjs --platform=node", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", + "vscode:prepublish": "NODE_ENV=production node ./esbuild.mjs", + "compile": "node ./esbuild.mjs", + "watch": "node ./esbuild.mjs --watch", "pretest": "npm run compile && npm run lint", "lint": "npm run prettier && npx eslint . --fix", "unitTests": "npm run pretest && node ./out/test/unit/runTest.js", "integrationTests": "npm run pretest && node ./out/test/integration/runTest.js", "allTests": "ts-node ./src/scripts/run_all_tests.ts", - "prettier": "prettier --write \"**/*.{js,ts,json}\"", + "prettier": "prettier --write \"**/*.{js,ts,json}\" --log-level silent", "package": "npm ci && vsce package --out out" }, "devDependencies": { "@types/chai": "^4.3.10", + "@types/command-exists": "^1.2.3", "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "^20.2.5", "@types/vscode": "^1.78.0", - "@types/command-exists": "^1.2.3", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "@vscode/l10n-dev": "^0.0.30", @@ -305,6 +335,7 @@ }, "dependencies": { "@vscode/l10n": "^0.0.16", + "@vscode/webview-ui-toolkit": "^1.4.0", "command-exists": "^1.2.3", "ts-sinon": "^2.0.2", "typescript": "^5.2.2" diff --git a/res/icons/qrc.png b/res/icons/qrc.png new file mode 100644 index 0000000..7cd2214 Binary files /dev/null and b/res/icons/qrc.png differ diff --git a/res/icons/ui.svg b/res/icons/ui.svg new file mode 100644 index 0000000..138ed02 --- /dev/null +++ b/res/icons/ui.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/lang/qrc/language-configuration.json b/res/lang/qrc/language-configuration.json new file mode 100644 index 0000000..6d332b7 --- /dev/null +++ b/res/lang/qrc/language-configuration.json @@ -0,0 +1,37 @@ +{ + "comments": { + "blockComment": [""] + }, + "brackets": [ + [""], + ["<", ">"], + ["{", "}"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "'", "close": "'", "notIn": ["string"] }, + { "open": "", "notIn": ["comment", "string"] } + ], + "surroundingPairs": [ + { "open": "'", "close": "'" }, + { "open": "\"", "close": "\"" }, + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "<", "close": ">" } + ], + "colorizedBracketPairs": [], + "folding": { + "markers": { + "start": "^\\s*", + "end": "^\\s*" + } + }, + "wordPattern": { + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)" + } +} diff --git a/res/lang/qrc/qrc.tmLanguage.json b/res/lang/qrc/qrc.tmLanguage.json new file mode 100644 index 0000000..1e45258 --- /dev/null +++ b/res/lang/qrc/qrc.tmLanguage.json @@ -0,0 +1,387 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/atom/language-xml/blob/master/grammars/xml.cson", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/atom/language-xml/commit/7bc75dfe779ad5b35d9bf4013d9181864358cb49", + "name": "qrc", + "scopeName": "source.qrc", + "patterns": [ + { + "begin": "(<\\?)\\s*([-_a-zA-Z0-9]+)", + "captures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "entity.name.tag.xml" + } + }, + "end": "(\\?>)", + "name": "meta.tag.preprocessor.xml", + "patterns": [ + { + "match": " ([a-zA-Z-]+)", + "name": "entity.other.attribute-name.xml" + }, + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + { + "begin": "()", + "name": "meta.tag.sgml.doctype.xml", + "patterns": [ + { + "include": "#internalSubset" + } + ] + }, + { + "include": "#comments" + }, + { + "begin": "(<)((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(?=(\\s[^>]*)?>)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "entity.name.tag.xml" + }, + "3": { + "name": "entity.name.tag.namespace.xml" + }, + "4": { + "name": "punctuation.separator.namespace.xml" + }, + "5": { + "name": "entity.name.tag.localname.xml" + } + }, + "end": "(>)()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "punctuation.definition.tag.xml" + }, + "3": { + "name": "entity.name.tag.xml" + }, + "4": { + "name": "entity.name.tag.namespace.xml" + }, + "5": { + "name": "punctuation.separator.namespace.xml" + }, + "6": { + "name": "entity.name.tag.localname.xml" + }, + "7": { + "name": "punctuation.definition.tag.xml" + } + }, + "name": "meta.tag.no-content.xml", + "patterns": [ + { + "include": "#tagStuff" + } + ] + }, + { + "begin": "()", + "name": "meta.tag.xml", + "patterns": [ + { + "include": "#tagStuff" + } + ] + }, + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + }, + { + "begin": "<%@", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.xml" + } + }, + "end": "%>", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.xml" + } + }, + "name": "source.java-props.embedded.xml", + "patterns": [ + { + "match": "page|include|taglib", + "name": "keyword.other.page-props.xml" + } + ] + }, + { + "begin": "<%[!=]?(?!--)", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.xml" + } + }, + "end": "(?!--)%>", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.xml" + } + }, + "name": "source.java.embedded.xml", + "patterns": [ + { + "include": "source.java" + } + ] + }, + { + "begin": "", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.unquoted.cdata.xml" + } + ], + "repository": { + "EntityDecl": { + "begin": "()", + "patterns": [ + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + "bare-ampersand": { + "match": "&", + "name": "invalid.illegal.bad-ampersand.xml" + }, + "doublequotedString": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.xml" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.quoted.double.xml", + "patterns": [ + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + } + ] + }, + "entity": { + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + }, + "3": { + "name": "punctuation.definition.constant.xml" + } + }, + "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)", + "name": "constant.character.entity.xml" + }, + "internalSubset": { + "begin": "(\\[)", + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + } + }, + "end": "(\\])", + "name": "meta.internalsubset.xml", + "patterns": [ + { + "include": "#EntityDecl" + }, + { + "include": "#parameterEntity" + }, + { + "include": "#comments" + } + ] + }, + "parameterEntity": { + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + }, + "3": { + "name": "punctuation.definition.constant.xml" + } + }, + "match": "(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)", + "name": "constant.character.parameter-entity.xml" + }, + "singlequotedString": { + "begin": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.xml" + } + }, + "end": "'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.quoted.single.xml", + "patterns": [ + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + } + ] + }, + "tagStuff": { + "patterns": [ + { + "captures": { + "1": { + "name": "entity.other.attribute-name.namespace.xml" + }, + "2": { + "name": "entity.other.attribute-name.xml" + }, + "3": { + "name": "punctuation.separator.namespace.xml" + }, + "4": { + "name": "entity.other.attribute-name.localname.xml" + } + }, + "match": "(?:^|\\s+)(?:([-\\w.]+)((:)))?([-\\w.:]+)\\s*=" + }, + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "<%--", + "captures": { + "0": { + "name": "punctuation.definition.comment.xml" + }, + "end": "--%>", + "name": "comment.block.xml" + } + }, + { + "begin": "", + "name": "comment.block.xml", + "patterns": [ + { + "begin": "--(?!>)", + "captures": { + "0": { + "name": "invalid.illegal.bad-comments-or-CDATA.xml" + } + } + } + ] + } + ] + } + } +} diff --git a/res/lang/ui/language-configuration.json b/res/lang/ui/language-configuration.json new file mode 100644 index 0000000..6d332b7 --- /dev/null +++ b/res/lang/ui/language-configuration.json @@ -0,0 +1,37 @@ +{ + "comments": { + "blockComment": [""] + }, + "brackets": [ + [""], + ["<", ">"], + ["{", "}"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "'", "close": "'", "notIn": ["string"] }, + { "open": "", "notIn": ["comment", "string"] } + ], + "surroundingPairs": [ + { "open": "'", "close": "'" }, + { "open": "\"", "close": "\"" }, + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "<", "close": ">" } + ], + "colorizedBracketPairs": [], + "folding": { + "markers": { + "start": "^\\s*", + "end": "^\\s*" + } + }, + "wordPattern": { + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)" + } +} diff --git a/res/lang/ui/ui.tmLanguage.json b/res/lang/ui/ui.tmLanguage.json new file mode 100644 index 0000000..10e3e62 --- /dev/null +++ b/res/lang/ui/ui.tmLanguage.json @@ -0,0 +1,387 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/atom/language-xml/blob/master/grammars/xml.cson", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/atom/language-xml/commit/7bc75dfe779ad5b35d9bf4013d9181864358cb49", + "name": "ui", + "scopeName": "source.ui", + "patterns": [ + { + "begin": "(<\\?)\\s*([-_a-zA-Z0-9]+)", + "captures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "entity.name.tag.xml" + } + }, + "end": "(\\?>)", + "name": "meta.tag.preprocessor.xml", + "patterns": [ + { + "match": " ([a-zA-Z-]+)", + "name": "entity.other.attribute-name.xml" + }, + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + { + "begin": "()", + "name": "meta.tag.sgml.doctype.xml", + "patterns": [ + { + "include": "#internalSubset" + } + ] + }, + { + "include": "#comments" + }, + { + "begin": "(<)((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(?=(\\s[^>]*)?>)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "entity.name.tag.xml" + }, + "3": { + "name": "entity.name.tag.namespace.xml" + }, + "4": { + "name": "punctuation.separator.namespace.xml" + }, + "5": { + "name": "entity.name.tag.localname.xml" + } + }, + "end": "(>)()", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.xml" + }, + "2": { + "name": "punctuation.definition.tag.xml" + }, + "3": { + "name": "entity.name.tag.xml" + }, + "4": { + "name": "entity.name.tag.namespace.xml" + }, + "5": { + "name": "punctuation.separator.namespace.xml" + }, + "6": { + "name": "entity.name.tag.localname.xml" + }, + "7": { + "name": "punctuation.definition.tag.xml" + } + }, + "name": "meta.tag.no-content.xml", + "patterns": [ + { + "include": "#tagStuff" + } + ] + }, + { + "begin": "()", + "name": "meta.tag.xml", + "patterns": [ + { + "include": "#tagStuff" + } + ] + }, + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + }, + { + "begin": "<%@", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.xml" + } + }, + "end": "%>", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.xml" + } + }, + "name": "source.java-props.embedded.xml", + "patterns": [ + { + "match": "page|include|taglib", + "name": "keyword.other.page-props.xml" + } + ] + }, + { + "begin": "<%[!=]?(?!--)", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.xml" + } + }, + "end": "(?!--)%>", + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.xml" + } + }, + "name": "source.java.embedded.xml", + "patterns": [ + { + "include": "source.java" + } + ] + }, + { + "begin": "", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.unquoted.cdata.xml" + } + ], + "repository": { + "EntityDecl": { + "begin": "()", + "patterns": [ + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + "bare-ampersand": { + "match": "&", + "name": "invalid.illegal.bad-ampersand.xml" + }, + "doublequotedString": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.xml" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.quoted.double.xml", + "patterns": [ + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + } + ] + }, + "entity": { + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + }, + "3": { + "name": "punctuation.definition.constant.xml" + } + }, + "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)", + "name": "constant.character.entity.xml" + }, + "internalSubset": { + "begin": "(\\[)", + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + } + }, + "end": "(\\])", + "name": "meta.internalsubset.xml", + "patterns": [ + { + "include": "#EntityDecl" + }, + { + "include": "#parameterEntity" + }, + { + "include": "#comments" + } + ] + }, + "parameterEntity": { + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + }, + "3": { + "name": "punctuation.definition.constant.xml" + } + }, + "match": "(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)", + "name": "constant.character.parameter-entity.xml" + }, + "singlequotedString": { + "begin": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.xml" + } + }, + "end": "'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.xml" + } + }, + "name": "string.quoted.single.xml", + "patterns": [ + { + "include": "#entity" + }, + { + "include": "#bare-ampersand" + } + ] + }, + "tagStuff": { + "patterns": [ + { + "captures": { + "1": { + "name": "entity.other.attribute-name.namespace.xml" + }, + "2": { + "name": "entity.other.attribute-name.xml" + }, + "3": { + "name": "punctuation.separator.namespace.xml" + }, + "4": { + "name": "entity.other.attribute-name.localname.xml" + } + }, + "match": "(?:^|\\s+)(?:([-\\w.]+)((:)))?([-\\w.:]+)\\s*=" + }, + { + "include": "#doublequotedString" + }, + { + "include": "#singlequotedString" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "<%--", + "captures": { + "0": { + "name": "punctuation.definition.comment.xml" + }, + "end": "--%>", + "name": "comment.block.xml" + } + }, + { + "begin": "", + "name": "comment.block.xml", + "patterns": [ + { + "begin": "--(?!>)", + "captures": { + "0": { + "name": "invalid.illegal.bad-comments-or-CDATA.xml" + } + } + } + ] + } + ] + } + } +} diff --git a/src/commands/file-ext-qrc.ts b/src/commands/file-ext-qrc.ts deleted file mode 100644 index 858be1b..0000000 --- a/src/commands/file-ext-qrc.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only - -import * as vscode from 'vscode'; - -export function registerQrcFile() { - return vscode.workspace.onDidOpenTextDocument((document) => { - if (document.fileName.toLowerCase().endsWith('.qrc')) { - // The code you place here will be executed every time a .qrc file is opened - // TODO : parse the .qrc file and provide IntelliSense for the resources - console.log('.qrc file', document.fileName); - void vscode.languages.setTextDocumentLanguage(document, 'xml'); - } - }); -} diff --git a/src/commands/file-ext-ui.ts b/src/commands/file-ext-ui.ts index 4c1f5ee..4a58710 100644 --- a/src/commands/file-ext-ui.ts +++ b/src/commands/file-ext-ui.ts @@ -9,7 +9,7 @@ import * as qtpath from '../util/get-qt-paths'; import * as local from '../util/localize'; import { getSelectedQtInstallationPath } from './register-qt-path'; -async function getQtDesignerPath() { +export async function getQtDesignerPath() { const selectedQtPath = await getSelectedQtInstallationPath(); if (selectedQtPath) { return await qtpath.locateQtDesignerExePath(selectedQtPath); @@ -18,11 +18,7 @@ async function getQtDesignerPath() { } const OpenedUiDocuments = new Map(); -async function openUiFileInQtDesigner( - textEditor: vscode.TextEditor, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _edit: vscode.TextEditorEdit -) { +async function openUiFileInQtDesigner(textEditor: vscode.TextEditor) { const document = textEditor.document; const uiFsPath = document.uri.fsPath || ''; if (uiFsPath.endsWith('.ui')) { @@ -48,15 +44,10 @@ export function registerUiFile(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerTextEditorCommand( 'vscode-qt-tools.openUiFileInQtDesigner', - (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) => { - void openUiFileInQtDesigner(textEditor, edit); + (textEditor: vscode.TextEditor) => { + void openUiFileInQtDesigner(textEditor); } ), - vscode.workspace.onDidOpenTextDocument((document) => { - if (document.fileName.toLowerCase().endsWith('.ui')) { - void vscode.languages.setTextDocumentLanguage(document, 'QML'); - } - }), vscode.workspace.onDidCloseTextDocument((document) => { const uiFsPath = document.uri.fsPath || ''; if (uiFsPath.endsWith('.ui')) { diff --git a/src/designer-client.ts b/src/designer-client.ts new file mode 100644 index 0000000..dd5bf49 --- /dev/null +++ b/src/designer-client.ts @@ -0,0 +1,40 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import * as child_process from 'child_process'; +import { getQtDesignerPath } from './commands/file-ext-ui'; +import { designerServer } from './designer-server'; + +class DesignerClient { + private process: child_process.ChildProcess | undefined; + + public async start() { + const designerExePath = await getQtDesignerPath(); + const designerServerPort = designerServer.getPort(); + if (!designerServerPort) { + throw new Error('Designer server is not running'); + } + + if (designerExePath) { + this.process = child_process + .spawn(designerExePath, ['--client ' + designerServerPort.toString()], { + shell: true + }) + .on('exit', () => { + this.process = undefined; + }); + } + } + + public isRunning() { + return this.process !== undefined; + } + + public stop() { + if (this.process) { + this.process.kill(); + } + } +} + +export const designerClient: DesignerClient = new DesignerClient(); diff --git a/src/designer-server.ts b/src/designer-server.ts new file mode 100644 index 0000000..d4fbb88 --- /dev/null +++ b/src/designer-server.ts @@ -0,0 +1,64 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import * as net from 'net'; +import { IsWindows } from './util/os'; + +class DesignerServer { + private server: net.Server; + private client: net.Socket | undefined; + private port: number; + private static readonly newLine = IsWindows ? '\r\n' : '\n'; + + constructor(port = 0) { + this.port = port; + this.server = net.createServer((socket) => { + socket.pipe(socket); + }); + this.client = undefined; + } + + public start() { + this.server + .listen(this.port, () => { + console.log(`Designer server is listening on ${this.port}`); + const myport = this.getPort(); + void myport; + }) + .on('connection', (socket) => { + this.onConnection(socket); + }) + .on('error', (err) => { + throw err; + }); + } + + private onConnection(socket: net.Socket) { + console.log('Designer server is connected'); + // print the address of the client + console.log(socket.remoteAddress); + this.client = socket; + } + + public stop() { + this.server.close(); + } + + public getPort() { + if (this.server.address()) { + return (this.server.address() as net.AddressInfo).port; + } + } + public isClientConnected() { + return this.client !== undefined && !this.client.destroyed; + } + + public sendFile(filePath: string) { + if (!this.client) { + throw new Error('No client connected'); + } + this.client.write(filePath.toString() + DesignerServer.newLine); + } +} + +export const designerServer = new DesignerServer(); diff --git a/src/editors/ui/ui-editor.ts b/src/editors/ui/ui-editor.ts new file mode 100644 index 0000000..5467870 --- /dev/null +++ b/src/editors/ui/ui-editor.ts @@ -0,0 +1,109 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import * as vscode from 'vscode'; +import { getNonce, getUri } from '../util'; +import { designerServer } from '../../designer-server'; +import { designerClient } from '../../designer-client'; + +export class UIEditorProvider implements vscode.CustomTextEditorProvider { + constructor(private readonly context: vscode.ExtensionContext) {} + + private static readonly viewType = 'qt.uiEditor'; + public static register(context: vscode.ExtensionContext): vscode.Disposable { + const provider = new UIEditorProvider(context); + const providerRegistration = vscode.window.registerCustomEditorProvider( + UIEditorProvider.viewType, + provider + ); + return providerRegistration; + } + public resolveCustomTextEditor( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): Promise { + void _token; + webviewPanel.webview.options = { + enableScripts: true + }; + webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview); + function updateWebview() { + void webviewPanel.webview.postMessage({ + type: 'update', + text: document.getText() + }); + } + const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument( + (e) => { + if (e.document.uri.toString() === document.uri.toString()) { + updateWebview(); + } + } + ); + + webviewPanel.onDidDispose(() => { + changeDocumentSubscription.dispose(); + }); + + webviewPanel.webview.onDidReceiveMessage(async (e: { type: string }) => { + switch (e.type) { + case 'run': + if (!designerClient.isRunning()) { + await designerClient.start(); + } + // wait for the client to connect + while (!designerServer.isClientConnected()) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + designerServer.sendFile(document.uri.fsPath); + break; + default: + throw new Error('Unknown message type'); + } + }); + + updateWebview(); + return Promise.resolve(); + } + + private getHtmlForWebview(webview: vscode.Webview): string { + // Use a nonce to whitelist which scripts can be run + const nonce = getNonce(); + const scriptUri = getUri(webview, this.context.extensionUri, [ + 'out', + 'editors', + 'ui', + 'webview-ui', + 'main.js' + ]); + + // prettier-ignore + const html = + ` + + + + + Open this file with Qt Designer + + + +
+ Open this file with Qt Designer +
+ + + `; + return html; + } +} diff --git a/src/editors/ui/webview-ui/main.ts b/src/editors/ui/webview-ui/main.ts new file mode 100644 index 0000000..064803c --- /dev/null +++ b/src/editors/ui/webview-ui/main.ts @@ -0,0 +1,56 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import { + provideVSCodeDesignSystem, + vsCodeButton +} from '@vscode/webview-ui-toolkit'; + +declare function acquireVsCodeApi(): { postMessage(message: unknown): void }; + +provideVSCodeDesignSystem().register(vsCodeButton()); + +const vscode = acquireVsCodeApi(); + +window.addEventListener('load', main); + +function main() { + const buttons = document.querySelectorAll('vscode-button'); + if (buttons.length === 0) { + throw new Error('No buttons found'); + } + const openWithDesignerButton = document.getElementById( + 'openWithDesignerButton' + ); + if (openWithDesignerButton) { + openWithDesignerButton.focus(); + } + function onOpenWithDesignerButtonClick() { + vscode.postMessage({ + type: 'run' + }); + } + openWithDesignerButton?.addEventListener( + 'click', + onOpenWithDesignerButtonClick + ); + + openWithDesignerButton?.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + event.preventDefault(); + onOpenWithDesignerButtonClick(); + } + }); + document.addEventListener('keydown', function (event) { + // if any arrow key is pressed, focus the this button + if ( + event.key === 'ArrowUp' || + event.key === 'ArrowDown' || + event.key === 'ArrowLeft' || + event.key === 'ArrowRight' + ) { + event.preventDefault(); + openWithDesignerButton?.focus(); + } + }); +} diff --git a/src/editors/util.ts b/src/editors/util.ts new file mode 100644 index 0000000..58f5777 --- /dev/null +++ b/src/editors/util.ts @@ -0,0 +1,22 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +import { Uri, Webview } from 'vscode'; + +export function getNonce() { + let text = ''; + const possible = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export function getUri( + webview: Webview, + extensionUri: Uri, + pathList: string[] +) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} diff --git a/src/extension.ts b/src/extension.ts index cf703bc..d51de3f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,6 @@ import { } from './commands/register-qt-path'; import { initCMakeKits } from './commands/detect-qt-cmake'; import { registerProFile } from './commands/file-ext-pro'; -import { registerQrcFile } from './commands/file-ext-qrc'; import { registerQdocFile } from './commands/file-ext-qdoc'; import { registerUiFile } from './commands/file-ext-ui'; import { registerKitDirectoryCommand } from './commands/kit-directory'; @@ -20,6 +19,9 @@ import { initStateManager } from './state'; import { configChecker } from './util/config'; import { registerResetQtExtCommand } from './commands/reset-qt-ext'; import { registerNatvisCommand } from './commands/natvis'; +import { designerServer } from './designer-server'; +import { designerClient } from './designer-client'; +import { UIEditorProvider } from './editors/ui/ui-editor'; export async function activate(context: vscode.ExtensionContext) { const promiseActivateCMake = vscode.extensions @@ -29,17 +31,19 @@ export async function activate(context: vscode.ExtensionContext) { initCMakeKits(context); initStateManager(context); + designerServer.start(); + registerUiFile(context); registerQtCommand(context); context.subscriptions.push( registerProFile(), - registerQrcFile(), registerQdocFile(), registerKitDirectoryCommand(), registerMinGWgdbCommand(), registerResetQtExtCommand(), - ...registerNatvisCommand() + ...registerNatvisCommand(), + UIEditorProvider.register(context) ); registerConfigWatchers(context); @@ -65,5 +69,7 @@ function registerConfigWatchers(context: vscode.ExtensionContext) { } export function deactivate() { + designerServer.stop(); + designerClient.stop(); console.log('Deactivating vscode-qt-tools'); } diff --git a/tsconfig.json b/tsconfig.json index 4f1649e..e23f5e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "target": "ES2020", "types": ["node"], "outDir": "out", - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "sourceMap": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */