From a1748e1604f295244594ece044925dbfdc46a7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=89=B2?= Date: Thu, 25 Nov 2021 17:44:37 +0800 Subject: [PATCH] feat: add `currentLine`, close #43 (#44) --- README.md | 1 + README.zh-CN.md | 1 + examples/demo.less | 1 + i18n/zh-cn/package.i18n.json | 3 + package.json | 37 +++++++++- package.nls.json | 3 + package.nls.zh-cn.json | 3 + src/extension.ts | 2 + src/hover.ts | 2 +- src/interface.ts | 3 + src/line-annotation.ts | 133 +++++++++++++++++++++++++++++++++++ src/rules.ts | 15 +++- 12 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/line-annotation.ts diff --git a/README.md b/README.md index 2108630..b089469 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Secondly, you can also configure the global. Open your user and workspace settin | `cssrem.ingoresViaCommand` | Ignores `px` to `rem` when trigger command (Unit: `string[]`), can be set `[ "1px", "0.5px" ]` | `[]` | | `cssrem.addMark` | Whether to enabled mark | `false` | | `cssrem.hover` | Whether to enable display conversion data on hover, `disabled`: Disabled, `always` Anything, `onlyMark`: Only valid when `cssrem.addMark` is `true` | `onlyMark` | +| `cssrem.currentLine` | Whether to display mark in after line, `disabled`: Disabled, `show` Show | `show` | | `cssrem.ingores` | Ignore file list, like this: `[ 'demo.less', 'src' ]` | `string[]` | | `cssrem.languages` | Support language list, default: `[ 'html', 'vue', 'css', 'postcss', 'less', 'scss', 'sass', 'stylus', 'javascriptreact', 'typescriptreact' ]` | `string[]` | | `cssrem.wxss` | **WXSS小程序样式** Whether to enable WXSS support | `false` | diff --git a/README.zh-CN.md b/README.zh-CN.md index 34a3b98..78a922e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -48,6 +48,7 @@ html vue css less scss sass stylus tpl(php smarty3) tsx jsx | `cssrem.ingoresViaCommand` | 当使用命令行批量转换时,允许忽略部分 `px` 值不转换成 `rem`(单位:`string[]`),例如:`[ "1px", "0.5px" ]` | `[]` | | `cssrem.addMark` | 是否启用加上标记 | `false` | | `cssrem.hover` | 是否启用悬停时显示转换数据, `disabled`: Disabled, `always` Anything, `onlyMark`: Only valid when `cssrem.addMark` is `true` | `onlyMark` | +| `cssrem.currentLine` | 是否当前行尾显示标记,`disabled`: Disabled, `show` Show | `show` | | `cssrem.ingores` | 忽略文件清单,例如:`[ 'demo.less', 'src' ]` | `string[]` | | `cssrem.languages` | 支持语言清单,默认:`[ 'html', 'vue', 'css', 'postcss', 'less', 'scss', 'sass', 'stylus', 'javascriptreact', 'typescriptreact' ]` | `string[]` | | `cssrem.wxss` | **WXSS小程序样式** 是否启用WXSS支持 | `false` | diff --git a/examples/demo.less b/examples/demo.less index 6616f83..c464cd9 100644 --- a/examples/demo.less +++ b/examples/demo.less @@ -1,3 +1,4 @@ body { font-size: 1rem; + padding: 10px 12px 1rem 1.2rem; } diff --git a/i18n/zh-cn/package.i18n.json b/i18n/zh-cn/package.i18n.json index 5d0206f..6fdfddf 100644 --- a/i18n/zh-cn/package.i18n.json +++ b/i18n/zh-cn/package.i18n.json @@ -8,6 +8,9 @@ "cssrem.hover.disabled": "禁用", "cssrem.hover.always": "任何,不管是否由插件转换而来的都显示", "cssrem.hover.onlyMark": "只对已经 `cssrem.addMark` 为 `true` 生效", + "cssrem.currentLine": "是否当前行尾显示标记, default: show", + "cssrem.currentLine.disabled": "禁用", + "cssrem.currentLine.show": "显示", "cssrem.ingores": "忽略文件清单,例如:`[ 'demo.less', 'src' ]`", "cssrem.languages": "支持语言清单,默认:`[ 'html', 'vue', 'css', 'postcss', 'less', 'scss', 'sass', 'stylus', 'javascriptreact', 'typescriptreact' ]`", "cssrem.wxss": "是否启用WXSS支持", diff --git a/package.json b/package.json index 50888df..8b1f8c2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cssrem", "displayName": "px to rem & rpx (cssrem)", "description": "Converts between px and rem & rpx units in VSCode", - "version": "2.5.1", + "version": "2.6.0", "publisher": "cipchk", "icon": "icon.png", "license": "MIT", @@ -156,6 +156,19 @@ }, "description": "%cssrem.languages%" }, + "cssrem.currentLine": { + "type": "string", + "enum": [ + "disabled", + "show" + ], + "default": "show", + "markdownEnumDescriptions": [ + "%cssrem.currentLine.disabled%", + "%cssrem.currentLine.show%" + ], + "description": "%cssrem.currentLine%" + }, "cssrem.wxss": { "type": "boolean", "default": false, @@ -172,7 +185,27 @@ "description": "%cssrem.wxssDeviceWidth%" } } - } + }, + "colors": [ + { + "id": "extension.cssrem.trailingLineBackgroundColor", + "description": "Specifies the background color of the blame annotation for the current line", + "defaults": { + "dark": "#00000000", + "light": "#00000000", + "highContrast": "#00000000" + } + }, + { + "id": "extension.cssrem.trailingLineForegroundColor", + "description": "Specifies the foreground color of the blame annotation for the current line", + "defaults": { + "dark": "#99999970", + "light": "#99999970", + "highContrast": "#99999999" + } + } + ] }, "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.nls.json b/package.nls.json index b0d4ab8..d4a8ac5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -8,6 +8,9 @@ "cssrem.hover.disabled": "Disabled", "cssrem.hover.always": "Anything, whether converted by the plug-in or not, will be displayed", "cssrem.hover.onlyMark": "Only valid when `cssrem.addMark` is `true`", + "cssrem.currentLine": "Whether to display mark in after line, default: show", + "cssrem.currentLine.disabled": "Disabled", + "cssrem.currentLine.show": "Show", "cssrem.ingores": "Ignore file list, like this: `[ 'demo.less', 'src' ]`", "cssrem.languages": "Support language list, default: `[ 'html', 'vue', 'css', 'postcss', 'less', 'scss', 'sass', 'stylus', 'javascriptreact', 'typescriptreact' ]`", "cssrem.wxss": "Whether to enable WXSS support", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index c5e0db1..7021a60 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -8,6 +8,9 @@ "cssrem.hover.disabled": "禁用", "cssrem.hover.always": "任何,不管是否由插件转换而来的都显示", "cssrem.hover.onlyMark": "只对已经 `cssrem.addMark` 为 `true` 生效", + "cssrem.currentLine": "是否当前行尾显示标记, default: show", + "cssrem.currentLine.disabled": "禁用", + "cssrem.currentLine.show": "显示", "cssrem.ingores": "忽略文件清单,例如:`[ 'demo.less', 'src' ]`", "cssrem.languages": "支持语言清单,默认:`[ 'html', 'vue', 'css', 'postcss', 'less', 'scss', 'sass', 'stylus', 'javascriptreact', 'typescriptreact' ]`", "cssrem.wxss": "是否启用WXSS支持", diff --git a/src/extension.ts b/src/extension.ts index 6125692..fe918dc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import { commands, ExtensionContext, languages, workspace } from 'vscode'; import CssRemProvider from './completion'; import { cog, loadConfig } from './config'; import CssRemHoverProvider from './hover'; +import { LineAnnotation } from './line-annotation'; import { CssRemProcess } from './process'; let process: CssRemProcess; @@ -50,6 +51,7 @@ export function activate(context: ExtensionContext) { }), ); } + if (cog.currentLine !== 'disabled') context.subscriptions.push(new LineAnnotation()); } // this method is called when your extension is deactivated diff --git a/src/hover.ts b/src/hover.ts index 29fef93..f5b8fb2 100644 --- a/src/hover.ts +++ b/src/hover.ts @@ -6,7 +6,7 @@ export default class implements HoverProvider { private getText(line: string, pos: Position): string { const point = pos.character; let text = ''; - line.replace(/[.0-9]+(px|rem|rpx)/g, (a, b, idx) => { + line.replace(/[.0-9]+(px|rem|rpx)/g, (a, _, idx) => { const start = idx + 1; const end = idx + a.length + 1; if (!text && point >= start && point <= end) { diff --git a/src/interface.ts b/src/interface.ts index c5e6124..ccb7e41 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -15,6 +15,7 @@ export interface Config { * Whether to enable display conversion data on hover, Default: onlyMark */ hover: 'disabled' | 'always' | 'onlyMark'; + currentLine: 'disabled' | 'show'; /** * 忽略清单 */ @@ -67,4 +68,6 @@ export interface ConvertResult { export interface HoverResult { type: string; documentation: string; + from: string; + to: string; } diff --git a/src/line-annotation.ts b/src/line-annotation.ts new file mode 100644 index 0000000..8e6e932 --- /dev/null +++ b/src/line-annotation.ts @@ -0,0 +1,133 @@ +import { + DecorationOptions, + DecorationRangeBehavior, + Disposable, + Range, + Selection, + TextDocument, + TextEditor, + TextEditorSelectionChangeEvent, + ThemeColor, + window, +} from 'vscode'; +import { cog, isIngore } from './config'; +import { RULES } from './rules'; + +const annotationDecoration = window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 1.5em', + textDecoration: 'none', + }, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, +}); + +interface LineSelection { + anchor: number; + active: number; +} + +export class LineAnnotation implements Disposable { + protected _disposable?: Disposable; + private _editor?: TextEditor; + private _enabled = false; + + constructor() { + this._disposable = Disposable.from( + window.onDidChangeActiveTextEditor(this.onActiveTextEditor, this), + window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this), + ); + } + + private onActiveTextEditor(e: TextEditor) { + this._enabled = cog.languages.includes(e.document.languageId) && !isIngore(e.document.uri); + } + + private onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) { + if (!this._enabled) return; + + if (!this.isTextEditor(e.textEditor)) return; + + const selections = this.toLineSelections(e.selections); + if (selections.length === 0 || !selections.every(s => s.active === s.anchor)) { + this.clear(e.textEditor); + return; + } + + this.refresh(e.textEditor, selections[0]); + } + + private async refresh(editor: TextEditor | undefined, selection: LineSelection) { + if (editor.document == null || (editor == null && this._editor == null)) return; + + if (this._editor !== editor) { + this.clear(this._editor); + this._editor = editor; + } + + const l = selection.active; + const message = this.genMessage(editor.document, l); + if (message == null) { + this.clear(this._editor); + return; + } + + const decoration: DecorationOptions = { + renderOptions: { + after: { + backgroundColor: new ThemeColor('extension.cssrem.trailingLineBackgroundColor'), + color: new ThemeColor('extension.cssrem.trailingLineForegroundColor'), + contentText: message, + fontWeight: 'normal', + fontStyle: 'normal', + textDecoration: 'none', + }, + }, + range: editor.document.validateRange(new Range(l, Number.MAX_SAFE_INTEGER, l, Number.MAX_SAFE_INTEGER)), + }; + + editor.setDecorations(annotationDecoration, [decoration]); + } + + private genMessage(doc: TextDocument, lineNumber: number): string | null { + const text = doc.lineAt(lineNumber).text.trim(); + if (text.length <= 0) return null; + const values = text.match(/([.0-9]+(px|rem|rpx))/g); + if (values == null) return null; + + const results = values + .map(str => ({ text: str, rule: RULES.find(w => w.hover && w.hover.test(str)) })) + .filter(item => item.rule != null) + .map(item => item.rule.hoverFn(item.text)); + + if (results.length <= 0) return null; + if (results.length === 1) return results[0].to; + return results.map(res => `${res.to}->${res.from}`).join(', '); + } + + private clear(editor: TextEditor | undefined) { + if (this._editor !== editor && this._editor != null) { + this.clearAnnotations(this._editor); + } + this.clearAnnotations(editor); + } + + private clearAnnotations(editor: TextEditor | undefined) { + if (editor === undefined || (editor as any)._disposed === true) return; + + editor.setDecorations(annotationDecoration, []); + } + + private isTextEditor(editor: TextEditor): boolean { + const scheme = editor.document.uri.scheme; + return scheme !== 'output' && scheme !== 'debug'; + } + + dispose() { + this.clearAnnotations(this._editor); + this._disposable.dispose(); + } + + toLineSelections(selections: readonly Selection[] | undefined): LineSelection[] { + return selections?.map(s => ({ active: s.active.line, anchor: s.anchor.line })); + } +} diff --git a/src/rules.ts b/src/rules.ts index 1946d99..af5f7c0 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -47,9 +47,12 @@ export function resetRules(): void { }, hover: /([-]?[\d.]+)px/, hoverFn: pxText => { - const val = +(parseFloat(pxText) / cog.rootFontSize).toFixed(cog.fixedDigits); + const px = parseFloat(pxText); + const val = +(px / cog.rootFontSize).toFixed(cog.fixedDigits); return { type: 'remToPx', + from: `${px}px`, + to: `${val}rem`, documentation: localize( `pxToRem.documentation.hover`, 'Converted from `{0}rem` according to the benchmark font-size is `{1}px`', @@ -88,9 +91,12 @@ export function resetRules(): void { }, hover: /([-]?[\d.]+)rem/, hoverFn: remText => { - const val = +(parseFloat(remText) * cog.rootFontSize).toFixed(cog.fixedDigits); + const rem = parseFloat(remText); + const val = +(rem * cog.rootFontSize).toFixed(cog.fixedDigits); return { type: 'pxToRem', + from: `${rem}rem`, + to: `${val}px`, documentation: localize( `remToPx.documentation.hover`, 'Converted from `{0}px` according to the benchmark font-size is `{1}px`, please refer to [Configuration Document](https://github.com/cipchk/vscode-cssrem#configuration) modify', @@ -171,9 +177,12 @@ export function resetRules(): void { }, hover: /([-]?[\d.]+)rpx/, hoverFn: rpxText => { - const val = +(parseFloat(rpxText) / (cog.wxssScreenWidth / cog.wxssDeviceWidth)).toFixed(cog.fixedDigits); + const rpx = parseFloat(rpxText); + const val = +(rpx / (cog.wxssScreenWidth / cog.wxssDeviceWidth)).toFixed(cog.fixedDigits); return { type: 'rpxToPx', + from: `${rpx}rpx`, + to: `${val}px`, documentation: localize( `rpxToPx.documentation.hover`, '**WXSS miniprogram style** Converted from `{0}px` (The current device width is `{1}px` and screen width is `{2}px`)',