From 7999e4446783066a9903246a43c56bad0f3a45de Mon Sep 17 00:00:00 2001 From: Shuang Wu Date: Sun, 28 Aug 2022 15:12:44 -0400 Subject: [PATCH] feat(qss): support QSS syntax highlighting --- README.md | 4 +- languages/qss.json | 31 ++ package.json | 20 ++ .../tests/assets/qss/QAbstractScrollArea.qss | 5 + python/tests/assets/qss/QCheckBox.qss | 40 +++ python/tests/assets/qss/QComboBox.qss | 49 +++ python/tests/assets/qss/QDockWidget.qss | 25 ++ snippets/qss.json | 43 +++ syntaxes/qss.tmLanguage.json | 279 ++++++++++++++++++ 9 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 languages/qss.json create mode 100644 python/tests/assets/qss/QAbstractScrollArea.qss create mode 100644 python/tests/assets/qss/QCheckBox.qss create mode 100644 python/tests/assets/qss/QComboBox.qss create mode 100644 python/tests/assets/qss/QDockWidget.qss create mode 100644 snippets/qss.json create mode 100644 syntaxes/qss.tmLanguage.json diff --git a/README.md b/README.md index 3497b52..f2f505c 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ - [x] Support user-defined tool args - [x] Support user-defined tool paths - [x] Build e2e tests (close #8) -- [ ] Languages features +- [x] Languages features - [x] `qml` - [x] `qmldir` - - [ ] `qss` + - [x] `qss` - [x] `qrc` - [x] `ui` - [ ] ~~`qt.ts`~~ diff --git a/languages/qss.json b/languages/qss.json new file mode 100644 index 0000000..1a34a6b --- /dev/null +++ b/languages/qss.json @@ -0,0 +1,31 @@ +{ + "comments": { + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}", "notIn": ["string", "comment"] }, + { "open": "[", "close": "]", "notIn": ["string", "comment"] }, + { "open": "(", "close": ")", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "folding": { + "markers": { + "start": "^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/", + "end": "^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/" + } + } +} diff --git a/package.json b/package.json index 2a7a57b..2dc7428 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,17 @@ ], "configuration": "./languages/qmldir.json" }, + { + "id": "qss", + "extensions": [ + ".qss" + ], + "aliases": [ + "QSS", + "qss" + ], + "configuration": "./languages/qss.json" + }, { "id": "xml", "firstLine": "^<\\?xml\\s+.*\\?>\\s*", @@ -295,6 +306,11 @@ "language": "qmldir", "scopeName": "source.qmldir", "path": "./syntaxes/qmldir.tmLanguage.json" + }, + { + "language": "qss", + "scopeName": "source.qss", + "path": "./syntaxes/qss.tmLanguage.json" } ], "snippets": [ @@ -305,6 +321,10 @@ { "language": "qmldir", "path": "./snippets/qmldir.json" + }, + { + "language": "qss", + "path": "./snippets/qss.json" } ] }, diff --git a/python/tests/assets/qss/QAbstractScrollArea.qss b/python/tests/assets/qss/QAbstractScrollArea.qss new file mode 100644 index 0000000..93aed1b --- /dev/null +++ b/python/tests/assets/qss/QAbstractScrollArea.qss @@ -0,0 +1,5 @@ +QTextEdit, QListView { + background-color: white; + background-image: url(draft.png); + background-attachment: scroll; +} \ No newline at end of file diff --git a/python/tests/assets/qss/QCheckBox.qss b/python/tests/assets/qss/QCheckBox.qss new file mode 100644 index 0000000..25695d1 --- /dev/null +++ b/python/tests/assets/qss/QCheckBox.qss @@ -0,0 +1,40 @@ +QCheckBox { + spacing: 5px; +} + +QCheckBox::indicator { + width: 13px; + height: 13px; +} + +QCheckBox::indicator:unchecked { + image: url(:/images/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:hover { + image: url(:/images/checkbox_unchecked_hover.png); +} + +QCheckBox::indicator:unchecked:pressed { + image: url(:/images/checkbox_unchecked_pressed.png); +} + +QCheckBox::indicator:checked { + image: url(:/images/checkbox_checked.png); +} + +QCheckBox::indicator:checked:hover { + image: url(:/images/checkbox_checked_hover.png); +} + +QCheckBox::indicator:checked:pressed { + image: url(:/images/checkbox_checked_pressed.png); +} + +QCheckBox::indicator:indeterminate:hover { + image: url(:/images/checkbox_indeterminate_hover.png); +} + +QCheckBox::indicator:indeterminate:pressed { + image: url(:/images/checkbox_indeterminate_pressed.png); +} \ No newline at end of file diff --git a/python/tests/assets/qss/QComboBox.qss b/python/tests/assets/qss/QComboBox.qss new file mode 100644 index 0000000..377dc3b --- /dev/null +++ b/python/tests/assets/qss/QComboBox.qss @@ -0,0 +1,49 @@ +QComboBox { + border: 1px solid gray; + border-radius: 3px; + padding: 1px 18px 1px 3px; + min-width: 6em; +} + +QComboBox:editable { + background: white; +} + +QComboBox:!editable, QComboBox::drop-down:editable { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #E1E1E1, stop: 0.4 #DDDDDD, + stop: 0.5 #D8D8D8, stop: 1.0 #D3D3D3); +} + +/* QComboBox gets the "on" state when the popup is open */ +QComboBox:!editable:on, QComboBox::drop-down:editable:on { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #D3D3D3, stop: 0.4 #D8D8D8, + stop: 0.5 #DDDDDD, stop: 1.0 #E1E1E1); +} + +QComboBox:on { /* shift the text when the popup opens */ + padding-top: 3px; + padding-left: 4px; +} + +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 1px; + border-left-color: darkgray; + border-left-style: solid; /* just a single line */ + border-top-right-radius: 3px; /* same radius as the QComboBox */ + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow { + image: url(/usr/share/icons/crystalsvg/16x16/actions/1downarrow.png); +} + +QComboBox::down-arrow:on { /* shift the arrow when popup is open */ + top: 1px; + left: 1px; +} \ No newline at end of file diff --git a/python/tests/assets/qss/QDockWidget.qss b/python/tests/assets/qss/QDockWidget.qss new file mode 100644 index 0000000..aff3396 --- /dev/null +++ b/python/tests/assets/qss/QDockWidget.qss @@ -0,0 +1,25 @@ +QDockWidget { + border: 1px solid lightgray; + titlebar-close-icon: url(close.png); + titlebar-normal-icon: url(undock.png); +} + +QDockWidget::title { + text-align: left; /* align the text to the left */ + background: lightgray; + padding-left: 5px; +} + +QDockWidget::close-button, QDockWidget::float-button { + border: 1px solid transparent; + background: darkgray; + padding: 0px; +} + +QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background: gray; +} + +QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + padding: 1px -1px -1px 1px; +} \ No newline at end of file diff --git a/snippets/qss.json b/snippets/qss.json new file mode 100644 index 0000000..bdb82a1 --- /dev/null +++ b/snippets/qss.json @@ -0,0 +1,43 @@ +{ + "RGB": { + "prefix": "rgb", + "body": ["rgb(${1:r}, ${2:g}, ${3:b})$0"], + "description": "RGB Color" + }, + "RGBA": { + "prefix": "rgba", + "body": ["rgba(${1:r}, ${2:g}, ${3:b}, ${4:alpha})$0"], + "description": "RGBA Color" + }, + "HSV": { + "prefix": "hsv", + "body": ["hsv(${1:h}, ${2:s}, ${3:v})$0"], + "description": "HSV Color" + }, + "HSVA": { + "prefix": "hsva", + "body": ["hsva(${1:h}, ${2:s}, ${3:v}, ${4:alpha})$0"], + "description": "HSVA Color" + }, + "Linear Gradient": { + "prefix": "qlineargradient", + "body": [ + "qlineargradient(x1:${1:x1}, y1:${2:y1}, x2:${3:x2}, y2:${4:y2}, stop:${5:stop}, stop:${6:stop}, stop:${7:stop})$0" + ], + "description": "Linear Gradient" + }, + "Radial Gradient": { + "prefix": "qradialgradient", + "body": [ + "qradialgradient(cx:${1:cx}, cy:${2:cy}, radius:${3:radius}, fx:${4:fx}, fy:${5:fy}, stop:${6:stop}, stop:${7:stop})$0" + ], + "description": "Radial Gradient" + }, + "Conical Gradient": { + "prefix": "qconicalgradient", + "body": [ + "qconicalgradient(cx:${1:cx}, cy:${2:cy}, angle:${3:angle}, stop:${4:stop}, stop:${5:stop})$0" + ], + "description": "Conical Gradient" + } +} diff --git a/syntaxes/qss.tmLanguage.json b/syntaxes/qss.tmLanguage.json new file mode 100644 index 0000000..73536e6 --- /dev/null +++ b/syntaxes/qss.tmLanguage.json @@ -0,0 +1,279 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Qt Style Sheet", + "scopeName": "source.qss", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#rule-list" + }, + { + "include": "#selector" + } + ], + "repository": { + "selector": { + "patterns": [ + { + "include": "#stylable-widgets" + }, + { + "include": "#sub-controls" + }, + { + "include": "#pseudo-states" + }, + { + "include": "#property-selector" + }, + { + "include": "#id-selector" + } + ] + }, + "rule-list": { + "patterns": [ + { + "begin": "\\{", + "end": "\\}", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#properties" + }, + { + "include": "#icon-properties" + } + ] + } + ] + }, + "stylable-widgets": { + "patterns": [ + { + "name": "entity.name.type.qss", + "match": "\\b(QAbstractScrollArea|QAbstractItemView|QCheckBox|QColumnView|QComboBox|QDateEdit|QDateTimeEdit|QDialog|QDialogButtonBox|QDockWidget|QDoubleSpinBox|QFrame|QGroupBox|QHeaderView|QLabel|QLineEdit|QListView|QListWidget|QMainWindow|QMenu|QMenuBar|QMessageBox|QProgressBar|QPlainTextEdit|QPushButton|QRadioButton|QScrollBar|QSizeGrip|QSlider|QSpinBox|QSplitter|QStatusBar|QTabBar|QTabWidget|QTableView|QTableWidget|QTextEdit|QTimeEdit|QToolBar|QToolButton|QToolBox|QToolTip|QTreeView|QTreeWidget|QWidget)\\b" + } + ] + }, + "sub-controls": { + "patterns": [ + { + "name": "entity.other.inherited-class.qss", + "match": "\\b(add-line|add-page|branch|chunk|close-button|corner|down-arrow|down-button|drop-down|float-button|groove|indicator|handle|icon|item|left-arrow|left-corner|menu-arrow|menu-button|menu-indicator|right-arrow|pane|right-corner|scroller|section|separator|sub-line|sub-page|tab|tab-bar|tear|tearoff|text|title|up-arrow|up-button)\\b" + } + ] + }, + "pseudo-states": { + "patterns": [ + { + "name": "keyword.control.qss", + "match": "\\b(active|adjoins-item|alternate|bottom|checked|closable|closed|default|disabled|editable|edit-focus|enabled|exclusive|first|flat|floatable|focus|has-children|has-siblings|horizontal|hover|indeterminate|last|left|maximized|middle|minimized|movable|no-frame|non-exclusive|off|on|only-one|open|next-selected|pressed|previous-selected|read-only|right|selected|top|unchecked|vertical|window)\\b" + } + ] + }, + "property-selector": { + "patterns": [ + { + "begin": "\\[", + "end": "\\]", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#string" + }, + { + "name": "variable.parameter.qml", + "match": "\\b[_a-zA-Z]\\w*\\b" + } + ] + } + ] + }, + "id-selector": { + "patterns": [ + { + "begin": "#", + "end": "\\s+", + "contentName": "entity.name.tag.qss" + } + ] + }, + "properties": { + "patterns": [ + { + "include": "#property-values" + }, + { + "name": "support.type.property-name.qss", + "match": "\\b(paint-alternating-row-colors-for-empty-area|dialogbuttonbox-buttons-have-icons|titlebar-show-tooltips-on-buttons|messagebox-text-interaction-flags|lineedit-password-mask-delay|outline-bottom-right-radius|lineedit-password-character|selection-background-color|outline-bottom-left-radius|border-bottom-right-radius|alternate-background-color|widget-animation-duration|border-bottom-left-radius|show-decoration-selected|outline-top-right-radius|outline-top-left-radius|border-top-right-radius|border-top-left-radius|background-attachment|subcontrol-position|border-bottom-width|border-bottom-style|border-bottom-color|background-position|border-right-width|border-right-style|border-right-color|subcontrol-origin|border-left-width|border-left-style|border-left-color|background-origin|background-repeat|border-top-width|border-top-style|border-top-color|background-image|background-color|text-decoration|selection-color|background-clip|padding-bottom|outline-radius|outline-offset|image-position|gridline-color|padding-right|outline-style|outline-color|margin-bottom|button-layout|border-radius|border-bottom|padding-left|margin-right|border-width|border-style|border-image|border-color|border-right|padding-top|margin-left|font-weight|font-family|border-left|text-align|min-height|max-height|margin-top|font-style|border-top|background|min-width|max-width|icon-size|font-size|position|spacing|padding|outline|opacity|margin|height|bottom|border|width|right|image|color|left|font|top)\\b" + }, + { + "include": "#icon-properties" + } + ] + }, + "icon-properties": { + "patterns": [ + { + "name": "support.type.property-name.qss", + "match": "\\b(backward-icon|cd-icon|computer-icon|desktop-icon|dialog-apply-icon|dialog-cancel-icon|dialog-close-icon|dialog-discard-icon|dialog-help-icon|dialog-no-icon|dialog-ok-icon|dialog-open-icon|dialog-reset-icon|dialog-save-icon|dialog-yes-icon|directory-closed-icon|directory-icon|directory-link-icon|directory-open-icon|dockwidget-close-icon|downarrow-icon|dvd-icon|file-icon|file-link-icon|filedialog-contentsview-icon|filedialog-detailedview-icon|filedialog-end-icon|filedialog-infoview-icon|filedialog-listview-icon|filedialog-new-directory-icon|filedialog-parent-directory-icon|filedialog-start-icon|floppy-icon|forward-icon|harddisk-icon|home-icon|leftarrow-icon|messagebox-critical-icon|messagebox-information-icon|messagebox-question-icon|messagebox-warning-icon|network-icon|rightarrow-icon|titlebar-contexthelp-icon|titlebar-maximize-icon|titlebar-menu-icon|titlebar-minimize-icon|titlebar-normal-icon|titlebar-close-icon|titlebar-shade-icon|titlebar-unshade-icon|trash-icon|uparrow-icon)\\b" + } + ] + }, + "property-values": { + "patterns": [ + { + "begin": ":", + "end": ";|(?=\\})", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#color" + }, + { + "description": "Gradient Type", + "begin": "\\b(qlineargradient|qradialgradient|qconicalgradient)\\s*\\(", + "beginCaptures": { + "1": { + "name": "entity.name.function.qss" + } + }, + "end": "\\)", + "patterns": [ + { + "include": "#comment-block" + }, + { + "name": "variable.parameter.qss", + "match": "\\b(x1|y1|x2|y2|stop|angle|radius|cx|cy|fx|fy)\\b" + }, + { + "include": "#color" + }, + { + "include": "#number" + } + ] + }, + { + "description": "URL Type", + "begin": "\\b(url)\\s*\\(", + "beginCaptures": { + "1": { + "name": "entity.name.function.qss" + } + }, + "end": "\\)", + "contentName": "string.unquoted.qss" + }, + { + "name": "entity.name.function.qss", + "match": "\\bpalette\\s*(?=\\()\\b" + }, + { + "name": "support.constant.property-value.qss", + "match": "\\b(highlighted-text|alternate-base|line-through|link-visited|dot-dot-dash|window-text|button-text|bright-text|underline|no-repeat|highlight|overline|absolute|relative|repeat-y|repeat-x|midlight|selected|disabled|dot-dash|content|padding|oblique|stretch|repeat|window|shadow|button|border|margin|active|italic|normal|outset|groove|double|dotted|dashed|repeat|scroll|center|bottom|light|solid|ridge|inset|fixed|right|text|link|dark|base|bold|none|left|mid|off|top|on)\\b" + }, + { + "name": "constant.language.boolean.qss", + "match": "\\b(true|false)\\b" + }, + { + "include": "#string" + }, + { + "include": "#number" + } + ] + } + ] + }, + "comment-block": { + "patterns": [ + { + "begin": "/\\*", + "end": "\\*/", + "name": "comment.block.qss" + } + ] + }, + "string": { + "description": "String literal with double or signle quote.", + "patterns": [ + { + "name": "string.quoted.single.qml", + "begin": "'", + "end": "'" + }, + { + "name": "string.quoted.double.qml", + "begin": "\"", + "end": "\"" + } + ] + }, + "color": { + "patterns": [ + { + "description": "Color Type", + "begin": "\\b(rgb|rgba|hsv|hsva)\\s*\\(", + "beginCaptures": { + "1": { + "name": "entity.name.function.qss" + } + }, + "end": "\\)", + "patterns": [ + { + "include": "#comment-block" + }, + { + "include": "#number" + } + ] + }, + { + "name": "support.constant.property-value.named-color.qss", + "match": "\\b(white|black|red|darkred|green|darkgreen|blue|darkblue|cyan|darkcyan|magenta|darkmagenta|yellow|darkyellow|gray|darkgray|lightgray|transparent|color0|color1)\\b" + }, + { + "name": "support.constant.property-value.color.qss", + "match": "#[0-9a-fA-F]{6}\\b" + } + ] + }, + "number": { + "patterns": [ + { + "description": "floating number", + "name": "constant.numeric.qss", + "match": "\\b(\\d+)?\\.(\\d+)\\b" + }, + { + "description": "percentage", + "name": "constant.numeric.qss", + "match": "\\b(\\d+)%" + }, + { + "description": "length", + "name": "constant.numeric.qss", + "match": "\\b(\\d+)(px|pt|em|ex)?\\b" + }, + { + "description": "integer", + "name": "constant.numeric.qss", + "match": "\\b(\\d+)\\b" + } + ] + } + } +}