Skip to content

Commit

Permalink
color provider + css diagnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Nov 12, 2024
1 parent c9dfd52 commit 7940dd8
Show file tree
Hide file tree
Showing 21 changed files with 192 additions and 65 deletions.
4 changes: 4 additions & 0 deletions crates/zed-plugin-gem/extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ schema_version = 1
authors = ["mantou132 <[email protected]>"]
repository = "https://github.com/mantou132/gem"
snippets = "./snippets/typescript.json"

[grammars.typescript]
repository = "https://github.com/tree-sitter/tree-sitter-typescript"
commit = "f975a621f4e7f532fe322e13c4f79495e0a7b2e7"
9 changes: 5 additions & 4 deletions packages/duoyun-ui/src/elements/file-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,23 +214,24 @@ export class DuoyunFilePickerElement extends GemElement implements BasePickerEle

return html`
<input
${this.#inputRef}
${this.#inputRef}
hidden
type="file"
?multiple=${this.multiple}
?disabled=${this.disabled}
@change=${this.#onChange}
.webkitdirectory=${this.directory}
accept=${this.#accept}>
</input>
accept=${this.#accept}
/>
<div
role="button"
tabindex=${-Number(this.disabled)}
aria-disabled=${this.disabled}
part=${DuoyunFilePickerElement.button}
class="item button"
@keydown=${commonHandle}
@click=${() => this.showPicker()}>
@click=${() => this.showPicker()}
>
<dy-use class="icon" .element=${icons.add}></dy-use>
<span class="name"><slot>${this.placeholder || 'Browse'}</slot></span>
</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/duoyun-ui/src/elements/image-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ const style = css`
}
.progress {
opacity: 0.4;
background: linear-gradient(to top, ${elementTheme.color} ${elementTheme.progress}, transparent ${elementTheme.progress});,
background: linear-gradient(
to top,
${elementTheme.color} ${elementTheme.progress},
transparent ${elementTheme.progress}
);
}
.icon {
width: 1.5em;
Expand Down
2 changes: 1 addition & 1 deletion packages/duoyun-ui/src/elements/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const style = css`
inline-size: 100%;
padding-inline: 0.5em;
border: none;
border-radio: inherit;
border-radius: inherit;
background-clip: text;
resize: none;
field-sizing: inherit;
Expand Down
2 changes: 1 addition & 1 deletion packages/duoyun-ui/src/elements/sort-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const style = css`
cursor: grabbing;
* {
pointer-event: none;
pointer-events: none;
}
}
dy-sort-item[handle]:state(grabbing),
Expand Down
2 changes: 1 addition & 1 deletion packages/gem/docs/en/004-blog/007-v2-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class MyElement extends GemElement {
#input = createRef();

render() {
return html`<input ${this.#input}></input>`;
return html`<input ${this.#input} />`;
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion packages/gem/docs/zh/004-blog/007-v2-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class MyElement extends GemElement {
#input = createRef();

render() {
return html`<input ${this.#input}></input>`;
return html`<input ${this.#input} />`;
}
}
```
Expand Down
Empty file modified packages/language-service/src/index.ts
100644 → 100755
Empty file.
3 changes: 2 additions & 1 deletion packages/vscode-gem-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"displayName": "Gem",
"description": "Gem plugin for VS Code",
"icon": "logo.png",
"version": "1.0.0",
"version": "1.0.1",
"engines": {
"vscode": "^1.94.0"
},
Expand Down Expand Up @@ -171,6 +171,7 @@
"@types/vscode": "^1.94.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"duoyun-ui": "^2.2.0",
"esbuild": "^0.24.0",
"typescript": "^5.6.2"
},
Expand Down
11 changes: 9 additions & 2 deletions packages/vscode-gem-plugin/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export const CSS_REG = /(?<start>\/\*\s*css\s*\*\/\s*`|(?<!`)(?:css|less|scss)\s*`)(?<content>.*?)(`(?=;|\s))/gis;
export const HTML_REG = /(?<start>\/\*\s*html\s*\*\/\s*`|(?<!`)(?:html|raw)\s*`)(?<content>[^`]*)(`)/gi;
export const COLOR_REG = /(?<start>'|")?(?<content>#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4}))($1|\s*;)/gi;

// 直接通过正则匹配 css 片段,通过条件的结束 ` 号匹配
export const CSS_REG = /(?<start>\/\*\s*css\s*\*\/\s*`|(?<!`)(?:css|less|scss)\s*`)(?<content>.*?)(`(?=;|,?\s*\)))/gis;
// 直接通过正则匹配 style 片段,通过条件的结束 ` 号匹配
// 语言服务和高亮都只支持 styled 写法
export const STYLE_REG = /(?<start>\/\*\s*style\s*\*\/\s*`|(?<!`)styled?\s*`)(?<content>.*?)(`(?=,|\s*}\s*\)))/gis;

