commit cec2620c5f136913ab3b3af1a81025a3b23ba339 Author: yutent Date: Fri Mar 3 12:07:45 2023 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eb14b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/*.vsix +**/*.js.map +node_modules +client +.vscode +.DS_STORE diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..b007fb1 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,10 @@ +jsxBracketSameLine: true +jsxSingleQuote: true +semi: false +singleQuote: true +printWidth: 80 +useTabs: false +tabWidth: 2 +trailingComma: none +bracketSpacing: true +arrowParens: avoid \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..3fdf4b1 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,7 @@ +**/*.ts +**/tsconfig.json +server/** +tests/** +.vscode/** +.gitignore +.git \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee12962 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## v1.0.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6af59d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Ahmed Tarek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..95f6381 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## string-html-css +一个高亮js代码中的 html/css/scss/sass/less的字符串, 并支持emmet. \ No newline at end of file diff --git a/assets/demo.gif b/assets/demo.gif new file mode 100644 index 0000000..32237c7 Binary files /dev/null and b/assets/demo.gif differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..fd6d756 Binary files /dev/null and b/assets/logo.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..022e2a6 --- /dev/null +++ b/package.json @@ -0,0 +1,127 @@ +{ + "name": "string-html-css", + "displayName": "string-html-css", + "description": "一个高亮js代码中的 html/css/scss/sass/less的字符串, 并支持emmet.", + "version": "1.0.0", + "publisher": "yutent", + "license": "MIT", + "icon": "docs/logo.png", + "bugs": { + "url": "https://github.com/yutent/vscode-string-html/issues" + }, + "keywords": [ + "html", + "css", + "template", + "polymer", + "lit-html" + ], + "repository": { + "type": "git", + "url": "https://github.com/yutent/vscode-string-html" + }, + "engines": { + "vscode": "^1.22.0" + }, + "scripts": { + "compile": "npx tsc -p .", + "watch:compile": "npx tsc --watch -p .", + "package": "npx vsce package" + }, + "categories": [ + "Programming Languages" + ], + "activationEvents": [ + "onLanguage:javascript", + "onLanguage:typescript", + "onLanguage:javascriptreact", + "onLanguage:typescriptreact" + ], + "main": "./dist/main.js", + "contributes": { + "commands": [ + { + "command": "editor.action.formatInlineHtml", + "title": "Format Inline HTML/CSS" + } + ], + "grammars": [ + { + "injectTo": [ + "source.js", + "source.js.jsx", + "source.jsx", + "source.ts", + "source.ts.tsx", + "source.tsx" + ], + "scopeName": "es6.inline.html", + "path": "./syntaxes/es6.inline.html.json", + "embeddedLanguages": { + "meta.embedded.block.html": "html", + "meta.template.expression.ts": "typescript" + } + }, + { + "injectTo": [ + "source.js", + "source.js.jsx", + "source.jsx", + "source.ts", + "source.ts.tsx", + "source.tsx" + ], + "scopeName": "es6.inline.css", + "path": "./syntaxes/es6.inline.css.json", + "embeddedLanguages": { + "meta.embedded.block.css": "css", + "meta.template.expression.ts": "typescript" + } + }, + { + "injectTo": [ + "source.js", + "source.js.jsx", + "source.jsx", + "source.ts", + "source.ts.tsx", + "source.tsx" + ], + "scopeName": "es6.inline.scss", + "path": "./syntaxes/es6.inline.scss.json", + "embeddedLanguages": { + "meta.embedded.block.css": "scss", + "meta.template.expression.ts": "typescript" + } + }, + { + "injectTo": [ + "source.js", + "source.js.jsx", + "source.jsx", + "source.ts", + "source.ts.tsx", + "source.tsx" + ], + "scopeName": "es6.inline.less", + "path": "./syntaxes/es6.inline.less.json", + "embeddedLanguages": { + "meta.embedded.block.css": "less", + "meta.template.expression.ts": "typescript" + } + } + ] + }, + "devDependencies": { + "@types/node": "7.0.43", + "@types/vscode": "^1.22.0", + "typescript": "^3.2.2", + "vsce": "^1.102.0", + "vscode-languageserver-types": "^3.6.0" + }, + "dependencies": { + "vscode-css-languageservice": "^3.0.7", + "vscode-emmet-helper": "^1.2.0", + "vscode-html-languageservice": "^2.1.1" + } +} diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..bc88983 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,42 @@ +// Code from https://github.com/Microsoft/typescript-styled-plugin/blob/master/src/styled-template-language-service.ts + +import { CompletionList, TextDocument, Position } from 'vscode' + +export class CompletionsCache { + private _cachedCompletionsFile?: string + private _cachedCompletionsPosition?: Position + private _cachedCompletionsContent?: string + private _completions?: CompletionList + + private equalPositions(left: Position, right: Position): boolean { + return left.line === right.line && left.character === right.character + } + + public getCached( + context: TextDocument, + position: Position + ): CompletionList | undefined { + if ( + this._completions && + context.fileName === this._cachedCompletionsFile && + this._cachedCompletionsPosition && + this.equalPositions(position, this._cachedCompletionsPosition) && + context.getText() === this._cachedCompletionsContent + ) { + return this._completions + } + + return undefined + } + + public updateCached( + context: TextDocument, + position: Position, + completions: CompletionList + ) { + this._cachedCompletionsFile = context.fileName + this._cachedCompletionsPosition = position + this._cachedCompletionsContent = context.getText() + this._completions = completions + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..8cd81d3 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,91 @@ +import { + languages as Languages, + ExtensionContext, + commands as Commands, + DocumentSelector +} from 'vscode' +import { HTMLCompletionItemProvider } from './providers/html' +import { + CSSCompletionItemProvider, + HTMLStyleCompletionItemProvider +} from './providers/css' +import { HTMLHoverProvider, CSSHoverProvider } from './providers/hover' +import { CodeFormatterProvider } from './providers/formatting' + +const selector: DocumentSelector = [ + 'typescriptreact', + 'javascriptreact', + 'typescript', + 'javascript' +] + +export function activate(Context: ExtensionContext) { + new CodeFormatterProvider() + + Languages.registerCompletionItemProvider( + selector, + new HTMLCompletionItemProvider(), + '<', + '!', + '.', + '}', + ':', + '*', + '$', + ']', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9' + ) + Languages.registerHoverProvider(selector, new HTMLHoverProvider()) + Languages.registerCompletionItemProvider( + selector, + new HTMLStyleCompletionItemProvider(), + '!', + '.', + '}', + ':', + '*', + '$', + ']', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9' + ) + Languages.registerHoverProvider(selector, new CSSHoverProvider()) + Languages.registerCompletionItemProvider( + selector, + new CSSCompletionItemProvider(), + '!', + '.', + '}', + ':', + '*', + '$', + ']', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9' + ) +} diff --git a/src/providers/css.ts b/src/providers/css.ts new file mode 100644 index 0000000..f1b329e --- /dev/null +++ b/src/providers/css.ts @@ -0,0 +1,217 @@ +import { + CompletionList, + CompletionItem, + TextDocument, + Position, + CancellationToken, + CompletionItemProvider +} from 'vscode' + +import { + getLanguageService as GetHTMLanguageService, + LanguageService as HTMLanguageService, + CompletionList as HTMLCompletionList +} from 'vscode-html-languageservice' + +import { + getCSSLanguageService as GetCSSLanguageService, + LanguageService as CSSLanguageService, + CompletionList as CSSCompletionList +} from 'vscode-css-languageservice' + +import * as emmet from 'vscode-emmet-helper' +import { + GetEmmetConfiguration, + MatchOffset, + CreateVirtualDocument, + GetLanguageRegions, + GetRegionAtOffset, + TranslateCompletionItems +} from '../util' + +import { CompletionsCache } from '../cache' + +export class HTMLStyleCompletionItemProvider implements CompletionItemProvider { + private _cssLanguageService: CSSLanguageService = GetCSSLanguageService() + private _HTMLanguageService: HTMLanguageService = GetHTMLanguageService() + private _expression = /(\/\*\s*html\s*\*\/\s*`|html\s*`)([^`]*)(`)/g + private _cache = new CompletionsCache() + + public provideCompletionItems( + document: TextDocument, + position: Position, + _token: CancellationToken + ): CompletionList { + const cached = this._cache.getCached(document, position) + + if (cached) { + return cached + } + + const currentLine = document.lineAt(position.line) + const empty = { + isIncomplete: false, + items: [] + } as CompletionList + + if (currentLine.isEmptyOrWhitespace) { + return empty + } + + const currentLineText = currentLine.text.trim() + const currentOffset = document.offsetAt(position) + const documentText = document.getText() + const match = MatchOffset(this._expression, documentText, currentOffset) + + if (!match) { + return empty + } + + // tslint:disable-next-line:no-magic-numbers + const matchContent: string = match[2] + const matchStartOffset = match.index + match[1].length + const matchEndOffset = match.index + match[0].length + const regions = GetLanguageRegions(this._HTMLanguageService, matchContent) + + if (regions.length <= 0) { + return empty + } + + const region = GetRegionAtOffset(regions, currentOffset - matchStartOffset) + + if (!region) { + return empty + } + + const virtualOffset = currentOffset - (matchStartOffset + region.start) + const virtualDocument = CreateVirtualDocument('css', region.content) + + const stylesheet = this._cssLanguageService.parseStylesheet(virtualDocument) + const emmetResults: HTMLCompletionList = { + isIncomplete: true, + items: [] + } + + this._cssLanguageService.setCompletionParticipants([ + emmet.getEmmetCompletionParticipants( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + 'css', + GetEmmetConfiguration(), + emmetResults + ) + ]) + + const completions = this._cssLanguageService.doComplete( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + stylesheet + ) + + if (emmetResults.items.length) { + completions.items.push(...emmetResults.items) + completions.isIncomplete = true + } + + this._cache.updateCached(document, position, completions as CompletionList) + + return { + isIncomplete: completions.isIncomplete, + items: TranslateCompletionItems(completions.items, currentLine) + } as CompletionList + } + + public resolveCompletionItem?( + item: CompletionItem, + _token: CancellationToken + ): CompletionItem | Thenable { + return item + } +} + +export class CSSCompletionItemProvider implements CompletionItemProvider { + private _CSSLanguageService: CSSLanguageService = GetCSSLanguageService() + private _expression = /(\/\*\s*(css|less|scss)\s*\*\/\s*`|css\s*`)([^`]*)(`)/g + private _cache = new CompletionsCache() + + public provideCompletionItems( + document: TextDocument, + position: Position, + _token: CancellationToken + ): CompletionList { + const cached = this._cache.getCached(document, position) + + if (cached) { + return cached + } + + const currentLine = document.lineAt(position.line) + const empty = { + isIncomplete: false, + items: [] + } as CompletionList + + if (currentLine.isEmptyOrWhitespace) { + return empty + } + + const currentLineText = currentLine.text.trim() + const currentOffset = document.offsetAt(position) + const documentText = document.getText() + const match = MatchOffset(this._expression, documentText, currentOffset) + + if (!match) { + return empty + } + + const dialect = match[2] + + // tslint:disable-next-line:no-magic-numbers + const matchContent: string = match[3] + const matchStartOffset = match.index + match[1].length + const matchEndOffset = match.index + match[0].length + const matchPosition = document.positionAt(matchStartOffset) + const virtualOffset = currentOffset - matchStartOffset + const virtualDocument = CreateVirtualDocument(dialect, matchContent) + const vCss = this._CSSLanguageService.parseStylesheet(virtualDocument) + const emmetResults: CSSCompletionList = { + isIncomplete: true, + items: [] + } + + this._CSSLanguageService.setCompletionParticipants([ + emmet.getEmmetCompletionParticipants( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + dialect, + GetEmmetConfiguration(), + emmetResults + ) + ]) + + const completions = this._CSSLanguageService.doComplete( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + vCss + ) + + if (emmetResults.items.length) { + completions.items.push(...emmetResults.items) + completions.isIncomplete = true + } + + this._cache.updateCached(document, position, completions as CompletionList) + + return { + isIncomplete: completions.isIncomplete, + items: TranslateCompletionItems(completions.items, currentLine) + } as CompletionList + } + + public resolveCompletionItem?( + item: CompletionItem, + _token: CancellationToken + ): CompletionItem | Thenable { + return item + } +} diff --git a/src/providers/formatting.ts b/src/providers/formatting.ts new file mode 100644 index 0000000..5288b60 --- /dev/null +++ b/src/providers/formatting.ts @@ -0,0 +1,81 @@ +import { + Range, + TextDocument, + WorkspaceEdit, + workspace as Workspace, + TextEditor, + commands as Commands, + Uri, + TextEdit, + Position +} from 'vscode' + +import { + getLanguageService as GetHTMLanguageService, + TextDocument as HTMLTextDocument, + Position as HTMLPosition +} from 'vscode-html-languageservice' + +import { + CreateVirtualDocument, + TranslateHTMLTextEdits, + Match, + GetLanguageRegions, + IEmbeddedRegion +} from '../util' + +export class CodeFormatterProvider { + private _expression = /(\/\*\s*html\s*\*\/\s*`|html\s*`)([^`]*)(`)/g + private document: TextDocument + + constructor() { + Commands.registerTextEditorCommand( + 'editor.action.formatInlineHtml', + this.format, + this + ) + } + + public format(textEditor: TextEditor) { + this.document = textEditor.document + + var documentText = this.document.getText() + var match = Match(this._expression, documentText) + + if (!match) { + return [] + } + + // TODO - Refactor, This have been used multiple times thourgh out the + // TODO - extension. + var matchStartOffset = match.index + match[1].length + var matchEndOffset = match.index + (match[2].length + match[3].length + 1) + var matchStartPosition = this.document.positionAt(matchStartOffset) + var matchEndPosition = this.document.positionAt(matchEndOffset) + + var text = this.document.getText( + new Range(matchStartPosition, matchEndPosition) + ) + var vHTML = CreateVirtualDocument('html', text) + + // TODO - Expose Formatting Options + const edits = TranslateHTMLTextEdits( + GetHTMLanguageService().format(vHTML, null, { + indentInnerHtml: false, + preserveNewLines: true, + tabSize: textEditor.options.tabSize, + insertSpaces: textEditor.options.insertSpaces, + endWithNewline: true + }), + matchStartPosition.line + 1 + ) + + Workspace.applyEdit(this.composeEdits(this.document.uri, edits)) + } + + private composeEdits(uri: Uri, edits: TextEdit[]): WorkspaceEdit { + var ws = new WorkspaceEdit() + ws.set(uri, edits) + return ws + } +} diff --git a/src/providers/hover.ts b/src/providers/hover.ts new file mode 100644 index 0000000..06d0b52 --- /dev/null +++ b/src/providers/hover.ts @@ -0,0 +1,98 @@ +import { + HoverProvider, + TextDocument, + Position, + CancellationToken, + Hover +} from 'vscode' + +import { + getLanguageService as GetHtmlLanguageService, + LanguageService as HtmlLanguageService, + CompletionList as HtmlCompletionList +} from 'vscode-html-languageservice' + +import { + getCSSLanguageService as GetCssLanguageService, + LanguageService as CssLanguageService +} from 'vscode-css-languageservice' + +import { CreateVirtualDocument, MatchOffset } from '../util' + +export class HTMLHoverProvider implements HoverProvider { + private _htmlLanguageService: HtmlLanguageService = GetHtmlLanguageService() + private _cssLanguageService: CssLanguageService = GetCssLanguageService() + // private _expression = /(html\s*`)([^`]*)(`)/g + private _expression = /(\/\*\s*html\s*\*\/\s*`|html\s*`)([^`]*)(`)/g + + provideHover( + document: TextDocument, + position: Position, + token: CancellationToken + ): Hover { + const currentOffset = document.offsetAt(position) + const documentText = document.getText() + const match = MatchOffset(this._expression, documentText, currentOffset) + + if (!match) { + return null + } + + // tslint:disable-next-line:no-magic-numbers + const matchContent: string = match[2] + const matchStartOffset = match.index + match[1].length + const virtualOffset = currentOffset - matchStartOffset + const virtualDocument = CreateVirtualDocument('html', matchContent) + const html = this._htmlLanguageService.parseHTMLDocument(virtualDocument) + const stylesheet = this._cssLanguageService.parseStylesheet(virtualDocument) + const hover = + this._htmlLanguageService.doHover( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + html + ) || + this._cssLanguageService.doHover( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + stylesheet + ) + + return hover as Hover + } +} + +export class CSSHoverProvider implements HoverProvider { + private _htmlLanguageService: HtmlLanguageService = GetHtmlLanguageService() + private _cssLanguageService: CssLanguageService = GetCssLanguageService() + private _expression = /(\/\*\s*(css|less|scss)\s*\*\/\s*`|css\s*`)([^`]*)(`)/g + + provideHover( + document: TextDocument, + position: Position, + token: CancellationToken + ): Hover { + const currentOffset = document.offsetAt(position) + const documentText = document.getText() + const match = MatchOffset(this._expression, documentText, currentOffset) + + if (!match) { + return null + } + + const dialect = match[2] + + // tslint:disable-next-line:no-magic-numbers + const matchContent: string = match[3] + const matchStartOffset = match.index + match[1].length + const virtualOffset = currentOffset - matchStartOffset + const virtualDocument = CreateVirtualDocument(dialect, matchContent) + const stylesheet = this._cssLanguageService.parseStylesheet(virtualDocument) + const hover = this._cssLanguageService.doHover( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + stylesheet + ) + + return hover as Hover + } +} diff --git a/src/providers/html.ts b/src/providers/html.ts new file mode 100644 index 0000000..b7d781c --- /dev/null +++ b/src/providers/html.ts @@ -0,0 +1,111 @@ +import { + CompletionList, + CompletionItem, + TextDocument, + Position, + CancellationToken, + CompletionItemProvider +} from 'vscode' + +import { + getLanguageService as GetHTMLanguageService, + LanguageService as HTMLanguageService, + CompletionList as HTMLCompletionList +} from 'vscode-html-languageservice' + +import * as emmet from 'vscode-emmet-helper' + +import { + GetEmmetConfiguration, + MatchOffset, + CreateVirtualDocument, + TranslateCompletionItems +} from '../util' + +import { CompletionsCache } from '../cache' + +export class HTMLCompletionItemProvider implements CompletionItemProvider { + private _htmlLanguageService: HTMLanguageService = GetHTMLanguageService() + private _expression = /(\/\*\s*html\s*\*\/\s*`|html\s*`)([^`]*)(`)/g + // private _expression = /(html\s*`)([^`]*)(`)/g + private _cache = new CompletionsCache() + + public provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken + ): CompletionList { + const cached = this._cache.getCached(document, position) + + if (cached) { + return cached + } + + const currentLine = document.lineAt(position.line) + const empty = { + isIncomplete: false, + items: [] + } as CompletionList + + if (currentLine.isEmptyOrWhitespace) { + return empty + } + + const currentLineText = currentLine.text.trim() + const currentOffset = document.offsetAt(position) + const documentText = document.getText() + const match = MatchOffset(this._expression, documentText, currentOffset) + + if (!match) { + return empty + } + + // tslint:disable-next-line:no-magic-numbers + const matchContent: string = match[2] + const matchStartOffset = match.index + match[1].length + const matchEndOffset = match.index + match[0].length + const matchPosition = document.positionAt(matchStartOffset) + const virtualOffset = currentOffset - matchStartOffset + const virtualDocument = CreateVirtualDocument('html', matchContent) + const vHtml = this._htmlLanguageService.parseHTMLDocument(virtualDocument) + const emmetResults: HTMLCompletionList = { + isIncomplete: true, + items: [] + } + + this._htmlLanguageService.setCompletionParticipants([ + emmet.getEmmetCompletionParticipants( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + 'html', + GetEmmetConfiguration(), + emmetResults + ) + ]) + + const completions = this._htmlLanguageService.doComplete( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + vHtml + ) + + if (emmetResults.items.length) { + completions.items.push(...emmetResults.items) + completions.isIncomplete = true + } + + this._cache.updateCached(document, position, completions as CompletionList) + + return { + isIncomplete: completions.isIncomplete, + items: TranslateCompletionItems(completions.items, currentLine, true) + } as CompletionList + } + + public resolveCompletionItem?( + item: CompletionItem, + token: CancellationToken + ): CompletionItem | Thenable { + return item + } +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..109cfc6 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,176 @@ +import { + workspace, + TextLine, + TextEdit, + Position, + Range, + CompletionItem, + Command +} from 'vscode' + +import { + TextDocument as HTMLTextDocument, + LanguageService, + TokenType as HTMLTokenType, + TextEdit as HTMLTextEdit +} from 'vscode-html-languageservice' + +import { EmmetConfiguration } from 'vscode-emmet-helper' + +export function GetEmmetConfiguration(): EmmetConfiguration { + const emmetConfig = workspace.getConfiguration('emmet') + return { + useNewEmmet: true, + showExpandedAbbreviation: emmetConfig.showExpandedAbbreviation, + showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions, + syntaxProfiles: emmetConfig.syntaxProfiles, + variables: emmetConfig.variables + } as EmmetConfiguration +} + +export function NotNull(input: any): T { + if (!input) { + return {} as T + } + return input as T +} + +export function MatchOffset( + regex: RegExp, + data: string, + offset: number +): RegExpMatchArray { + regex.exec(null) + + let match: RegExpExecArray + while ((match = regex.exec(data)) !== null) { + if ( + offset > match.index + match[1].length && + offset < match.index + match[0].length + ) { + return match + } + } + return null +} + +export function Match( + regex: RegExp, + data: string +): RegExpMatchArray { + regex.exec(null) + + let match: RegExpExecArray + while ((match = regex.exec(data)) !== null) { + return match + } + return null +} + +export function GetLanguageRegions( + service: LanguageService, + data: string +): IEmbeddedRegion[] { + const scanner = service.createScanner(data) + const regions: IEmbeddedRegion[] = [] + let tokenType: HTMLTokenType + + while ((tokenType = scanner.scan()) !== HTMLTokenType.EOS) { + switch (tokenType) { + case HTMLTokenType.Styles: + regions.push({ + languageId: 'css', + start: scanner.getTokenOffset(), + end: scanner.getTokenEnd(), + length: scanner.getTokenLength(), + content: scanner.getTokenText() + }) + break + default: + break + } + } + + return regions +} + +export function GetRegionAtOffset( + regions: IEmbeddedRegion[], + offset: number +): IEmbeddedRegion { + for (let region of regions) { + if (region.start <= offset) { + if (offset <= region.end) { + return region + } + } else { + break + } + } + return null +} + +export function TranslateHTMLTextEdits( + input: HTMLTextEdit[], + offset: number +): TextEdit[] { + return input.map((item: HTMLTextEdit) => { + const startPosition = new Position(item.range.start.line + offset, item.range.start.character); + const endPosition = new Position(item.range.end.line + offset - 1, item.range.end.character); + const itemRange = new Range(startPosition, endPosition); + return new TextEdit(itemRange, item.newText) + }) +} + +export function TranslateCompletionItems( + items, + line: TextLine, + expand: boolean = false +): CompletionItem[] { + return items.map((item: CompletionItem) => { + const result = item as CompletionItem + const range = new Range( + new Position(line.lineNumber, result.textEdit.range.start.character), + new Position(line.lineNumber, result.textEdit.range.end.character) + ) + + result.textEdit = null + + // @ts-ignore - setting range for intellisense to show results properly + result.range = range + + if (expand) { + // i use this to both expand html abbreviations and auto complete tags + result.command = { + title: 'Emmet Expand Abbreviation', + command: 'editor.emmet.action.expandAbbreviation' + } as Command + } + + return result + }) +} + +export function CreateVirtualDocument( + // context: TextDocument | HTMLTextDocument, + languageId: string, + // position: Position | HtmlPosition, + content: string +): HTMLTextDocument { + const doc = HTMLTextDocument.create( + `embedded://document.${languageId}`, + languageId, + 1, + content + ) + + return doc +} + +export interface IEmbeddedRegion { + languageId: string + start: number + end: number + length: number + content: string +} diff --git a/syntaxes/es6.inline.css.json b/syntaxes/es6.inline.css.json new file mode 100644 index 0000000..cc936b6 --- /dev/null +++ b/syntaxes/es6.inline.css.json @@ -0,0 +1,41 @@ +{ + "fileTypes": [ + "js", + "jsx", + "ts", + "tsx" + ], + "injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", + "patterns": [ + { + "contentName": "meta.embedded.block.css", + "begin": "(?x)(\\s*?(\\w+\\.)?(?:css|\/\\*\\s*css\\s*\\*\/)\\s*)(`)", + "beginCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.begin.ts" + }, + "1": { + "name": "entity.name.function.tagged-template.ts" + } + }, + "end": "(`)", + "endCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.end.ts" + } + }, + "patterns": [ + { + "include": "source.ts#template-substitution-element" + }, + { + "include": "source.css" + } + ] + }, + { + "include": "source.ts#template-substitution-element" + } + ], + "scopeName": "es6.inline.css" +} \ No newline at end of file diff --git a/syntaxes/es6.inline.html.json b/syntaxes/es6.inline.html.json new file mode 100644 index 0000000..32a8ff0 --- /dev/null +++ b/syntaxes/es6.inline.html.json @@ -0,0 +1,51 @@ +{ + "fileTypes": [ + "js", + "jsx", + "ts", + "tsx" + ], + "injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", + "injections": { + "L:source": { + "patterns": [ + { + "match": "<", + "name": "invalid.illegal.bad-angle-bracket.html" + } + ] + } + }, + "patterns": [ + { + "contentName": "meta.embedded.block.html", + "begin": "(?x)(\\s*?(\\w+\\.)?(?:html|\/\\*\\s*html\\s*\\*\/)\\s*)(`)", + "beginCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.begin.ts" + }, + "1": { + "name": "entity.name.function.tagged-template.ts" + } + }, + "end": "(`)", + "endCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.end.ts" + } + }, + "patterns": [ + { + "include": "source.ts#template-substitution-element" + }, + { + "include": "text.html.basic" + } + ] + }, + { + "include": "source.ts#template-substitution-element" + } + ], + "scopeName": "es6.inline.html" +} \ No newline at end of file diff --git a/syntaxes/es6.inline.less.json b/syntaxes/es6.inline.less.json new file mode 100644 index 0000000..8c41997 --- /dev/null +++ b/syntaxes/es6.inline.less.json @@ -0,0 +1,41 @@ +{ + "fileTypes": [ + "js", + "jsx", + "ts", + "tsx" + ], + "injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", + "patterns": [ + { + "contentName": "meta.embedded.block.css", + "begin": "(?x)(\\s*?(\\w+\\.)?(?:\/\\*\\s*less\\s*\\*\/)\\s*)(`)", + "beginCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.begin.ts" + }, + "1": { + "name": "entity.name.function.tagged-template.ts" + } + }, + "end": "(`)", + "endCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.end.ts" + } + }, + "patterns": [ + { + "include": "source.ts#template-substitution-element" + }, + { + "include": "source.css.less" + } + ] + }, + { + "include": "source.ts#template-substitution-element" + } + ], + "scopeName": "es6.inline.less" +} \ No newline at end of file diff --git a/syntaxes/es6.inline.scss.json b/syntaxes/es6.inline.scss.json new file mode 100644 index 0000000..55591da --- /dev/null +++ b/syntaxes/es6.inline.scss.json @@ -0,0 +1,41 @@ +{ + "fileTypes": [ + "js", + "jsx", + "ts", + "tsx" + ], + "injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", + "patterns": [ + { + "contentName": "meta.embedded.block.css", + "begin": "(?x)(\\s*?(\\w+\\.)?(?:\/\\*\\s*scss\\s*\\*\/)\\s*)(`)", + "beginCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.begin.ts" + }, + "1": { + "name": "entity.name.function.tagged-template.ts" + } + }, + "end": "(`)", + "endCaptures": { + "0": { + "name": "string.template.ts, punctuation.definition.string.template.end.ts" + } + }, + "patterns": [ + { + "include": "source.ts#template-substitution-element" + }, + { + "include": "source.css.scss" + } + ] + }, + { + "include": "source.ts#template-substitution-element" + } + ], + "scopeName": "es6.inline.scss" +} \ No newline at end of file diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..a2fc132 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,77 @@ +function html() +{ + html` + + + + this.click(e)} value="deadmau5 🐭" /> + +
+
+
+ +

${['❤️', '💛', '💚', '💙', '💜', '🖤']}

+ + `; + + /* html */` +
+
+
+
+
+

+
+ `; + + bug(/* html*/` +
+ `); + + bug(html` +
+ `); +} + +function bug() +{ + html`div...`; +} + +function css() +{ + css` + :host { + display: block; + } + `; + + /* css */` + :host { + display: block; + height: 50px; + } + `; + + /* css */` + :host { + display: block; + } + `; + + bug(/*css */` + :host { + display: block; + } + `) + + bug(css` + :host { + display: block; + } + `) +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bcb24bd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "sourceMap": false, + "outDir": "dist", + "rootDir": "src", + "lib": ["es2016"] + }, + "exclude": ["node_modules", "tests"] +}