Compare commits

...

6 Commits

Author SHA1 Message Date
yutent a3261162de 唉, 又更新了一堆没啥用的 2023-06-06 18:35:39 +08:00
yutent 583b7586df 一大波更新 2023-06-06 15:26:41 +08:00
yutent a52927cbd4 更新打包脚本 2023-06-05 18:45:47 +08:00
yutent 494b9f68e5 移除ts 2023-06-05 18:24:57 +08:00
yutent a3dd345793 更换ts为js 2023-06-05 10:15:03 +00:00
yutent 7d4010cdf6 更新配置 2023-06-05 10:12:40 +00:00
17 changed files with 263 additions and 579 deletions

View File

@ -5,3 +5,4 @@ tests/**
.vscode/** .vscode/**
.gitignore .gitignore
.git .git
build.mjs

36
build.mjs Normal file
View File

@ -0,0 +1,36 @@
/**
* {build}
* @author yutent<yutent.io@gmail.com>
* @date 2021/08/09 11:59:41
*/
import Es from 'esbuild'
import fs from 'iofs'
let args = process.argv.slice(2)
let entryPoints = fs.ls('./src', true).filter(it => fs.isfile(it))
if (args.includes('--watch')) {
let res = await Es.context({
entryPoints,
outdir: 'dist',
target: 'es2022',
format: 'cjs',
// external: ['vscode', 'util'],
platform: 'node'
// bundle: true
})
console.log('监听文件变化中...\n')
await res.watch()
} else {
Es.build({
entryPoints,
outdir: 'dist',
target: 'es2022',
format: 'cjs',
minify: true,
// external: ['vscode', 'util'],
platform: 'node'
// bundle: true
})
}

View File