// 处理后进行正则匹配,所以不需要验证后面的 ` 号
export const HTML_REG = /(?<start>\/\*\s*html\s*\*\/\s*`|(?<!`)(?:html|raw)\s*`)(?<content>[^`]*)(`)/gi;
20 changes: 8 additions & 12 deletions packages/vscode-gem-plugin/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const decorationType = window.createTextEditorDecorationType({ opacity: '1 !impo
const regEx = /(?<=^\s*@\w+\([^@\n]*\)\s+)(#\w+)/gm;

let activeEditor = window.activeTextEditor;
let timeout: NodeJS.Timeout;

function updateDecorations() {
if (!activeEditor || !activeEditor.document) {
Expand All @@ -25,34 +24,31 @@ function updateDecorations() {
activeEditor.setDecorations(decorationType, decorations);
}

let timeout: NodeJS.Timeout;
function triggerUpdateDecorations() {
clearTimeout(timeout);
timeout = setTimeout(updateDecorations, 0);
timeout = setTimeout(updateDecorations, 60);
}

if (activeEditor) {
triggerUpdateDecorations();
}

export function markDecorators(context: ExtensionContext) {
window.onDidChangeActiveTextEditor(
function (editor) {
context.subscriptions.push(
window.onDidChangeActiveTextEditor((editor) => {
activeEditor = editor;
if (editor) {
triggerUpdateDecorations();
}
},
null,
context.subscriptions,
}),
);

workspace.onDidChangeTextDocument(
function (event) {
context.subscriptions.push(
workspace.onDidChangeTextDocument((event) => {
if (activeEditor && event.document === activeEditor.document) {
triggerUpdateDecorations();
}
},
null,
context.subscriptions,
}),
);
}
66 changes: 66 additions & 0 deletions packages/vscode-gem-plugin/src/diagnostic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 只对 CSS 语法和属性做了简单的检查,不做值检查
// TODO: 激活扩展、打开工作区时需要自动诊断所有文件
// TODO: 使用 LRU 缓存

// eslint-disable-next-line import/no-unresolved
import { workspace, languages, window, Range, Diagnostic } from 'vscode';
import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice';
import type { ExtensionContext, TextDocument } from 'vscode';

import { CSS_REG, STYLE_REG } from './constants';
import { createVirtualDocument, removeSlot } from './util';

const diagnosticCollection = languages.createDiagnosticCollection('gem');
const cssLanguageService = getCSSLanguageService();

const updateDiagnostic = (document: TextDocument) => {
const diagnostics: Diagnostic[] = [];
const text = document.getText();

const matchFragments = (regexp: RegExp, appendBefore: string, appendAfter: string) => {
regexp.exec('null');

let match;
while ((match = regexp.exec(text))) {
const matchContent = match.groups!.content;
const offset = match.index + match.groups!.start.length;
const virtualDocument = createVirtualDocument('css', `${appendBefore}${removeSlot(matchContent)}${appendAfter}`);
const vCss = cssLanguageService.parseStylesheet(virtualDocument);
const oDiagnostics = cssLanguageService.doValidation(virtualDocument, vCss) as Diagnostic[];
for (const { message, range } of oDiagnostics) {
const { start, end } = range;
const startOffset = virtualDocument.offsetAt(start) - appendBefore.length + offset;
const endOffset = virtualDocument.offsetAt(end) - appendBefore.length + offset;
const nRange = new Range(document.positionAt(startOffset), document.positionAt(endOffset));
diagnostics.push(new Diagnostic(nRange, message));
}
}
};

matchFragments(CSS_REG, '', '');
matchFragments(STYLE_REG, ':host { ', ' }');

diagnosticCollection.set(document.uri, diagnostics);
};

export function markDiagnostic(context: ExtensionContext) {
context.subscriptions.push(diagnosticCollection);

context.subscriptions.push(
window.onDidChangeActiveTextEditor((editor) => {
if (editor) {
updateDiagnostic(editor.document);
}
}),
);

context.subscriptions.push(
workspace.onDidChangeTextDocument(({ document }) => {
updateDiagnostic(document);
}),
);

if (window.activeTextEditor) {
updateDiagnostic(window.activeTextEditor.document);
}
}
40 changes: 25 additions & 15 deletions packages/vscode-gem-plugin/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { LanguageClient, TransportKind } from 'vscode-languageclient/node';
import { HTMLCompletionItemProvider } from './providers/html';
import { CSSCompletionItemProvider, HTMLStyleCompletionItemProvider } from './providers/css';
import { StyleCompletionItemProvider } from './providers/style';
import { ColorProvider } from './providers/color';
import { HTMLHoverProvider, CSSHoverProvider, StyleHoverProvider } from './providers/hover';
import { markDecorators } from './decorators';
import { markDiagnostic } from './diagnostic';

const selector = ['typescriptreact', 'javascriptreact', 'typescript', 'javascript'];
const triggers = ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
Expand All @@ -36,24 +38,32 @@ function useLS(context: ExtensionContext) {
client.start();
}

function useBasic(context: ExtensionContext) {
export function activate(context: ExtensionContext) {
markDecorators(context);
languages.registerHoverProvider(selector, new HTMLHoverProvider());
languages.registerHoverProvider(selector, new StyleHoverProvider());
languages.registerHoverProvider(selector, new CSSHoverProvider());
languages.registerCompletionItemProvider(selector, new HTMLCompletionItemProvider(), '<', ...triggers);
languages.registerCompletionItemProvider(selector, new HTMLStyleCompletionItemProvider(), ...triggers);
languages.registerCompletionItemProvider(selector, new CSSCompletionItemProvider(), ...triggers);
languages.registerCompletionItemProvider(selector, new StyleCompletionItemProvider(), ...triggers);
}
markDiagnostic(context);

export function activate(context: ExtensionContext) {
useBasic(context);
context.subscriptions.push(languages.registerColorProvider(selector, new ColorProvider()));
context.subscriptions.push(languages.registerHoverProvider(selector, new HTMLHoverProvider()));
context.subscriptions.push(languages.registerHoverProvider(selector, new StyleHoverProvider()));
context.subscriptions.push(languages.registerHoverProvider(selector, new CSSHoverProvider()));
context.subscriptions.push(
languages.registerCompletionItemProvider(selector, new HTMLCompletionItemProvider(), '<', ...triggers),
);
context.subscriptions.push(
languages.registerCompletionItemProvider(selector, new HTMLStyleCompletionItemProvider(), ...triggers),
);
context.subscriptions.push(
languages.registerCompletionItemProvider(selector, new CSSCompletionItemProvider(), ...triggers),
);
context.subscriptions.push(
languages.registerCompletionItemProvider(selector, new StyleCompletionItemProvider(), ...triggers),
);

const disposable = commands.registerCommand('vscode-plugin-gem.helloWorld', () => {
window.showInformationMessage('Hello World from vscode-plugin-gem!');
});
context.subscriptions.push(disposable);
context.subscriptions.push(
commands.registerCommand('vscode-plugin-gem.helloWorld', () => {
window.showInformationMessage('Hello World from vscode-plugin-gem!');
}),
);

// TODO: 扩展配置
const enabledLS = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Code from https://github.com/Microsoft/typescript-styled-plugin/blob/master/src/styled-template-language-service.ts

import type { CompletionList, TextDocument, Position } from 'vscode';

export class CompletionsCache {
Expand All @@ -8,16 +6,16 @@ export class CompletionsCache {
#cachedCompletionsContent?: string;
#completions?: CompletionList;

#equalPositions(left: Position, right?: Position): boolean {
#equalPositions(left: Position, right?: Position) {
return !!right && left.line === right.line && left.character === right.character;
}

getCached(context: TextDocument, position: Position): CompletionList | undefined {
getCached(doc: TextDocument, position: Position) {
if (
this.#completions &&
context.fileName === this.#cachedCompletionsFile &&
doc.fileName === this.#cachedCompletionsFile &&
this.#equalPositions(position, this.#cachedCompletionsPosition) &&
context.getText() === this.#cachedCompletionsContent
doc.getText() === this.#cachedCompletionsContent
) {
return this.#completions;
}
Expand Down
30 changes: 30 additions & 0 deletions packages/vscode-gem-plugin/src/providers/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// eslint-disable-next-line import/no-unresolved
import { ColorPresentation, ColorInformation, Range, Color } from 'vscode';
import { rgbToHexColor, parseHexColor } from 'duoyun-ui/lib/color';
import type { HexColor } from 'duoyun-ui/lib/color';
import type { DocumentColorProvider, TextDocument } from 'vscode';

import { COLOR_REG } from '../constants';

export class ColorProvider implements DocumentColorProvider {
provideDocumentColors(document: TextDocument) {
COLOR_REG.exec('null');

const documentText = document.getText();
const colors: ColorInformation[] = [];

let match: RegExpExecArray | null;
while ((match = COLOR_REG.exec(documentText)) !== null) {
const hex = match.groups!.content as HexColor;
const [red, green, blue, alpha] = parseHexColor(hex);
const offset = match.index + (match.groups!.start?.length || 0);
const range = new Range(document.positionAt(offset), document.positionAt(offset + hex.length));
colors.push(new ColorInformation(range, new Color(red / 255, green / 255, blue / 255, alpha)));
}
return colors;
}

provideColorPresentations({ red, green, blue, alpha }: Color) {
return [new ColorPresentation(rgbToHexColor([red * 255, green * 255, blue * 255, alpha]))];
}
}
10 changes: 5 additions & 5 deletions packages/vscode-gem-plugin/src/providers/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import type {
CompletionItemProvider,
} from 'vscode';
import type { LanguageService as HTMLanguageService } from 'vscode-html-languageservice';
import type { LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
import { getLanguageService as getHTMLanguageService, TokenType as HTMLTokenType } from 'vscode-html-languageservice';
import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice';

import { matchOffset, createVirtualDocument, translateCompletionList, removeSlot } from '../util';
import { CompletionsCache } from '../cache';
import { CSS_REG, HTML_REG } from '../constants';

import { CompletionsCache } from './cache';

export function getRegionAtOffset(regions: IEmbeddedRegion[], offset: number) {
for (const region of regions) {
if (region.start <= offset) {
Expand Down Expand Up @@ -61,8 +61,8 @@ export function getLanguageRegions(service: HTMLanguageService, data: string) {
}

export class HTMLStyleCompletionItemProvider implements CompletionItemProvider {
#cssLanguageService: CSSLanguageService = getCSSLanguageService();
#htmlLanguageService: HTMLanguageService = getHTMLanguageService();
#cssLanguageService = getCSSLanguageService();
#htmlLanguageService = getHTMLanguageService();
#cache = new CompletionsCache();

provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken) {
Expand Down Expand Up @@ -109,7 +109,7 @@ export class HTMLStyleCompletionItemProvider implements CompletionItemProvider {
}

export class CSSCompletionItemProvider implements CompletionItemProvider {
#cssLanguageService: CSSLanguageService = getCSSLanguageService();
#cssLanguageService = getCSSLanguageService();
#cache = new CompletionsCache();

provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken) {
Expand Down
Loading

0 comments on commit 7940dd8

Please sign in to comment.