init
commit
cec2620c5f
|
@ -0,0 +1,6 @@
|
|||
**/*.vsix
|
||||
**/*.js.map
|
||||
node_modules
|
||||
client
|
||||
.vscode
|
||||
.DS_STORE
|
|
@ -0,0 +1,10 @@
|
|||
jsxBracketSameLine: true
|
||||
jsxSingleQuote: true
|
||||
semi: false
|
||||
singleQuote: true
|
||||
printWidth: 80
|
||||
useTabs: false
|
||||
tabWidth: 2
|
||||
trailingComma: none
|
||||
bracketSpacing: true
|
||||
arrowParens: avoid
|
|
@ -0,0 +1,7 @@
|
|||
**/*.ts
|
||||
**/tsconfig.json
|
||||
server/**
|
||||
tests/**
|
||||
.vscode/**
|
||||
.gitignore
|
||||
.git
|
|
@ -0,0 +1,3 @@
|
|||
# Changelog
|
||||
|
||||
## v1.0.0
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
## string-html-css
|
||||
一个高亮js代码中的 html/css/scss/sass/less的字符串, 并支持emmet.
|
Binary file not shown.
After Width: | Height: | Size: 520 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
)
|
||||
}
|
|
@ -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<CompletionItem> {
|
||||
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<CompletionItem> {
|
||||
return item
|
||||
}
|
||||
}
|
|
@ -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: <number>textEditor.options.tabSize,
|
||||
insertSpaces: <boolean>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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<CompletionItem> {
|
||||
return item
|
||||
}
|
||||
}
|
|
@ -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<T>(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
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
function html()
|
||||
{
|
||||
html`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<input type="button" @click=${(e) => this.click(e)} value="deadmau5 🐭" />
|
||||
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
|
||||
<h3>${['❤️', '💛', '💚', '💙', '💜', '🖤']}</h3>
|
||||
</body>
|
||||
`;
|
||||
|
||||
/* html */`
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<p></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
bug(/* html*/`
|
||||
<div></div>
|
||||
`);
|
||||
|
||||
bug(html`
|
||||
<div></div>
|
||||
`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["es2016"]
|
||||
},
|
||||
"exclude": ["node_modules", "tests"]
|
||||
}
|
Loading…
Reference in New Issue