@ -14,27 +14,25 @@
"css", "css",
"template", "template",
"polymer", "polymer",
"lit-html" "wkit"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/yutent/vscode-string-html" "url": "https://github.com/yutent/vscode-string-html"
}, },
"engines": { "engines": {
"vscode": "^1.22.0" "vscode": "^1.58.0"
}, },
"scripts": { "scripts": {
"start": "npx tsc --watch -p .", "start": "node build.mjs --watch",
"build": "npx tsc -p ." "build": "node build.mjs"
}, },
"categories": [ "categories": [
"Programming Languages" "Programming Languages"
], ],
"activationEvents": [ "activationEvents": [
"onLanguage:javascript", "onLanguage:javascript",
"onLanguage:typescript", "onLanguage:javascriptreact"
"onLanguage:javascriptreact",
"onLanguage:typescriptreact"
], ],
"main": "./dist/main.js", "main": "./dist/main.js",
"contributes": { "contributes": {
@ -47,79 +45,42 @@
"grammars": [ "grammars": [
{ {
"injectTo": [ "injectTo": [
"source.js", "source.js"
"source.js.jsx",
"source.jsx",
"source.ts",
"source.ts.tsx",
"source.tsx"
], ],
"scopeName": "es6.inline.html", "scopeName": "es6.inline.html",
"path": "./syntaxes/es6.inline.html.json", "path": "./syntaxes/es6.inline.html.json",
"embeddedLanguages": { "embeddedLanguages": {
"meta.embedded.block.html": "html", "meta.embedded.block.html": "html",
"meta.template.expression.ts": "typescript" "meta.template.expression.js": "javascript"
} }
}, },
{ {
"injectTo": [ "injectTo": [
"source.js", "source.js"
"source.js.jsx",
"source.jsx",
"source.ts",
"source.ts.tsx",
"source.tsx"
], ],
"scopeName": "es6.inline.css", "scopeName": "es6.inline.css",
"path": "./syntaxes/es6.inline.css.json", "path": "./syntaxes/es6.inline.css.json",
"embeddedLanguages": { "embeddedLanguages": {
"meta.embedded.block.css": "css", "meta.embedded.block.css": "css",
"meta.template.expression.ts": "typescript" "meta.template.expression.js": "javascript"
} }
}, },
{ {
"injectTo": [ "injectTo": [
"source.js", "source.js"
"source.js.jsx",
"source.jsx",
"source.ts",
"source.ts.tsx",
"source.tsx"
], ],
"scopeName": "es6.inline.scss", "scopeName": "es6.inline.scss",
"path": "./syntaxes/es6.inline.scss.json", "path": "./syntaxes/es6.inline.scss.json",
"embeddedLanguages": { "embeddedLanguages": {
"meta.embedded.block.css": "scss", "meta.embedded.block.css": "scss",
"meta.template.expression.ts": "typescript" "meta.template.expression.js": "javascript"
}
},
{
"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": "^18.14.5",
"@types/vscode": "^1.22.0",
"typescript": "^4.9.5",
"vscode-languageserver-types": "^3.6.0"
},
"dependencies": { "dependencies": {
"vscode-css-languageservice": "^3.0.7", "vscode-css-languageservice": "^6.2.6",
"vscode-emmet-helper": "^1.2.0", "@vscode/emmet-helper": "^2.8.8",
"vscode-html-languageservice": "^2.1.1" "vscode-html-languageservice": "^5.0.6"
} }
} }

31
src/cache.js Normal file
View File

@ -0,0 +1,31 @@
// Code from https://github.com/microsoft/typescript-styled-plugin/blob/main/src/_language-service.ts
function arePositionsEqual(left, right) {
return left.line === right.line && left.character === right.character
}
export class CompletionsCache {
_cachedCompletionsFile
_cachedCompletionsPosition
_cachedCompletionsContent
_completions
getCached(context, position) {
if (
this._completions &&
context.fileName === this._cachedCompletionsFile &&
this._cachedCompletionsPosition &&
arePositionsEqual(position, this._cachedCompletionsPosition) &&
context.text === this._cachedCompletionsContent
) {
return this._completions
}
}
updateCached(context, position, completions) {
this._cachedCompletionsFile = context.fileName
this._cachedCompletionsPosition = position
this._cachedCompletionsContent = context.text
this._completions = completions
}
}

View File

@ -1,42 +0,0 @@
// 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: any
) {
this._cachedCompletionsFile = context.fileName
this._cachedCompletionsPosition = position
this._cachedCompletionsContent = context.getText()
this._completions = completions
}
}

View File

@ -1,28 +1,21 @@
import { import { languages } from 'vscode'
languages as Languages, import { HTMLCompletionItemProvider } from './providers/html.js'
ExtensionContext,
commands as Commands,
DocumentSelector
} from 'vscode'
import { HTMLCompletionItemProvider } from './providers/html'
import { import {
CSSCompletionItemProvider, CSSCompletionItemProvider,
HTMLStyleCompletionItemProvider HTMLStyleCompletionItemProvider
} from './providers/css' } from './providers/css.js'
import { HTMLHoverProvider, CSSHoverProvider } from './providers/hover' import { HTMLHoverProvider, CSSHoverProvider } from './providers/hover.js'
import { CodeFormatterProvider } from './providers/formatting' import { CodeFormatterProvider } from './providers/formatting.js'
const selector = ['javascriptreact', 'javascript']
const selector: DocumentSelector = [ export function activate(Context) {
'typescriptreact',
'javascriptreact',
'typescript',
'javascript'
]
export function activate(Context: ExtensionContext) {
new CodeFormatterProvider() new CodeFormatterProvider()
Languages.registerCompletionItemProvider( languages.registerHoverProvider(selector, new HTMLHoverProvider())
languages.registerHoverProvider(selector, new CSSHoverProvider())
// HTMLCompletionItemProvider
languages.registerCompletionItemProvider(
selector, selector,
new HTMLCompletionItemProvider(), new HTMLCompletionItemProvider(),
'<', '<',
@ -44,8 +37,9 @@ export function activate(Context: ExtensionContext) {
'8', '8',
'9' '9'
) )
Languages.registerHoverProvider(selector, new HTMLHoverProvider())
Languages.registerCompletionItemProvider( // HTMLStyleCompletionItemProvider
languages.registerCompletionItemProvider(
selector, selector,
new HTMLStyleCompletionItemProvider(), new HTMLStyleCompletionItemProvider(),
'!', '!',
@ -66,8 +60,9 @@ export function activate(Context: ExtensionContext) {
'8', '8',
'9' '9'
) )
Languages.registerHoverProvider(selector, new CSSHoverProvider())
Languages.registerCompletionItemProvider( // CSSCompletionItemProvider
languages.registerCompletionItemProvider(
selector, selector,
new CSSCompletionItemProvider(), new CSSCompletionItemProvider(),
'!', '!',

View File

@ -1,25 +1,6 @@
import { import { getLanguageService } from 'vscode-html-languageservice'
CompletionList, import { getSCSSLanguageService } from 'vscode-css-languageservice'
CompletionItem, import * as emmet from '@vscode/emmet-helper'
TextDocument,
Position,
CancellationToken,
CompletionItemProvider
} from 'vscode'
import {
getLanguageService as GetHTMLanguageService,
LanguageService as HTMLanguageService,
CompletionList as HTMLCompletionList
} from 'vscode-html-languageservice'
import {
getSCSSLanguageService as GetSCSSLanguageService,
LanguageService as CSSLanguageService,
CompletionList as CSSCompletionList
} from 'vscode-css-languageservice'
import * as emmet from 'vscode-emmet-helper'
import { import {
GetEmmetConfiguration, GetEmmetConfiguration,
MatchOffset, MatchOffset,
@ -27,71 +8,54 @@ import {
GetLanguageRegions, GetLanguageRegions,
GetRegionAtOffset, GetRegionAtOffset,
TranslateCompletionItems TranslateCompletionItems
} from '../util' } from '../util.js'
import { CompletionsCache } from '../cache.js'
import { CompletionsCache } from '../cache' export class HTMLStyleCompletionItemProvider {
_cssLanguageService = getSCSSLanguageService()
_HTMLanguageService = getLanguageService()
_expression = /(html\s*`)([^`]*)(`)/g
_cache = new CompletionsCache()
export class HTMLStyleCompletionItemProvider implements CompletionItemProvider { provideCompletionItems(document, position, _token) {
private _cssLanguageService: CSSLanguageService = GetSCSSLanguageService()
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) const cached = this._cache.getCached(document, position)
if (cached) { if (cached) {
return cached return cached
} }
const currentLine = document.lineAt(position.line) const currentLine = document.lineAt(position.line)
const empty = { const empty = {
isIncomplete: false, isIncomplete: false,
items: [] items: []
} as CompletionList }
if (currentLine.isEmptyOrWhitespace) { if (currentLine.isEmptyOrWhitespace) {
return empty return empty
} }
const currentLineText = currentLine.text.trim() const currentLineText = currentLine.text.trim()
const currentOffset = document.offsetAt(position) const currentOffset = document.offsetAt(position)
const documentText = document.getText() const documentText = document.getText()
const match = MatchOffset(this._expression, documentText, currentOffset) const match = MatchOffset(this._expression, documentText, currentOffset)
if (!match) { if (!match) {
return empty return empty
} }
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
const matchContent: string = match[2] const matchContent = match[2]
const matchStartOffset = match.index + match[1].length const matchStartOffset = match.index + match[1].length
const matchEndOffset = match.index + match[0].length const matchEndOffset = match.index + match[0].length
const regions = GetLanguageRegions(this._HTMLanguageService, matchContent) const regions = GetLanguageRegions(this._HTMLanguageService, matchContent)
if (regions.length <= 0) { if (regions.length <= 0) {
return empty return empty
} }
const region = GetRegionAtOffset(regions, currentOffset - matchStartOffset) const region = GetRegionAtOffset(regions, currentOffset - matchStartOffset)
if (!region) { if (!region) {
return empty return empty
} }
const virtualOffset = currentOffset - (matchStartOffset + region.start) const virtualOffset = currentOffset - (matchStartOffset + region.start)
const virtualDocument = CreateVirtualDocument('css', region.content) const virtualDocument = CreateVirtualDocument('css', region.content)
const stylesheet = this._cssLanguageService.parseStylesheet(virtualDocument) const stylesheet = this._cssLanguageService.parseStylesheet(virtualDocument)
const emmetResults: HTMLCompletionList = { const emmetResults = {
isIncomplete: true, isIncomplete: true,
items: [] items: []
} }
this._cssLanguageService.setCompletionParticipants([ this._cssLanguageService.setCompletionParticipants([
emmet.getEmmetCompletionParticipants( emmet.getEmmetCompletionParticipants(
virtualDocument, virtualDocument,
@ -101,85 +65,64 @@ export class HTMLStyleCompletionItemProvider implements CompletionItemProvider {
emmetResults emmetResults
) )
]) ])
const completions = this._cssLanguageService.doComplete( const completions = this._cssLanguageService.doComplete(
virtualDocument, virtualDocument,
virtualDocument.positionAt(virtualOffset), virtualDocument.positionAt(virtualOffset),
stylesheet stylesheet
) )
if (emmetResults.items.length) { if (emmetResults.items.length) {
completions.items.push(...emmetResults.items) completions.items.push(...emmetResults.items)
completions.isIncomplete = true completions.isIncomplete = true
} }
this._cache.updateCached(document, position, completions) this._cache.updateCached(document, position, completions)
return { return {
isIncomplete: completions.isIncomplete, isIncomplete: completions.isIncomplete,
items: TranslateCompletionItems(completions.items, currentLine) items: TranslateCompletionItems(completions.items, currentLine)
} as CompletionList }
} }
resolveCompletionItem(item, _token) {
public resolveCompletionItem?(
item: CompletionItem,
_token: CancellationToken
): CompletionItem | Thenable<CompletionItem> {
return item return item
} }
} }
export class CSSCompletionItemProvider implements CompletionItemProvider { export class CSSCompletionItemProvider {
private _CSSLanguageService: CSSLanguageService = GetSCSSLanguageService() _CSSLanguageService = getSCSSLanguageService()
private _expression = _expression = /(css\s*`)([^`]*)(`)/g
/(\/\*\s*(css|less|scss|sass)\s*\*\/\s*`|css\s*`)([^`]*)(`)/g _cache = new CompletionsCache()
private _cache = new CompletionsCache()
public provideCompletionItems( provideCompletionItems(document, position, _token) {
document: TextDocument,
position: Position,
_token: CancellationToken
): CompletionList {
const cached = this._cache.getCached(document, position) const cached = this._cache.getCached(document, position)
if (cached) { if (cached) {
return cached return cached
} }
const currentLine = document.lineAt(position.line) const currentLine = document.lineAt(position.line)
const empty = { const empty = {
isIncomplete: false, isIncomplete: false,
items: [] items: []
} as CompletionList }
if (currentLine.isEmptyOrWhitespace) { if (currentLine.isEmptyOrWhitespace) {
return empty return empty
} }
const currentLineText = currentLine.text.trim() const currentLineText = currentLine.text.trim()
const currentOffset = document.offsetAt(position) const currentOffset = document.offsetAt(position)
const documentText = document.getText() const documentText = document.getText()
const match = MatchOffset(this._expression, documentText, currentOffset) const match = MatchOffset(this._expression, documentText, currentOffset)
if (!match) { if (!match) {
return empty return empty
} }
const dialect = match[2] const dialect = match[2]
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
const matchContent: string = match[3] const matchContent = match[3]
const matchStartOffset = match.index + match[1].length const matchStartOffset = match.index + match[1].length
const matchEndOffset = match.index + match[0].length const matchEndOffset = match.index + match[0].length
const matchPosition = document.positionAt(matchStartOffset) const matchPosition = document.positionAt(matchStartOffset)
const virtualOffset = currentOffset - matchStartOffset const virtualOffset = currentOffset - matchStartOffset
const virtualDocument = CreateVirtualDocument(dialect, matchContent) const virtualDocument = CreateVirtualDocument(dialect, matchContent)
const vCss = this._CSSLanguageService.parseStylesheet(virtualDocument) const vCss = this._CSSLanguageService.parseStylesheet(virtualDocument)
const emmetResults: CSSCompletionList = { const emmetResults = {
isIncomplete: true, isIncomplete: true,
items: [] items: []
} }
this._CSSLanguageService.setCompletionParticipants([ this._CSSLanguageService.setCompletionParticipants([
emmet.getEmmetCompletionParticipants( emmet.getEmmetCompletionParticipants(
virtualDocument, virtualDocument,
@ -189,30 +132,22 @@ export class CSSCompletionItemProvider implements CompletionItemProvider {
emmetResults emmetResults
) )
]) ])
const completions = this._CSSLanguageService.doComplete( const completions = this._CSSLanguageService.doComplete(
virtualDocument, virtualDocument,
virtualDocument.positionAt(virtualOffset), virtualDocument.positionAt(virtualOffset),
vCss vCss
) )
if (emmetResults.items.length) { if (emmetResults.items.length) {
completions.items.push(...emmetResults.items) completions.items.push(...emmetResults.items)
completions.isIncomplete = true completions.isIncomplete = true
} }
this._cache.updateCached(document, position, completions) this._cache.updateCached(document, position, completions)
return { return {
isIncomplete: completions.isIncomplete, isIncomplete: completions.isIncomplete,
items: TranslateCompletionItems(completions.items, currentLine) items: TranslateCompletionItems(completions.items, currentLine)
} as CompletionList }
} }
resolveCompletionItem(item, _token) {
public resolveCompletionItem?(
item: CompletionItem,
_token: CancellationToken
): CompletionItem | Thenable<CompletionItem> {
return item return item
} }
} }

View File

@ -1,78 +1,54 @@
import { import { Range, WorkspaceEdit, workspace, commands } from 'vscode'
Range, import { getLanguageService } from 'vscode-html-languageservice'
TextDocument,
WorkspaceEdit,
workspace as Workspace,
TextEditor,
commands as Commands,
Uri,
TextEdit,
Position
} from 'vscode'
import {
getLanguageService as GetHTMLanguageService,
Position as HTMLPosition
} from 'vscode-html-languageservice'
import { import {
CreateVirtualDocument, CreateVirtualDocument,
TranslateHTMLTextEdits, TranslateHTMLTextEdits,
Match, Match
GetLanguageRegions, } from '../util.js'
IEmbeddedRegion
} from '../util'
export class CodeFormatterProvider { export class CodeFormatterProvider {
private _expression = /(\/\*\s*html\s*\*\/\s*`|html\s*`)([^`]*)(`)/g _expression = /(html\s*`)([^`]*)(`)/g
private document: TextDocument document
constructor() { constructor() {
Commands.registerTextEditorCommand( commands.registerTextEditorCommand(
'editor.action.formatInlineHtml', 'editor.action.formatInlineHtml',
this.format, this.format,
this this
) )
} }
public format(textEditor: TextEditor) { format(textEditor) {
this.document = textEditor.document this.document = textEditor.document
var documentText = this.document.getText() var documentText = this.document.getText()
var match = Match(this._expression, documentText) var match = Match(this._expression, documentText)
if (!match) { if (!match) {
return [] return []
} }
// TODO - Refactor, This have been used multiple times thourgh out the // TODO - Refactor, This have been used multiple times thourgh out the
// TODO - extension. // TODO - extension.
var matchStartOffset = match.index + match[1].length var matchStartOffset = match.index + match[1].length
var matchEndOffset = match.index + (match[2].length + match[3].length + 1) var matchEndOffset = match.index + (match[2].length + match[3].length + 1)
var matchStartPosition = this.document.positionAt(matchStartOffset) var matchStartPosition = this.document.positionAt(matchStartOffset)
var matchEndPosition = this.document.positionAt(matchEndOffset) var matchEndPosition = this.document.positionAt(matchEndOffset)
var text = this.document.getText( var text = this.document.getText(
new Range(matchStartPosition, matchEndPosition) new Range(matchStartPosition, matchEndPosition)
) )
var vHTML = CreateVirtualDocument('html', text) var vHTML = CreateVirtualDocument('html', text)
// TODO - Expose Formatting Options // TODO - Expose Formatting Options
const edits = TranslateHTMLTextEdits( const edits = TranslateHTMLTextEdits(
GetHTMLanguageService().format(vHTML, null, { getLanguageService().format(vHTML, null, {
indentInnerHtml: false, indentInnerHtml: false,
preserveNewLines: true, preserveNewLines: true,
tabSize: <number>textEditor.options.tabSize, tabSize: textEditor.options.tabSize,
insertSpaces: <boolean>textEditor.options.insertSpaces, insertSpaces: textEditor.options.insertSpaces,
endWithNewline: true endWithNewline: true
}), }),
matchStartPosition.line + 1 matchStartPosition.line + 1
) )
workspace.applyEdit(this.composeEdits(this.document.uri, edits))
Workspace.applyEdit(this.composeEdits(this.document.uri, edits))
} }
composeEdits(uri, edits) {
private composeEdits(uri: Uri, edits: TextEdit[]): WorkspaceEdit {
var ws = new WorkspaceEdit() var ws = new WorkspaceEdit()
ws.set(uri, edits) ws.set(uri, edits)
return ws return ws

View File

@ -1,45 +1,21 @@
import { import { getLanguageService } from 'vscode-html-languageservice'
HoverProvider, import { getCSSLanguageService } from 'vscode-css-languageservice'
TextDocument, import { CreateVirtualDocument, MatchOffset } from '../util.js'
Position,
CancellationToken,
Hover
} from 'vscode'
import { export class HTMLHoverProvider {
getLanguageService as GetHtmlLanguageService, _htmlLanguageService = getLanguageService()
LanguageService as HtmlLanguageService, _cssLanguageService = getCSSLanguageService()
CompletionList as HtmlCompletionList _expression = /(html\s*`)([^`]*)(`)/g
} from 'vscode-html-languageservice'
import { provideHover(document, position, token) {
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 currentOffset = document.offsetAt(position)
const documentText = document.getText() const documentText = document.getText()
const match = MatchOffset(this._expression, documentText, currentOffset) const match = MatchOffset(this._expression, documentText, currentOffset)
if (!match) { if (!match) {
return null return null
} }
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
const matchContent: string = match[2] const matchContent = match[2]
const matchStartOffset = match.index + match[1].length const matchStartOffset = match.index + match[1].length
const virtualOffset = currentOffset - matchStartOffset const virtualOffset = currentOffset - matchStartOffset
const virtualDocument = CreateVirtualDocument('html', matchContent) const virtualDocument = CreateVirtualDocument('html', matchContent)
@ -56,33 +32,25 @@ export class HTMLHoverProvider implements HoverProvider {
virtualDocument.positionAt(virtualOffset), virtualDocument.positionAt(virtualOffset),
stylesheet stylesheet
) )
return hover
return hover as Hover
} }
} }
export class CSSHoverProvider implements HoverProvider { export class CSSHoverProvider {
private _htmlLanguageService: HtmlLanguageService = GetHtmlLanguageService() _htmlLanguageService = getLanguageService()
private _cssLanguageService: CssLanguageService = GetCssLanguageService() _cssLanguageService = getCSSLanguageService()
private _expression = /(\/\*\s*(css|less|scss)\s*\*\/\s*`|css\s*`)([^`]*)(`)/g _expression = /(css\s*`)([^`]*)(`)/g
provideHover( provideHover(document, position, token) {
document: TextDocument,
position: Position,
token: CancellationToken
): Hover {
const currentOffset = document.offsetAt(position) const currentOffset = document.offsetAt(position)
const documentText = document.getText() const documentText = document.getText()
const match = MatchOffset(this._expression, documentText, currentOffset) const match = MatchOffset(this._expression, documentText, currentOffset)
if (!match) { if (!match) {
return null return null
} }
const dialect = match[2] const dialect = match[2]
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
const matchContent: string = match[3] const matchContent = match[3]
const matchStartOffset = match.index + match[1].length const matchStartOffset = match.index + match[1].length
const virtualOffset = currentOffset - matchStartOffset const virtualOffset = currentOffset - matchStartOffset
const virtualDocument = CreateVirtualDocument(dialect, matchContent) const virtualDocument = CreateVirtualDocument(dialect, matchContent)
@ -92,7 +60,6 @@ export class CSSHoverProvider implements HoverProvider {
virtualDocument.positionAt(virtualOffset), virtualDocument.positionAt(virtualOffset),
stylesheet stylesheet
) )
return hover
return hover as Hover
} }
} }

View File

@ -1,78 +1,50 @@
import { import { getLanguageService } from 'vscode-html-languageservice'
CompletionList, import * as emmet from '@vscode/emmet-helper'
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 { import {
GetEmmetConfiguration, GetEmmetConfiguration,
MatchOffset, MatchOffset,
CreateVirtualDocument, CreateVirtualDocument,
TranslateCompletionItems TranslateCompletionItems
} from '../util' } from '../util.js'
import { CompletionsCache } from '../cache.js'
import { CompletionsCache } from '../cache' export class HTMLCompletionItemProvider {
_htmlLanguageService = getLanguageService()
_expression = /(html\s*`)([^`]*)(`)/g
_cache = new CompletionsCache()
export class HTMLCompletionItemProvider implements CompletionItemProvider { provideCompletionItems(document, position, token) {
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) const cached = this._cache.getCached(document, position)
if (cached) { if (cached) {
return cached return cached
} }
const currentLine = document.lineAt(position.line) const currentLine = document.lineAt(position.line)
const empty = { const empty = {
isIncomplete: false, isIncomplete: false,
items: [] items: []
} as CompletionList }
if (currentLine.isEmptyOrWhitespace) { if (currentLine.isEmptyOrWhitespace) {
return empty return empty
} }
const currentLineText = currentLine.text.trim() const currentLineText = currentLine.text.trim()
const currentOffset = document.offsetAt(position) const currentOffset = document.offsetAt(position)
const documentText = document.getText() const documentText = document.getText()
const match = MatchOffset(this._expression, documentText, currentOffset) const match = MatchOffset(this._expression, documentText, currentOffset)
if (!match) { if (!match) {
return empty return empty
} }
// tslint:disable-next-line:no-magic-numbers // tslint:disable-next-line:no-magic-numbers
const matchContent: string = match[2] const matchContent = match[2]
const matchStartOffset = match.index + match[1].length const matchStartOffset = match.index + match[1].length
const matchEndOffset = match.index + match[0].length const matchEndOffset = match.index + match[0].length
const matchPosition = document.positionAt(matchStartOffset) const matchPosition = document.positionAt(matchStartOffset)
const virtualOffset = currentOffset - matchStartOffset const virtualOffset = currentOffset - matchStartOffset
const virtualDocument = CreateVirtualDocument('html', matchContent) const virtualDocument = CreateVirtualDocument('html', matchContent)
const vHtml = this._htmlLanguageService.parseHTMLDocument(virtualDocument) const vHtml = this._htmlLanguageService.parseHTMLDocument(virtualDocument)
const emmetResults: HTMLCompletionList = { const emmetResults = {
isIncomplete: true, isIncomplete: true,
items: [] items: []
} }
this._htmlLanguageService.setCompletionParticipants([ this._htmlLanguageService.setCompletionParticipants([
emmet.getEmmetCompletionParticipants( emmet.getEmmetCompletionParticipants(
virtualDocument, virtualDocument,
@ -82,30 +54,22 @@ export class HTMLCompletionItemProvider implements CompletionItemProvider {
emmetResults emmetResults
) )
]) ])
const completions = this._htmlLanguageService.doComplete( const completions = this._htmlLanguageService.doComplete(
virtualDocument, virtualDocument,
virtualDocument.positionAt(virtualOffset), virtualDocument.positionAt(virtualOffset),
vHtml vHtml
) )
if (emmetResults.items.length) { if (emmetResults.items.length) {
completions.items.push(...emmetResults.items) completions.items.push(...emmetResults.items)
completions.isIncomplete = true completions.isIncomplete = true
} }
this._cache.updateCached(document, position, completions) this._cache.updateCached(document, position, completions)
return { return {
isIncomplete: completions.isIncomplete, isIncomplete: completions.isIncomplete,
items: TranslateCompletionItems(completions.items, currentLine, true) items: TranslateCompletionItems(completions.items, currentLine, true)
} as CompletionList }
} }
resolveCompletionItem(item, token) {
public resolveCompletionItem?(
item: CompletionItem,
token: CancellationToken
): CompletionItem | Thenable<CompletionItem> {
return item return item
} }
} }

View File

@ -1,23 +1,7 @@
import { import { workspace, TextEdit, Position, Range } from 'vscode'
workspace, import { TextDocument, TokenType } from 'vscode-html-languageservice'
TextLine,
TextEdit,
Position,
Range,
CompletionItem,
Command
} from 'vscode'
import { export function GetEmmetConfiguration() {
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') const emmetConfig = workspace.getConfiguration('emmet')
return { return {
useNewEmmet: true, useNewEmmet: true,
@ -25,24 +9,19 @@ export function GetEmmetConfiguration(): EmmetConfiguration {
showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions, showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions,
syntaxProfiles: emmetConfig.syntaxProfiles, syntaxProfiles: emmetConfig.syntaxProfiles,
variables: emmetConfig.variables variables: emmetConfig.variables
} as EmmetConfiguration
}
export function NotNull<T>(input: any): T {
if (!input) {
return {} as T
} }
return input as T
} }
export function MatchOffset( export function NotNull(input) {
regex: RegExp, if (!input) {
data: string, return {}
offset: number }
): RegExpMatchArray { return input
regex.exec(null) }
let match: RegExpExecArray export function MatchOffset(regex, data, offset) {
regex.exec(null)
let match
while ((match = regex.exec(data)) !== null) { while ((match = regex.exec(data)) !== null) {
if ( if (
offset > match.index + match[1].length && offset > match.index + match[1].length &&
@ -53,28 +32,21 @@ export function MatchOffset(
} }
return null return null
} }
export function Match(regex, data) {
export function Match(regex: RegExp, data: string): RegExpMatchArray {
regex.exec(null) regex.exec(null)
let match
let match: RegExpExecArray
while ((match = regex.exec(data)) !== null) { while ((match = regex.exec(data)) !== null) {
return match return match
} }
return null return null
} }
export function GetLanguageRegions(service, data) {
export function GetLanguageRegions(
service: LanguageService,
data: string
): IEmbeddedRegion[] {
const scanner = service.createScanner(data) const scanner = service.createScanner(data)
const regions: IEmbeddedRegion[] = [] const regions = []
let tokenType: HTMLTokenType let tokenType
while ((tokenType = scanner.scan()) !== TokenType.EOS) {
while ((tokenType = scanner.scan()) !== HTMLTokenType.EOS) {
switch (tokenType) { switch (tokenType) {
case HTMLTokenType.Styles: case TokenType.Styles:
regions.push({ regions.push({
languageId: 'css', languageId: 'css',
start: scanner.getTokenOffset(), start: scanner.getTokenOffset(),
@ -87,14 +59,9 @@ export function GetLanguageRegions(
break break
} }
} }
return regions return regions
} }
export function GetRegionAtOffset(regions, offset) {
export function GetRegionAtOffset(
regions: IEmbeddedRegion[],
offset: number
): IEmbeddedRegion {
for (let region of regions) { for (let region of regions) {
if (region.start <= offset) { if (region.start <= offset) {
if (offset <= region.end) { if (offset <= region.end) {
@ -106,12 +73,8 @@ export function GetRegionAtOffset(
} }
return null return null
} }
export function TranslateHTMLTextEdits(input, offset) {
export function TranslateHTMLTextEdits( return input.map(item => {
input: HTMLTextEdit[],
offset: number
): TextEdit[] {
return input.map((item: HTMLTextEdit) => {
const startPosition = new Position( const startPosition = new Position(
item.range.start.line + offset, item.range.start.line + offset,
item.range.start.character item.range.start.character
@ -124,56 +87,37 @@ export function TranslateHTMLTextEdits(
return new TextEdit(itemRange, item.newText) return new TextEdit(itemRange, item.newText)
}) })
} }
export function TranslateCompletionItems(items, line, expand = false) {
export function TranslateCompletionItems( return items.map(item => {
items, const result = item
line: TextLine,
expand: boolean = false
): CompletionItem[] {
return items.map((item: CompletionItem) => {
const result = item as CompletionItem
const range = new Range( const range = new Range(
new Position(line.lineNumber, result.textEdit.range.start.character), new Position(line.lineNumber, result.textEdit.range.start.character),
new Position(line.lineNumber, result.textEdit.range.end.character) new Position(line.lineNumber, result.textEdit.range.end.character)
) )
result.textEdit = null result.textEdit = null
// @ts-ignore - setting range for intellisense to show results properly // @ts-ignore - setting range for intellisense to show results properly
result.range = range result.range = range
if (expand) { if (expand) {
// i use this to both expand html abbreviations and auto complete tags // i use this to both expand html abbreviations and auto complete tags
result.command = { result.command = {
title: 'Emmet Expand Abbreviation', title: 'Emmet Expand Abbreviation',
command: 'editor.emmet.action.expandAbbreviation' command: 'editor.emmet.action.expandAbbreviation'
} as Command }
} }
return result return result
}) })
} }
export function CreateVirtualDocument( export function CreateVirtualDocument(
// context: TextDocument | HTMLTextDocument, // context: TextDocument | TextDocument,
languageId: string, languageId,
// position: Position | HtmlPosition, // position: Position | HtmlPosition,
content: string content
): HTMLTextDocument { ) {
const doc = HTMLTextDocument.create( const doc = TextDocument.create(
`embedded://document.${languageId}`, `embedded://document.${languageId}`,
languageId, languageId,
1, 1,
content content
) )
return doc return doc
} }
export interface IEmbeddedRegion {
languageId: string
start: number
end: number
length: number
content: string
}

View File

@ -1,27 +1,28 @@
{ {
"fileTypes": ["js", "jsx", "ts", "tsx"], "scopeName": "es6.inline.css",
"injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", "fileTypes": ["js"],
"injectionSelector": "L:source.js -comment -string",
"patterns": [ "patterns": [
{ {
"contentName": "meta.embedded.block.css", "contentName": "meta.embedded.block.css",
"begin": "(?x)(\\s*?(\\w+\\.)?(?:css|/\\*\\s*css\\s*\\*/)\\s*)(`)", "begin": "(?i)(\\s*css)(`)",
"beginCaptures": { "beginCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.begin.ts" "name": "string.template.js, punctuation.definition.string.template.begin.js"
}, },
"1": { "1": {
"name": "entity.name.function.tagged-template.ts" "name": "entity.name.function.tagged-template.js"
} }
}, },
"end": "(`)", "end": "(`)",
"endCaptures": { "endCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.end.ts" "name": "string.template.js, punctuation.definition.string.template.end.js"
} }
}, },
"patterns": [ "patterns": [
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
}, },
{ {
"include": "source.css.scss" "include": "source.css.scss"
@ -29,8 +30,7 @@
] ]
}, },
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
} }
], ]
"scopeName": "es6.inline.css"
} }

View File

@ -1,6 +1,7 @@
{ {
"fileTypes": ["js", "jsx", "ts", "tsx"], "scopeName": "es6.inline.html",
"injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", "fileTypes": ["js"],
"injectionSelector": "L:source.js -comment -string",
"injections": { "injections": {
"L:source": { "L:source": {
"patterns": [ "patterns": [
@ -14,24 +15,24 @@
"patterns": [ "patterns": [
{ {
"contentName": "meta.embedded.block.html", "contentName": "meta.embedded.block.html",
"begin": "(?x)(\\s*?(\\w+\\.)?(?:html|/\\*\\s*html\\s*\\*/)\\s*)(`)", "begin": "(?i)(\\s*html)(`)",
"beginCaptures": { "beginCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.begin.ts" "name": "string.template.js, punctuation.definition.string.template.begin.js"
}, },
"1": { "1": {
"name": "entity.name.function.tagged-template.ts" "name": "entity.name.function.tagged-template.js"
} }
}, },
"end": "(`)", "end": "(`)",
"endCaptures": { "endCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.end.ts" "name": "string.template.js, punctuation.definition.string.template.end.js"
} }
}, },
"patterns": [ "patterns": [
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
}, },
{ {
"include": "text.html.basic" "include": "text.html.basic"
@ -39,8 +40,7 @@
] ]
}, },
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
} }
], ]
"scopeName": "es6.inline.html"
} }

View File

@ -1,36 +0,0 @@
{
"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+\\.)?(?:less|/\\*\\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"
}

View File

@ -1,27 +1,28 @@
{ {
"fileTypes": ["js", "jsx", "ts", "tsx"], "scopeName": "es6.inline.scss",
"injectionSelector": "L:source.js -comment -string, L:source.jsx -comment -string, L:source.ts -comment -string, L:source.tsx -comment -string", "fileTypes": ["js"],
"injectionSelector": "L:source.js -comment -string",
"patterns": [ "patterns": [
{ {
"contentName": "meta.embedded.block.css", "contentName": "meta.embedded.block.css",
"begin": "(?x)(\\s*?(\\w+\\.)?(?:scss|/\\*\\s*scss\\s*\\*/)\\s*)(`)", "begin": "(?i)(\\s*scss)(`)",
"beginCaptures": { "beginCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.begin.ts" "name": "string.template.js, punctuation.definition.string.template.begin.js"
}, },
"1": { "1": {
"name": "entity.name.function.tagged-template.ts" "name": "entity.name.function.tagged-template.js"
} }
}, },
"end": "(`)", "end": "(`)",
"endCaptures": { "endCaptures": {
"0": { "0": {
"name": "string.template.ts, punctuation.definition.string.template.end.ts" "name": "string.template.js, punctuation.definition.string.template.end.js"
} }
}, },
"patterns": [ "patterns": [
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
}, },
{ {
"include": "source.css.scss" "include": "source.css.scss"
@ -29,8 +30,7 @@
] ]
}, },
{ {
"include": "source.ts#template-substitution-element" "include": "source.js#template-substitution-element"
} }
], ]
"scopeName": "es6.inline.scss"
} }

View File

@ -1,77 +1,40 @@
function html() html`
{ <style>
html` :host {
<style> display: block;
:host { }
display: block; </style>
}
</style>
<body> <body>
<input type="button" @click=${(e) => this.click(e)} value="deadmau5 🐭" /> <input type="button" @click=${e => this.click(e)} value="deadmau5 🐭" />
<div></div> <div></div>
<div></div> <div></div>
<div></div> <div></div>
<h3>${['❤️', '💛', '💚', '💙', '💜', '🖤']}</h3> <h3>${['❤️', '💛', '💚', '💙', '💜', '🖤']}</h3>
</body> </body>
`; `
/* html */` bug(html` <div></div> `)
<div></div>
<div></div>
<div></div>
<div></div>
<div>
<p></p>
</div>
`;
bug(/* html*/` function bug() {
<div></div> html`div...`
`);
bug(html`
<div></div>
`);
} }
function bug() css`
{ .foo {
html`div...`; display: block;
}
function css() .bar {
{ color: #f30;
css` }
:host { }
display: block; `
}
`;
/* css */` css`
:host { :host {
display: block; display: flex;
height: 50px; height: 50px;
} }
`; `
/* css */`
:host {
display: block;
}
`;
bug(/*css */`
:host {
display: block;
}
`)
bug(css`
:host {
display: block;
}
`)
}

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": false,
"outDir": "dist",
"rootDir": "src",
"lib": ["es2016"]
},
"exclude": ["node_modules", "tests"]
}