├── .gitignore ├── images ├── icon.png ├── screenshot.gif ├── screenshots.png └── language-mode.png ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── server ├── lib │ └── OSSREADME.json ├── tsconfig.json ├── src │ ├── utils │ │ ├── arrays.ts │ │ ├── errors.ts │ │ ├── edits.ts │ │ ├── documentContext.ts │ │ └── strings.ts │ ├── languageModelCache.ts │ ├── modes │ │ ├── cssMode.ts │ │ ├── formatting.ts │ │ ├── htmlMode.ts │ │ ├── languageModes.ts │ │ ├── embeddedSupport.ts │ │ └── javascriptMode.ts │ └── htmlServerMain.ts ├── package.json └── package-lock.json ├── .editorconfig ├── tsconfig.json ├── .vscodeignore ├── blade.configuration.json ├── snippets ├── livewire.json ├── helpers.json ├── blade.json └── snippets.json ├── webpack.config.js ├── src ├── services │ └── BladeFormatter.ts ├── providers │ └── BladeFormattingEditProvider.ts └── extension.ts ├── LICENSE.md ├── package.json ├── README.md ├── CHANGELOG.md └── tests └── test.blade.php /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | *.vsix 4 | **/*.log 5 | .vscode/tests/ 6 | **/temp/ -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/HEAD/images/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.triggerExpansionOnTab": true, 3 | "blade.format.enable": true 4 | } -------------------------------------------------------------------------------- /images/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/HEAD/images/screenshot.gif -------------------------------------------------------------------------------- /images/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/HEAD/images/screenshots.png -------------------------------------------------------------------------------- /images/language-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/HEAD/images/language-mode.png -------------------------------------------------------------------------------- /server/lib/OSSREADME.json: -------------------------------------------------------------------------------- 1 | // ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS: 2 | [{ 3 | "name": "definitelytyped", 4 | "repositoryURL": "https://github.com/DefinitelyTyped/DefinitelyTyped", 5 | "license": "MIT" 6 | }] -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./out", 6 | "noUnusedLocals": true, 7 | "lib": [ 8 | "es5", "es2015.promise" 9 | ], 10 | "typeRoots": [ 11 | "node_modules/@types" 12 | ], 13 | }, 14 | "exclude": [ 15 | "**/jquery.d.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "type": "shell", 4 | "command": "npm", 5 | // run the custom script "compile" as defined in package.json 6 | "args": [ 7 | "run", 8 | "compile", 9 | "--loglevel", 10 | "silent" 11 | ], 12 | "isBackground": true, 13 | // The tsc compiler is started in watching mode 14 | "problemMatcher": "$tsc-watch" 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "typeRoots": [ 10 | "node_modules/@types" 11 | ], 12 | "sourceMap": false, 13 | "rootDir": ".", 14 | "skipLibCheck": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "exclude": [ 18 | "server" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/**/* 2 | .gitignore 3 | tests/**/* 4 | .vscode-test/** 5 | out/test/** 6 | out/src/** 7 | test/** 8 | src/** 9 | **/*.map 10 | tsconfig.json 11 | vsc-extension-quickstart.md 12 | server/src/** 13 | server/lib/** 14 | server/*.json 15 | server/node_modules/.bin/ 16 | server/node_modules/@types/ 17 | server/node_modules/**/docs/ 18 | server/node_modules/**/*.md 19 | server/node_modules/**/*.txt 20 | server/node_modules/**/*.editorconfig 21 | webpack.config.js 22 | node_modules/** 23 | **/*.ts 24 | **/tsconfig.json 25 | -------------------------------------------------------------------------------- /blade.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "blockComment": [ "{{--", "--}}" ] 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["'", "'"] 16 | ], 17 | "surroundingPairs": [ 18 | ["{", "}"], 19 | ["[", "]"], 20 | ["(", ")"], 21 | ["\"", "\""], 22 | ["'", "'"] 23 | ] 24 | } -------------------------------------------------------------------------------- /server/src/utils/arrays.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | export function pushAll(to: T[], from: T[]) { 8 | if (from) { 9 | for (var i = 0; i < from.length; i++) { 10 | to.push(from[i]); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /snippets/livewire.json: -------------------------------------------------------------------------------- 1 | { 2 | "livewireStyles": { 3 | "prefix": "livewire:styles", 4 | "body": "@livewireStyles", 5 | "description": "Livewire Styles directive" 6 | }, 7 | "livewireScripts": { 8 | "prefix": "livewire:scripts", 9 | "body": "@livewireScripts", 10 | "description": "Livewire Scripts directive" 11 | }, 12 | "livewire-component": { 13 | "prefix": "livewire:component", 14 | "body": "@livewire('${1:component}', ['${2:user}' => \\$${3:user}]${4:, key(\\$$3->id)})", 15 | "description": "Livewire nesting components" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "1.0.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "outFiles": [ 14 | "${workspaceRoot}/out/**/*.js" 15 | ], 16 | "preLaunchTask": "npm" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', 10 | entry: './src/extension.ts', 11 | output: { 12 | path: path.resolve(__dirname, 'out'), 13 | filename: 'extension.js', 14 | libraryTarget: 'commonjs2' 15 | }, 16 | devtool: 'source-map', 17 | externals: { 18 | vscode: 'commonjs vscode' 19 | }, 20 | resolve: { 21 | extensions: ['.ts', '.js'] 22 | }, 23 | module: { 24 | noParse: /node_modules\/vscode-languageserver-types\/lib\/umd\/main.js/, 25 | rules: [ 26 | { 27 | test: /\.ts$/, 28 | exclude: /node_modules/, 29 | use: [ 30 | { 31 | loader: 'ts-loader' 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | }; 38 | module.exports = config; -------------------------------------------------------------------------------- /src/services/BladeFormatter.ts: -------------------------------------------------------------------------------- 1 | export class BladeFormatter { 2 | private newLine: string = "\n"; 3 | private indentPattern: string; 4 | 5 | constructor(options?: IBladeFormatterOptions) { 6 | options = options || {}; 7 | 8 | // set default values for options 9 | options.tabSize = options.tabSize || 4; 10 | options.insertSpaces = options.insertSpaces ?? true; 11 | 12 | this.indentPattern = options.insertSpaces ? " ".repeat(options.tabSize) : "\t"; 13 | } 14 | 15 | format(inputText: string): string { 16 | let inComment = false; 17 | let output = inputText; 18 | 19 | // fix #57 url extra space after formatting 20 | output = output.replace(/url\(\"(\s*)/g, "url\(\""); 21 | 22 | // return the formatted input text with trailing white spaces removed 23 | return output.trim(); 24 | } 25 | } 26 | 27 | export interface IBladeFormatterOptions { 28 | insertSpaces?: boolean; 29 | tabSize?: number; 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Winnie Lin 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /server/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | export function formatError(message: string, err: any): string { 8 | if (err instanceof Error) { 9 | let error = err; 10 | return `${message}: ${error.message}\n${error.stack}`; 11 | } else if (typeof err === 'string') { 12 | return `${message}: ${err}`; 13 | } else if (err) { 14 | return `${message}: ${err.toString()}`; 15 | } 16 | return message; 17 | } 18 | 19 | export function runSafe(func: () => Thenable | T, errorVal: T, errorMessage: string): Thenable | T { 20 | try { 21 | let t = func(); 22 | if (t instanceof Promise) { 23 | return t.then(void 0, e => { 24 | console.error(formatError(errorMessage, e)); 25 | return errorVal; 26 | }); 27 | } 28 | return t; 29 | } catch (e) { 30 | console.error(formatError(errorMessage, e)); 31 | return errorVal; 32 | } 33 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-html-languageserver", 3 | "description": "HTML language server", 4 | "version": "1.0.0", 5 | "author": "Microsoft Corporation", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "dependencies": { 11 | "typescript": "^2.7.1", 12 | "vscode-css-languageservice": "^4.0.2", 13 | "vscode-html-languageservice": "^3.0.2", 14 | "vscode-emmet-helper": "^1.2.15", 15 | "vscode-languageserver": "^4.0.0-next.4", 16 | "vscode-languageserver-types": "^3.14.0", 17 | "vscode-nls": "^4.1.1", 18 | "vscode-uri": "^1.0.6" 19 | }, 20 | "devDependencies": { 21 | "@types/mocha": "2.2.33", 22 | "@types/node": "7.0.43" 23 | }, 24 | "scripts": { 25 | "compile": "gulp compile-extension:html-server", 26 | "watch": "gulp watch-extension:html-server", 27 | "install-service-next": "yarn add vscode-css-languageservice@next && yarn add vscode-html-languageservice@next", 28 | "install-service-local": "npm install ../../../../vscode-css-languageservice -f && npm install ../../../../vscode-html-languageservice -f", 29 | "install-server-next": "yarn add vscode-languageserver@next", 30 | "install-server-local": "npm install ../../../../vscode-languageserver-node/server -f" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/utils/edits.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { TextDocument, TextEdit, Position } from 'vscode-languageserver-types'; 8 | 9 | export function applyEdits(document: TextDocument, edits: TextEdit[]): string { 10 | let text = document.getText(); 11 | let sortedEdits = edits.sort((a, b) => { 12 | let startDiff = comparePositions(a.range.start, b.range.start); 13 | if (startDiff === 0) { 14 | return comparePositions(a.range.end, b.range.end); 15 | } 16 | return startDiff; 17 | }); 18 | sortedEdits.forEach(e => { 19 | let startOffset = document.offsetAt(e.range.start); 20 | let endOffset = document.offsetAt(e.range.end); 21 | text = text.substring(0, startOffset) + e.newText + text.substring(endOffset, text.length); 22 | }); 23 | return text; 24 | } 25 | 26 | function comparePositions(p1: Position, p2: Position) { 27 | let diff = p2.line - p1.line; 28 | if (diff === 0) { 29 | return p2.character - p1.character; 30 | } 31 | return diff; 32 | } -------------------------------------------------------------------------------- /server/src/utils/documentContext.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { DocumentContext } from 'vscode-html-languageservice'; 8 | import { endsWith, startsWith } from '../utils/strings'; 9 | import * as url from 'url'; 10 | import { WorkspaceFolder } from 'vscode-languageserver-protocol/lib/protocol.workspaceFolders.proposed'; 11 | 12 | export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { 13 | function getRootFolder(): string | undefined { 14 | for (let folder of workspaceFolders) { 15 | let folderURI = folder.uri; 16 | if (!endsWith(folderURI, '/')) { 17 | folderURI = folderURI + '/'; 18 | } 19 | if (startsWith(documentUri, folderURI)) { 20 | return folderURI; 21 | } 22 | } 23 | return void 0; 24 | } 25 | 26 | return { 27 | resolveReference: (ref, base = documentUri) => { 28 | if (ref[0] === '/') { // resolve absolute path against the current workspace folder 29 | if (startsWith(base, 'file://')) { 30 | let folderUri = getRootFolder(); 31 | if (folderUri) { 32 | return folderUri + ref.substr(1); 33 | } 34 | } 35 | } 36 | return url.resolve(base, ref); 37 | }, 38 | }; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /snippets/helpers.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Paths */ 3 | "Path-elixir": { 4 | "prefix": "lv:elixir", 5 | "body": "{{ elixir('${1:file}') }}", 6 | "description": "(deprecated) elixir path" 7 | }, 8 | "Path-mix": { 9 | "prefix": "lv:mix", 10 | "body": "{{ mix('${1:file}') }}", 11 | "description": "mix path" 12 | }, 13 | /* Strings */ 14 | "String-trans": { 15 | "prefix": "lv:trans", 16 | "body": "{{ trans('$1') }}", 17 | "description": "trans" 18 | }, 19 | /* URLs */ 20 | "URL-action": { 21 | "prefix": "lv:action", 22 | "body": "{{ action('${1:ControllerName}', [${2:'id'=>1}]) }}", 23 | "description": "URL-action" 24 | }, 25 | "URL-secure-asset": { 26 | "prefix": "lv:secure-asset", 27 | "body": "{{ secure_asset('$1', ${2:\\$title}, ${3:\\$attributes=[]}) }}", 28 | "description": "URL-secure-asset" 29 | }, 30 | "URL-url": { 31 | "prefix": "lv:url", 32 | "body": "{{ url('${1:url}', [$2]) }}", 33 | "description": "URL-url" 34 | }, 35 | "URL-asset": { 36 | "prefix": "lv:asset", 37 | "body": "{{ asset('$1') }}", 38 | "description": "URL-asset" 39 | }, 40 | "URL-route": { 41 | "prefix": "lv:route", 42 | "body": "{{ route('${1:routeName}', [${2:'id'=>1}]) }}", 43 | "description": "URL-route" 44 | }, 45 | /* Miscellaneous */ 46 | "Form-csrf-field": { 47 | "prefix": "lv:csrf-field", 48 | "body": "{{ csrf_field() }}", 49 | "description": "CSRF hidden field" 50 | }, 51 | "csrf-token": { 52 | "prefix": "lv:csrf-token", 53 | "body": "{{ csrf_token() }}", 54 | "description": "CSRF token" 55 | }, 56 | /* Paginate */ 57 | "Paginate-links": { 58 | "prefix": "lv:pagination-links", 59 | "body": "{{ \\$${1:collection}->links() }}", 60 | "description": "pagination links" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /snippets/blade.json: -------------------------------------------------------------------------------- 1 | { 2 | "Blade-component": { 3 | "prefix": "Blade::component", 4 | "body": "Blade::component('${1:package-name}', ${2:PackageNameComponent}::class);", 5 | "description": "Registering Package Components (AppServiceProvider boot method)" 6 | }, 7 | "Blade-include": { 8 | "prefix": "Blade::include", 9 | "body": "Blade::include('${1:includes.input}', '${2:input}');", 10 | "description": "Aliasing Includes (AppServiceProvider boot method)" 11 | }, 12 | "Blade-if": { 13 | "prefix": "Blade::if", 14 | "body": [ 15 | "Blade::if('${1:env}', function ($${2:environment}) {", 16 | " ${3:return app()->environment($$environment);}", 17 | "});" 18 | ], 19 | "description": "Custom If Statements (AppServiceProvider boot method)" 20 | }, 21 | "Blade-directive": { 22 | "prefix": "Blade::directive", 23 | "body": [ 24 | "Blade::directive('${1:datetime}', function ($${2:expression}) {", 25 | " ${3:return \"format('m/d/Y H:i'); ?>\";}", 26 | "});" 27 | ], 28 | "description": "Custom directive (AppServiceProvider boot method)" 29 | }, 30 | "Blade-stringable": { 31 | "prefix": "Blade::stringable", 32 | "body": [ 33 | "Blade::stringable(function (${1:Money} $${2:money}) {", 34 | " ${3:return $$money->formatTo('en_GB');}", 35 | "});" 36 | ], 37 | "description": "Custom echo handlers (AppServiceProvider boot method)" 38 | }, 39 | "Blade-render": { 40 | "prefix": "Blade::render", 41 | "body": "Blade::render(${1:'Blade template string'}, ${2:\\$data});", 42 | "description": "Transform a raw Blade template string into valid HTML (Laravel 9.x)" 43 | }, 44 | "Blade-renderComponent": { 45 | "prefix": "Blade::renderComponent", 46 | "body": "Blade::renderComponent(new ${1:HelloComponent}(${2:\\$params}));", 47 | "description": "Render a given class component by passing the component instance to the method (Laravel 9.x)" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/providers/BladeFormattingEditProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as html from 'vscode-html-languageservice'; 3 | import * as lst from 'vscode-languageserver-textdocument'; 4 | import { BladeFormatter, IBladeFormatterOptions } from "../services/BladeFormatter"; 5 | 6 | const service = html.getLanguageService() 7 | 8 | export class BladeFormattingEditProvider implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider 9 | { 10 | formatterOptions: IBladeFormatterOptions; 11 | 12 | provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions): vscode.TextEdit[] { 13 | let range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position((document.lineCount - 1), Number.MAX_VALUE)); 14 | return this.provideFormattingEdits(document, document.validateRange(range), options); 15 | } 16 | 17 | provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions): vscode.TextEdit[] { 18 | return this.provideFormattingEdits(document, range, options); 19 | } 20 | 21 | private provideFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions): vscode.TextEdit[] { 22 | 23 | this.formatterOptions = { 24 | tabSize: options.tabSize, 25 | insertSpaces: options.insertSpaces 26 | }; 27 | 28 | // Mapping HTML format options 29 | let htmlFormatConfig = vscode.workspace.getConfiguration('html.format'); 30 | Object.assign(options, htmlFormatConfig); 31 | 32 | // format as html 33 | let doc = lst.TextDocument.create(document.uri.fsPath, 'html', 1, document.getText()); 34 | let htmlTextEdit = service.format(doc, range, options); 35 | 36 | // format as blade 37 | let formatter = new BladeFormatter(this.formatterOptions); 38 | let bladeText = formatter.format(htmlTextEdit[0].newText); 39 | 40 | return [vscode.TextEdit.replace(range, bladeText)]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number, length: number } { 8 | let lineStart = offset; 9 | while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) { 10 | lineStart--; 11 | } 12 | let offsetInLine = offset - lineStart; 13 | let lineText = text.substr(lineStart); 14 | 15 | // make a copy of the regex as to not keep the state 16 | let flags = wordDefinition.ignoreCase ? 'gi' : 'g'; 17 | wordDefinition = new RegExp(wordDefinition.source, flags); 18 | 19 | let match = wordDefinition.exec(lineText); 20 | while (match && match.index + match[0].length < offsetInLine) { 21 | match = wordDefinition.exec(lineText); 22 | } 23 | if (match && match.index <= offsetInLine) { 24 | return { start: match.index + lineStart, length: match[0].length }; 25 | } 26 | 27 | return { start: offset, length: 0 }; 28 | } 29 | 30 | export function startsWith(haystack: string, needle: string): boolean { 31 | if (haystack.length < needle.length) { 32 | return false; 33 | } 34 | 35 | for (let i = 0; i < needle.length; i++) { 36 | if (haystack[i] !== needle[i]) { 37 | return false; 38 | } 39 | } 40 | 41 | return true; 42 | } 43 | 44 | export function endsWith(haystack: string, needle: string): boolean { 45 | let diff = haystack.length - needle.length; 46 | if (diff > 0) { 47 | return haystack.indexOf(needle, diff) === diff; 48 | } else if (diff === 0) { 49 | return haystack === needle; 50 | } else { 51 | return false; 52 | } 53 | } 54 | 55 | export function repeat(value: string, count: number) { 56 | var s = ''; 57 | while (count > 0) { 58 | if ((count & 1) === 1) { 59 | s += value; 60 | } 61 | value += value; 62 | count = count >>> 1; 63 | } 64 | return s; 65 | } 66 | 67 | export function isWhitespaceOnly(str: string) { 68 | return /^\s*$/.test(str); 69 | } 70 | 71 | export function isEOL(content: string, offset: number) { 72 | return isNewlineCharacter(content.charCodeAt(offset)); 73 | } 74 | 75 | const CR = '\r'.charCodeAt(0); 76 | const NL = '\n'.charCodeAt(0); 77 | export function isNewlineCharacter(charCode: number) { 78 | return charCode === CR || charCode === NL; 79 | } -------------------------------------------------------------------------------- /server/src/languageModelCache.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { TextDocument } from 'vscode-languageserver'; 8 | 9 | export interface LanguageModelCache { 10 | get(document: TextDocument): T; 11 | onDocumentRemoved(document: TextDocument): void; 12 | dispose(): void; 13 | } 14 | 15 | export function getLanguageModelCache(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache { 16 | let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {}; 17 | let nModels = 0; 18 | 19 | let cleanupInterval: NodeJS.Timer | undefined = void 0; 20 | if (cleanupIntervalTimeInSec > 0) { 21 | cleanupInterval = setInterval(() => { 22 | let cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; 23 | let uris = Object.keys(languageModels); 24 | for (let uri of uris) { 25 | let languageModelInfo = languageModels[uri]; 26 | if (languageModelInfo.cTime < cutoffTime) { 27 | delete languageModels[uri]; 28 | nModels--; 29 | } 30 | } 31 | }, cleanupIntervalTimeInSec * 1000); 32 | } 33 | 34 | return { 35 | get(document: TextDocument): T { 36 | let version = document.version; 37 | let languageId = document.languageId; 38 | let languageModelInfo = languageModels[document.uri]; 39 | if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) { 40 | languageModelInfo.cTime = Date.now(); 41 | return languageModelInfo.languageModel; 42 | } 43 | let languageModel = parse(document); 44 | languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }; 45 | if (!languageModelInfo) { 46 | nModels++; 47 | } 48 | 49 | if (nModels === maxEntries) { 50 | let oldestTime = Number.MAX_VALUE; 51 | let oldestUri = null; 52 | for (let uri in languageModels) { 53 | let languageModelInfo = languageModels[uri]; 54 | if (languageModelInfo.cTime < oldestTime) { 55 | oldestUri = uri; 56 | oldestTime = languageModelInfo.cTime; 57 | } 58 | } 59 | if (oldestUri) { 60 | delete languageModels[oldestUri]; 61 | nModels--; 62 | } 63 | } 64 | return languageModel; 65 | 66 | }, 67 | onDocumentRemoved(document: TextDocument) { 68 | let uri = document.uri; 69 | if (languageModels[uri]) { 70 | delete languageModels[uri]; 71 | nModels--; 72 | } 73 | }, 74 | dispose() { 75 | if (typeof cleanupInterval !== 'undefined') { 76 | clearInterval(cleanupInterval); 77 | cleanupInterval = void 0; 78 | languageModels = {}; 79 | nModels = 0; 80 | } 81 | } 82 | }; 83 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-blade", 3 | "displayName": "Laravel Blade Snippets", 4 | "description": "Laravel blade snippets and syntax highlight support", 5 | "version": "1.37.0", 6 | "publisher": "onecentlin", 7 | "author": { 8 | "name": "Winnie Lin", 9 | "email": "onecentlin@gmail.com", 10 | "url": "https://devmanna.blogspot.com" 11 | }, 12 | "homepage": "https://github.com/onecentlin/laravel-blade-snippets-vscode", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/onecentlin/laravel-blade-snippets-vscode" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/onecentlin/laravel-blade-snippets-vscode/issues" 19 | }, 20 | "engines": { 21 | "vscode": "^1.75.0" 22 | }, 23 | "keywords": [ 24 | "laravel", 25 | "blade", 26 | "template", 27 | "snippet", 28 | "formatter" 29 | ], 30 | "icon": "images/icon.png", 31 | "galleryBanner": { 32 | "color": "#f66f62", 33 | "theme": "dark" 34 | }, 35 | "categories": [ 36 | "Programming Languages", 37 | "Snippets", 38 | "Formatters" 39 | ], 40 | "main": "./out/extension.js", 41 | "scripts": { 42 | "build-srv": "cd ./server && npm install && tsc -p ./", 43 | "compile": "tsc -watch -p ./", 44 | "vscode:prepublish": "webpack --mode production && pushd \"./\" && npm run build-srv && popd", 45 | "webpack": "webpack --mode development", 46 | "webpack-dev": "webpack --mode development --watch" 47 | }, 48 | "contributes": { 49 | "languages": [ 50 | { 51 | "id": "blade", 52 | "aliases": [ 53 | "Blade", 54 | "blade" 55 | ], 56 | "extensions": [ 57 | ".blade.php" 58 | ], 59 | "configuration": "./blade.configuration.json" 60 | } 61 | ], 62 | "grammars": [ 63 | { 64 | "language": "blade", 65 | "scopeName": "text.html.php.blade", 66 | "path": "./syntaxes/blade.tmLanguage.json", 67 | "embeddedLanguages": { 68 | "source.php": "php", 69 | "source.css": "css", 70 | "source.js": "javascript" 71 | } 72 | } 73 | ], 74 | "snippets": [ 75 | { 76 | "language": "blade", 77 | "path": "./snippets/snippets.json" 78 | }, 79 | { 80 | "language": "blade", 81 | "path": "./snippets/helpers.json" 82 | }, 83 | { 84 | "language": "blade", 85 | "path": "./snippets/livewire.json" 86 | }, 87 | { 88 | "language": "php", 89 | "path": "./snippets/blade.json" 90 | } 91 | ], 92 | "configuration": { 93 | "title": "Blade Configuration", 94 | "properties": { 95 | "blade.format.enable": { 96 | "type": "boolean", 97 | "default": false, 98 | "description": "Enable format blade file" 99 | } 100 | } 101 | } 102 | }, 103 | "devDependencies": { 104 | "@types/node": "^16.18.16", 105 | "@types/vscode": "^1.75.0", 106 | "ts-loader": "^9.5.2", 107 | "typescript": "^5.8.2", 108 | "webpack": "^5.98.0", 109 | "webpack-cli": "^5.0.1" 110 | }, 111 | "dependencies": { 112 | "vscode-css-languageservice": "^6.3.2", 113 | "vscode-html-languageservice": "^4.2.5", 114 | "vscode-languageclient": "^6.1.4", 115 | "vscode-languageserver-types": "^3.17.5" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /server/src/modes/cssMode.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; 8 | import { TextDocument, Position, Range } from 'vscode-languageserver-types'; 9 | import { getCSSLanguageService, Stylesheet, ICompletionParticipant } from 'vscode-css-languageservice'; 10 | import { LanguageMode, Settings } from './languageModes'; 11 | import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; 12 | import { Color } from 'vscode-languageserver-protocol/lib/protocol.colorProvider.proposed'; 13 | 14 | export function getCSSMode(documentRegions: LanguageModelCache): LanguageMode { 15 | let cssLanguageService = getCSSLanguageService(); 16 | let embeddedCSSDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css')); 17 | let cssStylesheets = getLanguageModelCache(10, 60, document => cssLanguageService.parseStylesheet(document)); 18 | 19 | return { 20 | getId() { 21 | return 'css'; 22 | }, 23 | configure(options: any) { 24 | cssLanguageService.configure(options && options.css); 25 | }, 26 | doValidation(document: TextDocument, settings?: Settings) { 27 | let embedded = embeddedCSSDocuments.get(document); 28 | return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css); 29 | }, 30 | doComplete(document: TextDocument, position: Position) { 31 | let embedded = embeddedCSSDocuments.get(document); 32 | return cssLanguageService.doComplete(embedded, position, cssStylesheets.get(embedded)); 33 | }, 34 | setCompletionParticipants(registeredCompletionParticipants: ICompletionParticipant[]) { 35 | cssLanguageService.setCompletionParticipants(registeredCompletionParticipants); 36 | }, 37 | doHover(document: TextDocument, position: Position) { 38 | let embedded = embeddedCSSDocuments.get(document); 39 | return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded)); 40 | }, 41 | findDocumentHighlight(document: TextDocument, position: Position) { 42 | let embedded = embeddedCSSDocuments.get(document); 43 | return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded)); 44 | }, 45 | findDocumentSymbols(document: TextDocument) { 46 | let embedded = embeddedCSSDocuments.get(document); 47 | return cssLanguageService.findDocumentSymbols(embedded, cssStylesheets.get(embedded)).filter(s => s.name !== CSS_STYLE_RULE); 48 | }, 49 | findDefinition(document: TextDocument, position: Position) { 50 | let embedded = embeddedCSSDocuments.get(document); 51 | return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded)); 52 | }, 53 | findReferences(document: TextDocument, position: Position) { 54 | let embedded = embeddedCSSDocuments.get(document); 55 | return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded)); 56 | }, 57 | findDocumentColors(document: TextDocument) { 58 | let embedded = embeddedCSSDocuments.get(document); 59 | return cssLanguageService.findDocumentColors(embedded, cssStylesheets.get(embedded)); 60 | }, 61 | getColorPresentations(document: TextDocument, color: Color, range: Range) { 62 | let embedded = embeddedCSSDocuments.get(document); 63 | return cssLanguageService.getColorPresentations(embedded, cssStylesheets.get(embedded), color, range); 64 | }, 65 | onDocumentRemoved(document: TextDocument) { 66 | embeddedCSSDocuments.onDocumentRemoved(document); 67 | cssStylesheets.onDocumentRemoved(document); 68 | }, 69 | dispose() { 70 | embeddedCSSDocuments.dispose(); 71 | cssStylesheets.dispose(); 72 | } 73 | }; 74 | } -------------------------------------------------------------------------------- /server/src/modes/formatting.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { applyEdits } from '../utils/edits'; 8 | import { TextDocument, Range, TextEdit, FormattingOptions, Position } from 'vscode-languageserver-types'; 9 | import { LanguageModes, Settings, LanguageModeRange } from './languageModes'; 10 | import { pushAll } from '../utils/arrays'; 11 | import { isEOL } from '../utils/strings'; 12 | 13 | export function format(languageModes: LanguageModes, document: TextDocument, formatRange: Range, formattingOptions: FormattingOptions, settings: Settings | undefined, enabledModes: { [mode: string]: boolean }) { 14 | let result: TextEdit[] = []; 15 | 16 | let endPos = formatRange.end; 17 | let endOffset = document.offsetAt(endPos); 18 | let content = document.getText(); 19 | if (endPos.character === 0 && endPos.line > 0 && endOffset !== content.length) { 20 | // if selection ends after a new line, exclude that new line 21 | let prevLineStart = document.offsetAt(Position.create(endPos.line - 1, 0)); 22 | while (isEOL(content, endOffset - 1) && endOffset > prevLineStart) { 23 | endOffset--; 24 | } 25 | formatRange = Range.create(formatRange.start, document.positionAt(endOffset)); 26 | } 27 | 28 | 29 | // run the html formatter on the full range and pass the result content to the embedded formatters. 30 | // from the final content create a single edit 31 | // advantages of this approach are 32 | // - correct indents in the html document 33 | // - correct initial indent for embedded formatters 34 | // - no worrying of overlapping edits 35 | 36 | // make sure we start in html 37 | let allRanges = languageModes.getModesInRange(document, formatRange); 38 | let i = 0; 39 | let startPos = formatRange.start; 40 | let isHTML = (range: LanguageModeRange) => range.mode && range.mode.getId() === 'html'; 41 | 42 | while (i < allRanges.length && !isHTML(allRanges[i])) { 43 | let range = allRanges[i]; 44 | if (!range.attributeValue && range.mode && range.mode.format) { 45 | let edits = range.mode.format(document, Range.create(startPos, range.end), formattingOptions, settings); 46 | pushAll(result, edits); 47 | } 48 | startPos = range.end; 49 | i++; 50 | } 51 | if (i === allRanges.length) { 52 | return result; 53 | } 54 | // modify the range 55 | formatRange = Range.create(startPos, formatRange.end); 56 | 57 | // perform a html format and apply changes to a new document 58 | let htmlMode = languageModes.getMode('html')!; 59 | let htmlEdits = htmlMode.format!(document, formatRange, formattingOptions, settings); 60 | let htmlFormattedContent = applyEdits(document, htmlEdits); 61 | let newDocument = TextDocument.create(document.uri + '.tmp', document.languageId, document.version, htmlFormattedContent); 62 | try { 63 | // run embedded formatters on html formatted content: - formatters see correct initial indent 64 | let afterFormatRangeLength = document.getText().length - document.offsetAt(formatRange.end); // length of unchanged content after replace range 65 | let newFormatRange = Range.create(formatRange.start, newDocument.positionAt(htmlFormattedContent.length - afterFormatRangeLength)); 66 | let embeddedRanges = languageModes.getModesInRange(newDocument, newFormatRange); 67 | 68 | let embeddedEdits: TextEdit[] = []; 69 | 70 | for (let r of embeddedRanges) { 71 | let mode = r.mode; 72 | if (mode && mode.format && enabledModes[mode.getId()] && !r.attributeValue) { 73 | let edits = mode.format(newDocument, r, formattingOptions, settings); 74 | for (let edit of edits) { 75 | embeddedEdits.push(edit); 76 | } 77 | } 78 | } 79 | 80 | if (embeddedEdits.length === 0) { 81 | pushAll(result, htmlEdits); 82 | return result; 83 | } 84 | 85 | // apply all embedded format edits and create a single edit for all changes 86 | let resultContent = applyEdits(newDocument, embeddedEdits); 87 | let resultReplaceText = resultContent.substring(document.offsetAt(formatRange.start), resultContent.length - afterFormatRangeLength); 88 | 89 | result.push(TextEdit.replace(formatRange, resultReplaceText)); 90 | return result; 91 | } finally { 92 | languageModes.onDocumentRemoved(newDocument); 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import * as html from 'vscode-html-languageservice'; 4 | import * as lst from 'vscode-languageserver-types'; 5 | import * as nls from 'vscode-nls'; 6 | import { BladeFormattingEditProvider } from './providers/BladeFormattingEditProvider'; 7 | import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams } from 'vscode-languageclient'; 8 | 9 | const service = html.getLanguageService() 10 | const localize = nls.loadMessageBundle(); 11 | 12 | class DocumentHighlight implements vscode.DocumentHighlightProvider 13 | { 14 | provideDocumentHighlights(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DocumentHighlight[] | Thenable { 15 | let doc = lst.TextDocument.create(document.uri.fsPath, 'html', 1, document.getText()); 16 | return (service.findDocumentHighlights(doc, position, service.parseHTMLDocument(doc)) as any); 17 | } 18 | } // DocumentHighlight 19 | 20 | export function activate(context: vscode.ExtensionContext) { 21 | 22 | let documentSelector: vscode.DocumentSelector = { 23 | language: 'blade' 24 | }; 25 | 26 | context.subscriptions.push(vscode.languages.registerDocumentHighlightProvider(documentSelector, new DocumentHighlight)); 27 | 28 | let bladeFormatCfg = vscode.workspace.getConfiguration('blade.format'); 29 | if (bladeFormatCfg.get('enable')) { 30 | context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(documentSelector, new BladeFormattingEditProvider)); 31 | context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new BladeFormattingEditProvider)); 32 | } 33 | 34 | // Set html indent 35 | const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']; 36 | vscode.languages.setLanguageConfiguration('blade', { 37 | indentationRules: { 38 | increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|)|\{[^}"']*$/, 39 | decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/ 40 | }, 41 | wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, 42 | onEnterRules: [ 43 | { 44 | beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), 45 | afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i, 46 | action: { indentAction: vscode.IndentAction.IndentOutdent } 47 | }, 48 | { 49 | beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), 50 | action: { indentAction: vscode.IndentAction.Indent } 51 | } 52 | ], 53 | }); 54 | 55 | // The server is implemented in node 56 | let serverModule = context.asAbsolutePath(path.join('server', 'out', 'htmlServerMain.js')); 57 | // The debug options for the server 58 | let debugOptions = { execArgv: ['--nolazy', '--inspect=6045'] }; 59 | 60 | // If the extension is launch in debug mode the debug server options are use 61 | // Otherwise the run options are used 62 | let serverOptions: ServerOptions = { 63 | run: { module: serverModule, transport: TransportKind.ipc }, 64 | debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } 65 | }; 66 | 67 | let embeddedLanguages = { css: true, javascript: true }; 68 | // Options to control the language client 69 | let clientOptions: LanguageClientOptions = { 70 | documentSelector: ['blade'], 71 | synchronize: { 72 | configurationSection: ['blade', 'css', 'javascript', 'emmet'], // the settings to synchronize 73 | }, 74 | initializationOptions: { 75 | embeddedLanguages 76 | } 77 | }; 78 | 79 | // Create the language client and start the client. 80 | let client = new LanguageClient('blade', localize('bladeserver.name', 'BLADE Language Server'), serverOptions, clientOptions); 81 | client.registerProposedFeatures(); 82 | context.subscriptions.push(client.start()); 83 | } 84 | 85 | export function deactivate() { 86 | 87 | } 88 | -------------------------------------------------------------------------------- /server/src/modes/htmlMode.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { getLanguageModelCache } from '../languageModelCache'; 8 | import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions, HTMLFormatConfiguration, TokenType } from 'vscode-html-languageservice'; 9 | import { TextDocument, Position, Range } from 'vscode-languageserver-types'; 10 | import { LanguageMode, Settings } from './languageModes'; 11 | 12 | export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageMode { 13 | let globalSettings: Settings = {}; 14 | let htmlDocuments = getLanguageModelCache(10, 60, document => htmlLanguageService.parseHTMLDocument(document)); 15 | let completionParticipants = []; 16 | return { 17 | getId() { 18 | return 'html'; 19 | }, 20 | configure(options: any) { 21 | globalSettings = options; 22 | }, 23 | doComplete(document: TextDocument, position: Position, settings: Settings = globalSettings) { 24 | let options = settings && settings.html && settings.html.suggest; 25 | let doAutoComplete = settings && settings.html && settings.html.autoClosingTags; 26 | if (doAutoComplete) { 27 | options.hideAutoCompleteProposals = true; 28 | } 29 | 30 | const htmlDocument = htmlDocuments.get(document); 31 | const offset = document.offsetAt(position); 32 | const node = htmlDocument.findNodeBefore(offset); 33 | const scanner = htmlLanguageService.createScanner(document.getText(), node.start); 34 | let token = scanner.scan(); 35 | while (token !== TokenType.EOS && scanner.getTokenOffset() <= offset) { 36 | if (token === TokenType.Content && offset <= scanner.getTokenEnd()) { 37 | completionParticipants.forEach(participant => { if (participant.onHtmlContent) { participant.onHtmlContent(); } }); 38 | break; 39 | } 40 | token = scanner.scan(); 41 | } 42 | return htmlLanguageService.doComplete(document, position, htmlDocument, options); 43 | }, 44 | setCompletionParticipants(registeredCompletionParticipants: any[]) { 45 | completionParticipants = registeredCompletionParticipants; 46 | }, 47 | doHover(document: TextDocument, position: Position) { 48 | return htmlLanguageService.doHover(document, position, htmlDocuments.get(document)); 49 | }, 50 | findDocumentHighlight(document: TextDocument, position: Position) { 51 | return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document)); 52 | }, 53 | findDocumentLinks(document: TextDocument, documentContext: DocumentContext) { 54 | return htmlLanguageService.findDocumentLinks(document, documentContext); 55 | }, 56 | findDocumentSymbols(document: TextDocument) { 57 | return htmlLanguageService.findDocumentSymbols(document, htmlDocuments.get(document)); 58 | }, 59 | format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings) { 60 | let formatSettings: HTMLFormatConfiguration = settings && settings.html && settings.html.format; 61 | if (formatSettings) { 62 | formatSettings = merge(formatSettings, {}); 63 | } else { 64 | formatSettings = {}; 65 | } 66 | if (formatSettings.contentUnformatted) { 67 | formatSettings.contentUnformatted = formatSettings.contentUnformatted + ',script'; 68 | } else { 69 | formatSettings.contentUnformatted = 'script'; 70 | } 71 | formatSettings = merge(formatParams, formatSettings); 72 | return htmlLanguageService.format(document, range, formatSettings); 73 | }, 74 | doAutoClose(document: TextDocument, position: Position) { 75 | let offset = document.offsetAt(position); 76 | let text = document.getText(); 77 | if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) { 78 | return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document)); 79 | } 80 | return null; 81 | }, 82 | onDocumentRemoved(document: TextDocument) { 83 | htmlDocuments.onDocumentRemoved(document); 84 | }, 85 | dispose() { 86 | htmlDocuments.dispose(); 87 | } 88 | }; 89 | } 90 | 91 | function merge(src: any, dst: any): any { 92 | for (var key in src) { 93 | if (src.hasOwnProperty(key)) { 94 | dst[key] = src[key]; 95 | } 96 | } 97 | return dst; 98 | } 99 | -------------------------------------------------------------------------------- /server/src/modes/languageModes.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { getLanguageService as getHTMLLanguageService, DocumentContext } from 'vscode-html-languageservice'; 8 | import { 9 | CompletionItem, Location, SignatureHelp, Definition, TextEdit, TextDocument, Diagnostic, DocumentLink, Range, 10 | Hover, DocumentHighlight, CompletionList, Position, FormattingOptions, SymbolInformation 11 | } from 'vscode-languageserver-types'; 12 | 13 | import { ColorInformation, ColorPresentation, Color } from 'vscode-languageserver-protocol/lib/protocol.colorProvider.proposed'; 14 | 15 | import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache'; 16 | import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; 17 | import { getCSSMode } from './cssMode'; 18 | import { getJavascriptMode } from './javascriptMode'; 19 | import { getHTMLMode } from './htmlMode'; 20 | 21 | export { ColorInformation, ColorPresentation, Color }; 22 | 23 | export interface Settings { 24 | css?: any; 25 | html?: any; 26 | javascript?: any; 27 | emmet?: { [key: string]: any }; 28 | } 29 | 30 | export interface SettingProvider { 31 | getDocumentSettings(textDocument: TextDocument): Thenable; 32 | } 33 | 34 | export interface LanguageMode { 35 | getId(): string; 36 | configure?: (options: Settings) => void; 37 | doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[]; 38 | doComplete?: (document: TextDocument, position: Position, settings?: Settings) => CompletionList | null; 39 | setCompletionParticipants?: (registeredCompletionParticipants: any[]) => void; 40 | doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem | null; 41 | doHover?: (document: TextDocument, position: Position) => Hover | null; 42 | doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null; 43 | findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[]; 44 | findDocumentSymbols?: (document: TextDocument) => SymbolInformation[]; 45 | findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[]; 46 | findDefinition?: (document: TextDocument, position: Position) => Definition | null; 47 | findReferences?: (document: TextDocument, position: Position) => Location[]; 48 | format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => TextEdit[]; 49 | findDocumentColors?: (document: TextDocument) => ColorInformation[]; 50 | getColorPresentations?: (document: TextDocument, color: Color, range: Range) => ColorPresentation[]; 51 | doAutoClose?: (document: TextDocument, position: Position) => string | null; 52 | onDocumentRemoved(document: TextDocument): void; 53 | dispose(): void; 54 | } 55 | 56 | export interface LanguageModes { 57 | getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined; 58 | getModesInRange(document: TextDocument, range: Range): LanguageModeRange[]; 59 | getAllModes(): LanguageMode[]; 60 | getAllModesInDocument(document: TextDocument): LanguageMode[]; 61 | getMode(languageId: string): LanguageMode | undefined; 62 | onDocumentRemoved(document: TextDocument): void; 63 | dispose(): void; 64 | } 65 | 66 | export interface LanguageModeRange extends Range { 67 | mode: LanguageMode | undefined; 68 | attributeValue?: boolean; 69 | } 70 | 71 | export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }): LanguageModes { 72 | 73 | var htmlLanguageService = getHTMLLanguageService(); 74 | let documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(htmlLanguageService, document)); 75 | 76 | let modelCaches: LanguageModelCache[] = []; 77 | modelCaches.push(documentRegions); 78 | 79 | let modes = Object.create(null); 80 | modes['html'] = getHTMLMode(htmlLanguageService); 81 | if (supportedLanguages['css']) { 82 | modes['css'] = getCSSMode(documentRegions); 83 | } 84 | if (supportedLanguages['javascript']) { 85 | modes['javascript'] = getJavascriptMode(documentRegions); 86 | } 87 | return { 88 | getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined { 89 | let languageId = documentRegions.get(document).getLanguageAtPosition(position); 90 | if (languageId) { 91 | return modes[languageId]; 92 | } 93 | return void 0; 94 | }, 95 | getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] { 96 | return documentRegions.get(document).getLanguageRanges(range).map(r => { 97 | return { 98 | start: r.start, 99 | end: r.end, 100 | mode: r.languageId && modes[r.languageId], 101 | attributeValue: r.attributeValue 102 | }; 103 | }); 104 | }, 105 | getAllModesInDocument(document: TextDocument): LanguageMode[] { 106 | let result = []; 107 | for (let languageId of documentRegions.get(document).getLanguagesInDocument()) { 108 | let mode = modes[languageId]; 109 | if (mode) { 110 | result.push(mode); 111 | } 112 | } 113 | return result; 114 | }, 115 | getAllModes(): LanguageMode[] { 116 | let result = []; 117 | for (let languageId in modes) { 118 | let mode = modes[languageId]; 119 | if (mode) { 120 | result.push(mode); 121 | } 122 | } 123 | return result; 124 | }, 125 | getMode(languageId: string): LanguageMode { 126 | return modes[languageId]; 127 | }, 128 | onDocumentRemoved(document: TextDocument) { 129 | modelCaches.forEach(mc => mc.onDocumentRemoved(document)); 130 | for (let mode in modes) { 131 | modes[mode].onDocumentRemoved(document); 132 | } 133 | }, 134 | dispose(): void { 135 | modelCaches.forEach(mc => mc.dispose()); 136 | modelCaches = []; 137 | for (let mode in modes) { 138 | modes[mode].dispose(); 139 | } 140 | modes = {}; 141 | } 142 | }; 143 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Laravel Blade Snippets](https://marketplace.visualstudio.com/items?itemName=onecentlin.laravel-blade) 2 | 3 | Laravel blade snippets and syntax highlight support for Visual Studio Code. 4 | 5 | > Suggest Laravel related extension: [Laravel Snippets](https://marketplace.visualstudio.com/items?itemName=onecentlin.laravel5-snippets) 6 | 7 | ## Screenshot 8 | 9 | ![Demo](https://github.com/onecentlin/laravel-blade-snippets-vscode/raw/master/images/screenshot.gif) 10 | 11 | ## User Settings 12 | 13 | Open `Preferences` -> `Settings` 14 | 15 | ```json 16 | "emmet.triggerExpansionOnTab": true, // enable tab to expanse emmet tags 17 | "blade.format.enable": true, // if you would like to enable blade format 18 | ``` 19 | 20 | Specific settings for blade language 21 | 22 | ```json 23 | "[blade]": { 24 | "editor.autoClosingBrackets": "always" 25 | }, 26 | ``` 27 | 28 | ## Features 29 | 30 | - Blade syntax highlight 31 | - Blade snippets 32 | - Emmet works in blade template 33 | - Blade formatting 34 | 35 | ## Blade Syntax Hightlight 36 | 37 | - Auto detected with `.blade.php` extension 38 | - Manually switch language mode to `Blade` (`Ctrl + K, M` or `⌘ + K, M`) 39 | 40 | ## Laravel Blade Snippets 41 | 42 | | Trigger | Snippet | 43 | | ------------------- | ----------------------------------------- | 44 | | b:extends | @extends | 45 | | b:yield | @yield | 46 | | b:section | @section...@endsection | 47 | | b:section-show | @section...@show | 48 | | b:if | @if...@endif | 49 | | b:if-else | @if...@else...@endif | 50 | | b:unless | @unless...@endunless | 51 | | b:has-section | @hasSection...@else...@endif | 52 | | b:for | @for...@endfor | 53 | | b:foreach | @foreach...@endforeach | 54 | | b:forelse | @forelse...@empty...@endforelse | 55 | | b:while | @while...@endwhile | 56 | | b:each | @each | 57 | | b:push | @push...@endpush | 58 | | b:stack | @stack | 59 | | b:inject | @inject | 60 | | b:comment | {{-- comment --}} (`Ctrl + /` or `⌘ + /`) | 61 | | b:echo | {{ $data }} | 62 | | b:echo-html | {!! $html !!} | 63 | | b:echo-raw | @{{ variable }} | 64 | | b:can | @can...@endcan (v5.1) | 65 | | b:can-elsecan | @can...@elsecan...@endcan (v5.1) | 66 | | b:canany | @canany...@endcanany (v5.8) | 67 | | b:canany-elsecanany | @canany...@elsecanany...@endcanany (v5.8) | 68 | | b:cannot | @cannot...@endcannot (v5.3) | 69 | | b:cannot-elsecannot | @cannot...@elsecannot...@endcannot (v5.3) | 70 | | b:verbatim | @verbatim...@endverbatim (v5.3) | 71 | | b:php | @php...@endphp (v5.3) | 72 | | b:includeIf | @includeIf (v5.3) | 73 | | b:includeWhen | @includeWhen (v5.4) | 74 | | b:includeFirst | @includeFirst (v5.5) | 75 | | b:includeUnless | @includeUnless (v6.x) | 76 | | b:component | @component...@endcomponent (v5.4) | 77 | | b:slot | @slot...@endslot (v5.4) | 78 | | b:isset | @isset...@endisset (v5.4) | 79 | | b:empty | @empty...@endempty (v5.4) | 80 | | b:auth | @auth...@endauth (v5.5) | 81 | | b:guest | @guest...@endguest (v5.5) | 82 | | b:switch | @switch...@case...@endswitch (v5.5) | 83 | | b:lang | @lang | 84 | | b:csrf | @csrf (v5.6) | 85 | | b:method | @method(...) (v5.6) | 86 | | b:dump | @dump(...) (v5.6) | 87 | | b:dd | @dd(...) (v5.6) | 88 | | b:prepend | @prepend...@endprepen (v5.6) | 89 | | b:error | @error...@enderror (v5.8) | 90 | | b:props | @props (v7.4) | 91 | | b:production | @production...@endproduction | 92 | | b:env | @env...@endenv | 93 | | b:once | @once...@endonce | 94 | | b:class | @class (v8.51) | 95 | | b:aware | @aware (v8.64) | 96 | | b:js | @js (v8.71) | 97 | | b:checked | @checked (v9.x) | 98 | | b:selected | @selected (v9.x) | 99 | | b:disabled | @disabled (v9.x) | 100 | | b:style | @style (v9.x) | 101 | | b:readonly | @readonly (v9.x) | 102 | | b:required | @required (v9.x) | 103 | | b:pushOnce | @pushOnce...@endPushOnce (v9.x) | 104 | | b:pushIf | @pushIf...@endPushIf (v9.x) | 105 | | b:prependOnce | @prependOnce...@endPrependOnce (v9.x) | 106 | | b:session | @session ... @endsession (v10.x) | 107 | | b:use | @use (v10.x) | 108 | | b:use-alias | @use (v10.x) | 109 | 110 | ### $loop variable (Laravel v5.3+) 111 | 112 | | Trigger | Snippet | 113 | | ------------ | ------------------------------------------------------ | 114 | | b:loop | $loop->(index,remaining,count,first,last,depth,parent) | 115 | | b:loop-first | @if($loop->first)...@endif | 116 | | b:loop-last | @if($loop->last)...@endif | 117 | 118 | ## Laravel Helper Snippets for Blade 119 | 120 | | Trigger | Laravel Helper | 121 | | ------------------- | --------------------- | 122 | | lv:elixir | elixir() - deprecated | 123 | | lv:mix | mix() (v5.4) | 124 | | lv:trans | trans() | 125 | | lv:action | action() | 126 | | lv:secure-asset | secure_asset() | 127 | | lv:url | url() | 128 | | lv:asset | asset() | 129 | | lv:route | route() | 130 | | lv:csrf-field | csrf_field() | 131 | | lv:csrf-token | csrf_token() | 132 | | lv:pagination-links | $collection->links() | 133 | 134 | ## Blade extensions 135 | 136 | Register in the `boot` method of `ServiceProvider` 137 | 138 | - `Blade::component` 139 | - `Blade::include` 140 | - `Blade::if` 141 | - `Blade::directive` 142 | - `Blade::stringable` 143 | 144 | Rendering inline blade templates 145 | 146 | - `Blade::render` 147 | - `Blade::renderComponent` 148 | 149 | ## Contact 150 | 151 | Please file any [issues](https://github.com/onecentlin/laravel-blade-snippets-vscode/issues) or have a suggestion please tweet me [@onecentlin](https://twitter.com/onecentlin). 152 | 153 | ## Credits 154 | 155 | - Blade language grammar is based on [Medalink syntax definition](https://github.com/Medalink/laravel-blade) for Sublime Text; Converted from [Blade templating support in Atom](https://github.com/jawee/language-blade) 156 | - Textmate language format file is based on [Textmate bundle for Laravel 5](https://github.com/loranger/Laravel.tmbundle). 157 | 158 | ## License 159 | 160 | Please read [License](https://github.com/onecentlin/laravel-blade-snippets-vscode/blob/master/LICENSE.md) for more information 161 | -------------------------------------------------------------------------------- /server/src/modes/embeddedSupport.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | 8 | import { TextDocument, Position, LanguageService, TokenType, Range } from 'vscode-html-languageservice'; 9 | 10 | export interface LanguageRange extends Range { 11 | languageId: string | undefined; 12 | attributeValue?: boolean; 13 | } 14 | 15 | export interface HTMLDocumentRegions { 16 | getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument; 17 | getLanguageRanges(range: Range): LanguageRange[]; 18 | getLanguageAtPosition(position: Position): string | undefined; 19 | getLanguagesInDocument(): string[]; 20 | getImportedScripts(): string[]; 21 | } 22 | 23 | export var CSS_STYLE_RULE = '__'; 24 | 25 | interface EmbeddedRegion { languageId: string | undefined; start: number; end: number; attributeValue?: boolean; } 26 | 27 | 28 | export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions { 29 | let regions: EmbeddedRegion[] = []; 30 | let scanner = languageService.createScanner(document.getText()); 31 | let lastTagName: string = ''; 32 | let lastAttributeName: string | null = null; 33 | let languageIdFromType: string | undefined = undefined; 34 | let importedScripts: string[] = []; 35 | 36 | let token = scanner.scan(); 37 | while (token !== TokenType.EOS) { 38 | switch (token) { 39 | case TokenType.StartTag: 40 | lastTagName = scanner.getTokenText(); 41 | lastAttributeName = null; 42 | languageIdFromType = 'javascript'; 43 | break; 44 | case TokenType.Styles: 45 | regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); 46 | break; 47 | case TokenType.Script: 48 | regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); 49 | break; 50 | case TokenType.AttributeName: 51 | lastAttributeName = scanner.getTokenText(); 52 | break; 53 | case TokenType.AttributeValue: 54 | if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') { 55 | let value = scanner.getTokenText(); 56 | if (value[0] === '\'' || value[0] === '"') { 57 | value = value.substr(1, value.length - 1); 58 | } 59 | importedScripts.push(value); 60 | } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') { 61 | if (/["'](module|(text|application)\/(java|ecma)script)["']/.test(scanner.getTokenText())) { 62 | languageIdFromType = 'javascript'; 63 | } else { 64 | languageIdFromType = void 0; 65 | } 66 | } else { 67 | let attributeLanguageId = getAttributeLanguage(lastAttributeName!); 68 | if (attributeLanguageId) { 69 | let start = scanner.getTokenOffset(); 70 | let end = scanner.getTokenEnd(); 71 | let firstChar = document.getText()[start]; 72 | if (firstChar === '\'' || firstChar === '"') { 73 | start++; 74 | end--; 75 | } 76 | regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true }); 77 | } 78 | } 79 | lastAttributeName = null; 80 | break; 81 | } 82 | token = scanner.scan(); 83 | } 84 | return { 85 | getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range), 86 | getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) => getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues), 87 | getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position), 88 | getLanguagesInDocument: () => getLanguagesInDocument(document, regions), 89 | getImportedScripts: () => importedScripts 90 | }; 91 | } 92 | 93 | 94 | function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] { 95 | let result: LanguageRange[] = []; 96 | let currentPos = range ? range.start : Position.create(0, 0); 97 | let currentOffset = range ? document.offsetAt(range.start) : 0; 98 | let endOffset = range ? document.offsetAt(range.end) : document.getText().length; 99 | for (let region of regions) { 100 | if (region.end > currentOffset && region.start < endOffset) { 101 | let start = Math.max(region.start, currentOffset); 102 | let startPos = document.positionAt(start); 103 | if (currentOffset < region.start) { 104 | result.push({ 105 | start: currentPos, 106 | end: startPos, 107 | languageId: 'html' 108 | }); 109 | } 110 | let end = Math.min(region.end, endOffset); 111 | let endPos = document.positionAt(end); 112 | if (end > region.start) { 113 | result.push({ 114 | start: startPos, 115 | end: endPos, 116 | languageId: region.languageId, 117 | attributeValue: region.attributeValue 118 | }); 119 | } 120 | currentOffset = end; 121 | currentPos = endPos; 122 | } 123 | } 124 | if (currentOffset < endOffset) { 125 | let endPos = range ? range.end : document.positionAt(endOffset); 126 | result.push({ 127 | start: currentPos, 128 | end: endPos, 129 | languageId: 'html' 130 | }); 131 | } 132 | return result; 133 | } 134 | 135 | function getLanguagesInDocument(document: TextDocument, regions: EmbeddedRegion[]): string[] { 136 | let result = []; 137 | for (let region of regions) { 138 | if (region.languageId && result.indexOf(region.languageId) === -1) { 139 | result.push(region.languageId); 140 | if (result.length === 3) { 141 | return result; 142 | } 143 | } 144 | } 145 | result.push('html'); 146 | return result; 147 | } 148 | 149 | function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined { 150 | let offset = document.offsetAt(position); 151 | for (let region of regions) { 152 | if (region.start <= offset) { 153 | if (offset <= region.end) { 154 | return region.languageId; 155 | } 156 | } else { 157 | break; 158 | } 159 | } 160 | return 'html'; 161 | } 162 | 163 | function getEmbeddedDocument(document: TextDocument, contents: EmbeddedRegion[], languageId: string, ignoreAttributeValues: boolean): TextDocument { 164 | let currentPos = 0; 165 | let oldContent = document.getText(); 166 | let result = ''; 167 | let lastSuffix = ''; 168 | for (let c of contents) { 169 | if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) { 170 | result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c)); 171 | result += oldContent.substring(c.start, c.end); 172 | currentPos = c.end; 173 | lastSuffix = getSuffix(c); 174 | } 175 | } 176 | result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, ''); 177 | return TextDocument.create(document.uri, languageId, document.version, result); 178 | } 179 | 180 | function getPrefix(c: EmbeddedRegion) { 181 | if (c.attributeValue) { 182 | switch (c.languageId) { 183 | case 'css': return CSS_STYLE_RULE + '{'; 184 | } 185 | } 186 | return ''; 187 | } 188 | function getSuffix(c: EmbeddedRegion) { 189 | if (c.attributeValue) { 190 | switch (c.languageId) { 191 | case 'css': return '}'; 192 | case 'javascript': return ';'; 193 | } 194 | } 195 | return ''; 196 | } 197 | 198 | function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { 199 | let accumulatedWS = 0; 200 | result += before; 201 | for (let i = start + before.length; i < end; i++) { 202 | let ch = oldContent[i]; 203 | if (ch === '\n' || ch === '\r') { 204 | // only write new lines, skip the whitespace 205 | accumulatedWS = 0; 206 | result += ch; 207 | } else { 208 | accumulatedWS++; 209 | } 210 | } 211 | result = append(result, ' ', accumulatedWS - after.length); 212 | result += after; 213 | return result; 214 | } 215 | 216 | function append(result: string, str: string, n: number): string { 217 | while (n > 0) { 218 | if (n & 1) { 219 | result += str; 220 | } 221 | n >>= 1; 222 | str += str; 223 | } 224 | return result; 225 | } 226 | 227 | function getAttributeLanguage(attributeName: string): string | null { 228 | let match = attributeName.match(/^(style)$|^(on\w+)$/i); 229 | if (!match) { 230 | return null; 231 | } 232 | return match[1] ? 'css' : 'javascript'; 233 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.37.0 2 | 3 | - Add `b:use` and `b:use-alias` snippets 4 | - Update package dependencies 5 | 6 | ## 1.36.1 7 | 8 | - Fix `b:session` and `b:prepend` snippets ([@prshendra](https://github.com/prshendra) - PR #180) 9 | 10 | ## 1.36.0 11 | 12 | - Add `@session` directive and `b:session` snippet 13 | - Add `@use` directive 14 | - Add `@vite` directive 15 | 16 | ## 1.35.0 17 | 18 | - Add `@pushIf` directive and `b:pushIf` snippet ([@AbdelrhmanSaid](https://github.com/AbdelrhmanSaid) - PR #174) 19 | - Add `` highlight ([@solicomo](https://github.com/solicomo) - PR #175) 20 | - Enhance BladeFormatter.ts ([@1cbyc](https://github.com/1cbyc) - PR #167) 21 | - Update package dependencies 22 | 23 | ## 1.34.0 24 | 25 | - Add `b:prepend` snippet 26 | - Add `@prependOnce` directive and `b:prependOnce` snippet 27 | - Update syntax supports: prependOnce directive 28 | - Update package dependencies 29 | 30 | ## 1.33.0 31 | 32 | - Add `@dd` directive and `b:dd` snippet ([@BrayanCaro](https://github.com/BrayanCaro) - PR #158) 33 | - Add `@style`, `@readonly`, `@required`, `@pushOnce` directive 34 | - Add `b:style`, `b:readonly`, `b:required`, `b:pushOnce` snippet 35 | - Update syntax supports: style, readonly, required, pushOnce directive 36 | 37 | ## 1.32.0 38 | 39 | - Add `@disabled` directive and `b:disabled` snippet ([@JustinByrne](https://github.com/JustinByrne) - PR #151) 40 | - Add `b:class` snippet (PR #136 and PR #140 - Thanks to [@lakuapik](https://github.com/lakuapik) and [@wilsenhc](https://github.com/wilsenhc)) 41 | 42 | ## 1.31.0 43 | 44 | - Add `b:aware` and `b:js` snippet 45 | - Add `@aware` directive ([Laravel 8.64](https://laravel-news.com/laravel-8-64-0)) 46 | - Add `@js` directive ([Laravel 8.71](https://laravel-news.com/laravel-8-71-0)) 47 | - Update `Blade::render` and `Blade::renderComponent` snippet 48 | 49 | ## 1.30.0 50 | 51 | Add Laravel 9 features 52 | 53 | - Add `b:checked` and `b:selected` snippet 54 | - Add `@checked` and `@selected` directive syntax highlight 55 | - Add `Blade::render` and `Blade::renderComponent` snippet 56 | 57 | ## 1.29.0 58 | 59 | Happy New Year 2022! 60 | 61 | - Add `b:canany` and `b:canany-cananyelse` snippet ([@JustinByrne](https://github.com/JustinByrne) - PR #144) 62 | - Fix snippet 63 | - Update blade syntaxes 64 | - Update packages 65 | 66 | ## 1.28.0 67 | 68 | - Added support attribute expressions syntax highlighting ([@cpof-tea](https://github.com/cpof-tea) - PR #138) 69 | 70 | ## 1.27.0 71 | 72 | - Add `@class` directive syntax highlight 73 | - Update blade syntaxes 74 | - Fix snippet 75 | 76 | ## 1.26.0 77 | 78 | - Add `b:once` snippet ([@lakuapik](https://github.com/lakuapik) - PR #137) 79 | - Add `Blade::stringable` snippet ([@lakuapik](https://github.com/lakuapik) - PR #135) 80 | - Update packages 81 | 82 | ## 1.25.0 83 | 84 | - Add `@once` directive 85 | - Fix #121 @php() highlighting 86 | - Update blade syntaxes 87 | 88 | ## 1.24.0 89 | 90 | - Update blade syntaxes 91 | 92 | ## 1.23.0 93 | 94 | - Add `@livewireStyles`, `@livewireScripts`, `@livewire` directive (v8.x) 95 | - Add `livewire:styles`, `livewire:scripts`, `livewire:component` snippets 96 | - Cleanup snippets 97 | 98 | ## 1.22.0 99 | 100 | - Add `@includeUnless` directive (v6.x) 101 | - Add environment directives: `@production`, `@env` (v7.x) 102 | - Rename language mode using `Blade` instead of `Laravel Blade` 103 | - Enable language feature in blade language mode 104 | - Reduce extension package size 105 | 106 | ## 1.21.0 107 | 108 | - Add `b:error` snippets ([@CaddyDz](https://github.com/CaddyDz) - PR #95) 109 | - Add `b:props` snippets 110 | - Add blade extensions snippets 111 | - `Blade::component` 112 | - `Blade::include` 113 | - `Blade::if` 114 | - `Blade::directive` 115 | 116 | ## 1.20.0 117 | 118 | - Update blade formatter fixed for updated languageservice 119 | 120 | ## 1.19.0 121 | 122 | - Append html format options to html formatter ([@ayatkyo](https://github.com/ayatkyo) - PR #87) 123 | - Update package dependencies 124 | 125 | ## 1.18.0 126 | 127 | - Add `b:csrf`, `b:method`, `b:dump` snipptes ([@HasanAlyazidi](https://github.com/HasanAlyazidi) - PR #60) 128 | - Fix comment with extra spaces (#59) 129 | - Fix formatting issue in url syntax (#57) 130 | - Fix shorthand `@php()` for Roots/Sage WordPress Template with html tag syntax highlight (#53) 131 | 132 | ## 1.17.0 133 | 134 | - Syntax highlighting enhancement 135 | - Add syntax highlighting for class static method 136 | - Add `b:lang` snippet (#52) 137 | 138 | ## 1.16.0 139 | 140 | - Fix tag attributes completition (#24) 141 | - Fix comment issue in `script`, `style`, `php` block with `Ctrl + /` or `⌘ + /` keymap shortcut (#25, #34) 142 | 143 | ## 1.15.0 144 | 145 | - Support Envoy directives: `@setup`, `@servers`, `@task`, `@story`, `@finished`, `@slack` (#41) 146 | 147 | ## 1.14.2 148 | 149 | - Fix error in Blade Language Server (#46) 150 | - Fix extensionPath of undefined (#47) 151 | - Emmet setting changed (#48) 152 | > Settings below for blade is no longer needed. 153 | >```json 154 | >"emmet.includeLanguages": { 155 | > "blade": "html" 156 | >}, 157 | >``` 158 | 159 | ## 1.14.0 160 | 161 | - Fix blade syntax broken with VSCode 1.20.0 release (#42) 162 | - Modify the highlight, add to the style and script autocomplete ([@tiansin](https://github.com/tiansin) - PR #43) 163 | - Fix javascript autocompletion not working in script tag (#39) 164 | - Add `b:unless` snippet 165 | 166 | ## 1.13.0 167 | 168 | - Fix spaces on format (#40) 169 | - Enable format selection (#10) 170 | - Enhance blade format (#32, #36) 171 | 172 | ## 1.12.0 173 | 174 | - Add `blade.format.enable` configuration setting for manual enable blade file format. (#30) 175 | ```json 176 | "blade.format.enable": true, 177 | ``` 178 | - Add `@includeFirst` directive 179 | - Add `b:includeFirst` snippet 180 | - Fix minor syntax issue 181 | 182 | ## 1.11.0 183 | 184 | - Fix indent issue #9, #35 ([@izcream](https://github.com/izcream) - PR #38) 185 | - Fix minor whitespace inconsistencies ([@raniesantos](https://github.com/raniesantos) - PR #28) 186 | 187 | ## 1.10.0 188 | 189 | - Update syntax highlighting 190 | - Added `Document Highlight Provider` and `Document Format Provider` ([@TheColorRed](https://github.com/TheColorRed) - PR #17) 191 | 192 | ## 1.9.0 193 | 194 | Laravel 5.4 blade directives & snippets: 195 | 196 | - Add `@isset`, `@empty`, `@includeWhen` directives 197 | - Add `b:isset`, `b:empty`, `b:includeWhen` snippets 198 | 199 | Laravel 5.5 blade directives & snippets: 200 | 201 | - Add `@auth`, `@guest`, `@switch`, `@case`, `@default` directives 202 | - Add `b:auth`, `b:guest`, `b:switch` snippets 203 | 204 | Syntax Enhancement 205 | 206 | - Change grammar of blade directive ([@mikebronner](https://github.com/mikebronner) - PR #23) 207 | 208 | ## 1.8.2 209 | 210 | - Update README (#18, #19) 211 | 212 | ## 1.8.1 213 | 214 | - Fix syntax parse failed (#5) 215 | 216 | ## 1.8.0 217 | 218 | - Add `@can` and `@cannot` related directives (#4) 219 | - Add `b:can`, `b:can-elsecan`, `b:cannot`, `b:cannot-elsecannot` authorizing snippets (#4) 220 | - Add `lv:mix` helper 221 | - Fix for loop snippet 222 | 223 | ## 1.7.0 224 | 225 | - Enhance blade syntax highlighting 226 | - Fix loop snippets 227 | 228 | ## 1.6.1 229 | 230 | - Fix extra slashes in `lv:*` helper snippets 231 | 232 | ## 1.6 233 | 234 | - Support `@component` and `@slot` directive added in Laravel 5.4 235 | - Fix #3 issue 236 | 237 | ## 1.5 238 | 239 | Support new directive added in Laravel 5.3 240 | 241 | ### PHP 242 | 243 | In some situations, it's useful to embed PHP code into your views. You can use the Blade `@php` directive to execute a block of plain PHP within your template: 244 | 245 | ``` 246 | @php 247 | // 248 | @endphp 249 | ``` 250 | 251 | ### Include Sub-views 252 | 253 | If you attempt to `@include` a view which does not exist, Laravel will throw an error. If you would like to include a view that may or may not be present, you should use the `@includeIf` directive: 254 | 255 | ``` 256 | @includeIf('view.name', ['some' => 'data']) 257 | ``` 258 | 259 | ## 1.4 260 | 261 | Update language mode recognition and emmet setting for VS Code 1.5+ 262 | 263 | ## 1.3 264 | 265 | Support Laravel 5.3 blade syntax 266 | 267 | * `@verbatim` - displaying JavaScript variables in a large portion in template 268 | 269 | ``` 270 | @verbatim 271 |
272 | Hello, {{ name }}. 273 |
274 | @endverbatim 275 | ``` 276 | 277 | * `$loop` variable : index, remaining, count, first, last, depth, parent 278 | 279 | ``` 280 | $loop->index 281 | $loop->remaining 282 | $loop->count 283 | $loop->first 284 | $loop->last 285 | $loop->depth 286 | $loop->parent 287 | ``` 288 | 289 | * Add pagination links helper snippet: `lv:pagination-links` 290 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-html-languageserver", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vscode-html-languageserver", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "typescript": "^2.7.1", 13 | "vscode-css-languageservice": "^4.0.2", 14 | "vscode-emmet-helper": "^1.2.15", 15 | "vscode-html-languageservice": "^3.0.2", 16 | "vscode-languageserver": "^4.0.0-next.4", 17 | "vscode-languageserver-types": "^3.14.0", 18 | "vscode-nls": "^4.1.1", 19 | "vscode-uri": "^1.0.6" 20 | }, 21 | "devDependencies": { 22 | "@types/mocha": "2.2.33", 23 | "@types/node": "7.0.43" 24 | }, 25 | "engines": { 26 | "node": "*" 27 | } 28 | }, 29 | "node_modules/@emmetio/extract-abbreviation": { 30 | "version": "0.1.6", 31 | "resolved": "https://registry.npmjs.org/@emmetio/extract-abbreviation/-/extract-abbreviation-0.1.6.tgz", 32 | "integrity": "sha512-Ce3xE2JvTSEbASFbRbA1gAIcMcZWdS2yUYRaQbeM0nbOzaZrUYfa3ePtcriYRZOZmr+CkKA+zbjhvTpIOAYVcw==" 33 | }, 34 | "node_modules/@types/mocha": { 35 | "version": "2.2.33", 36 | "resolved": "http://registry.npm.taobao.org/@types/mocha/download/@types/mocha-2.2.33.tgz", 37 | "integrity": "sha1-15oAYewnA3n02eIl9AlvtDZmne8=", 38 | "dev": true 39 | }, 40 | "node_modules/@types/node": { 41 | "version": "7.0.43", 42 | "resolved": "http://registry.npm.taobao.org/@types/node/download/@types/node-7.0.43.tgz", 43 | "integrity": "sha1-oYfghJWgdfIAypRgeckU4aX+liw=", 44 | "dev": true 45 | }, 46 | "node_modules/jsonc-parser": { 47 | "version": "1.0.3", 48 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-1.0.3.tgz", 49 | "integrity": "sha512-hk/69oAeaIzchq/v3lS50PXuzn5O2ynldopMC+SWBql7J2WtdptfB9dy8Y7+Og5rPkTCpn83zTiO8FMcqlXJ/g==" 50 | }, 51 | "node_modules/typescript": { 52 | "version": "2.7.1", 53 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", 54 | "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", 55 | "bin": { 56 | "tsc": "bin/tsc", 57 | "tsserver": "bin/tsserver" 58 | }, 59 | "engines": { 60 | "node": ">=4.2.0" 61 | } 62 | }, 63 | "node_modules/vscode-css-languageservice": { 64 | "version": "4.0.2", 65 | "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-4.0.2.tgz", 66 | "integrity": "sha512-pTnfXbsME3pl+yDfhUp/mtvPyIJk0Le4zqJxDn56s9GY9LqY0RmkSEh0oHH6D0HXR3Ni6wKosIaqu8a2G0+jdw==", 67 | "dependencies": { 68 | "vscode-languageserver-types": "^3.15.0-next.2", 69 | "vscode-nls": "^4.1.1" 70 | } 71 | }, 72 | "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { 73 | "version": "3.15.0-next.2", 74 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.2.tgz", 75 | "integrity": "sha512-2JkrMWWUi2rlVLSo9OFR2PIGUzdiowEM8NgNYiwLKnXTjpwpjjIrJbNNxDik7Rv4oo9KtikcFQZKXbrKilL/MQ==" 76 | }, 77 | "node_modules/vscode-css-languageservice/node_modules/vscode-nls": { 78 | "version": "4.1.1", 79 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 80 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 81 | }, 82 | "node_modules/vscode-emmet-helper": { 83 | "version": "1.2.15", 84 | "resolved": "https://registry.npmjs.org/vscode-emmet-helper/-/vscode-emmet-helper-1.2.15.tgz", 85 | "integrity": "sha512-JplvmMMWSvm/6/dZezix2ADPM49u6YahPYjs/QToohUpomW/2Eb27ecCrkCyOGBPfKLKGiOPHCssss8TSDA9ag==", 86 | "deprecated": "This package has been renamed to @vscode/emmet-helper, please update to the new name", 87 | "dependencies": { 88 | "@emmetio/extract-abbreviation": "0.1.6", 89 | "jsonc-parser": "^1.0.0", 90 | "vscode-languageserver-types": "^3.6.0-next.1" 91 | } 92 | }, 93 | "node_modules/vscode-html-languageservice": { 94 | "version": "3.0.2", 95 | "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-3.0.2.tgz", 96 | "integrity": "sha512-MP9al7nk1SqQwW4GdDy6Ec3UU1GKy0Wf4pzo3nQ5lgdScb2pajV7iyXZIGJk7jQbifkZWnG0jB7CKecTNFynJw==", 97 | "dependencies": { 98 | "vscode-languageserver-types": "^3.15.0-next.2", 99 | "vscode-nls": "^4.1.1", 100 | "vscode-uri": "^2.0.1" 101 | } 102 | }, 103 | "node_modules/vscode-html-languageservice/node_modules/vscode-languageserver-types": { 104 | "version": "3.15.0-next.2", 105 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.2.tgz", 106 | "integrity": "sha512-2JkrMWWUi2rlVLSo9OFR2PIGUzdiowEM8NgNYiwLKnXTjpwpjjIrJbNNxDik7Rv4oo9KtikcFQZKXbrKilL/MQ==" 107 | }, 108 | "node_modules/vscode-html-languageservice/node_modules/vscode-nls": { 109 | "version": "4.1.1", 110 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 111 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 112 | }, 113 | "node_modules/vscode-html-languageservice/node_modules/vscode-uri": { 114 | "version": "2.0.3", 115 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.0.3.tgz", 116 | "integrity": "sha512-4D3DI3F4uRy09WNtDGD93H9q034OHImxiIcSq664Hq1Y1AScehlP3qqZyTkX/RWxeu0MRMHGkrxYqm2qlDF/aw==" 117 | }, 118 | "node_modules/vscode-jsonrpc": { 119 | "version": "3.6.0-next.1", 120 | "resolved": "http://registry.npm.taobao.org/vscode-jsonrpc/download/vscode-jsonrpc-3.6.0-next.1.tgz", 121 | "integrity": "sha1-PLRj3/5YQtauwWcYypJScIzWqr4=", 122 | "engines": { 123 | "node": ">=4.0.0 || >=6.0.0" 124 | } 125 | }, 126 | "node_modules/vscode-languageserver": { 127 | "version": "4.0.0-next.4", 128 | "resolved": "http://registry.npm.taobao.org/vscode-languageserver/download/vscode-languageserver-4.0.0-next.4.tgz", 129 | "integrity": "sha1-FiRAsVvtqrB+FnbwRuTZuFeLPZI=", 130 | "dependencies": { 131 | "vscode-languageserver-protocol": "^3.6.0-next.5", 132 | "vscode-uri": "^1.0.1" 133 | }, 134 | "bin": { 135 | "installServerIntoExtension": "bin/installServerIntoExtension" 136 | } 137 | }, 138 | "node_modules/vscode-languageserver-protocol": { 139 | "version": "3.6.0-next.5", 140 | "resolved": "http://registry.npm.taobao.org/vscode-languageserver-protocol/download/vscode-languageserver-protocol-3.6.0-next.5.tgz", 141 | "integrity": "sha1-7S7C23WYJvdTwKE5d9+yvtxNMbM=", 142 | "dependencies": { 143 | "vscode-jsonrpc": "^3.6.0-next.1", 144 | "vscode-languageserver-types": "^3.6.0-next.1" 145 | } 146 | }, 147 | "node_modules/vscode-languageserver-types": { 148 | "version": "3.14.0", 149 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz", 150 | "integrity": "sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==" 151 | }, 152 | "node_modules/vscode-languageserver/node_modules/vscode-uri": { 153 | "version": "1.0.8", 154 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", 155 | "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" 156 | }, 157 | "node_modules/vscode-nls": { 158 | "version": "4.1.1", 159 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 160 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 161 | }, 162 | "node_modules/vscode-uri": { 163 | "version": "1.0.8", 164 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", 165 | "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" 166 | } 167 | }, 168 | "dependencies": { 169 | "@emmetio/extract-abbreviation": { 170 | "version": "0.1.6", 171 | "resolved": "https://registry.npmjs.org/@emmetio/extract-abbreviation/-/extract-abbreviation-0.1.6.tgz", 172 | "integrity": "sha512-Ce3xE2JvTSEbASFbRbA1gAIcMcZWdS2yUYRaQbeM0nbOzaZrUYfa3ePtcriYRZOZmr+CkKA+zbjhvTpIOAYVcw==" 173 | }, 174 | "@types/mocha": { 175 | "version": "2.2.33", 176 | "resolved": "http://registry.npm.taobao.org/@types/mocha/download/@types/mocha-2.2.33.tgz", 177 | "integrity": "sha1-15oAYewnA3n02eIl9AlvtDZmne8=", 178 | "dev": true 179 | }, 180 | "@types/node": { 181 | "version": "7.0.43", 182 | "resolved": "http://registry.npm.taobao.org/@types/node/download/@types/node-7.0.43.tgz", 183 | "integrity": "sha1-oYfghJWgdfIAypRgeckU4aX+liw=", 184 | "dev": true 185 | }, 186 | "jsonc-parser": { 187 | "version": "1.0.3", 188 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-1.0.3.tgz", 189 | "integrity": "sha512-hk/69oAeaIzchq/v3lS50PXuzn5O2ynldopMC+SWBql7J2WtdptfB9dy8Y7+Og5rPkTCpn83zTiO8FMcqlXJ/g==" 190 | }, 191 | "typescript": { 192 | "version": "2.7.1", 193 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", 194 | "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==" 195 | }, 196 | "vscode-css-languageservice": { 197 | "version": "4.0.2", 198 | "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-4.0.2.tgz", 199 | "integrity": "sha512-pTnfXbsME3pl+yDfhUp/mtvPyIJk0Le4zqJxDn56s9GY9LqY0RmkSEh0oHH6D0HXR3Ni6wKosIaqu8a2G0+jdw==", 200 | "requires": { 201 | "vscode-languageserver-types": "^3.15.0-next.2", 202 | "vscode-nls": "^4.1.1" 203 | }, 204 | "dependencies": { 205 | "vscode-languageserver-types": { 206 | "version": "3.15.0-next.2", 207 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.2.tgz", 208 | "integrity": "sha512-2JkrMWWUi2rlVLSo9OFR2PIGUzdiowEM8NgNYiwLKnXTjpwpjjIrJbNNxDik7Rv4oo9KtikcFQZKXbrKilL/MQ==" 209 | }, 210 | "vscode-nls": { 211 | "version": "4.1.1", 212 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 213 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 214 | } 215 | } 216 | }, 217 | "vscode-emmet-helper": { 218 | "version": "1.2.15", 219 | "resolved": "https://registry.npmjs.org/vscode-emmet-helper/-/vscode-emmet-helper-1.2.15.tgz", 220 | "integrity": "sha512-JplvmMMWSvm/6/dZezix2ADPM49u6YahPYjs/QToohUpomW/2Eb27ecCrkCyOGBPfKLKGiOPHCssss8TSDA9ag==", 221 | "requires": { 222 | "@emmetio/extract-abbreviation": "0.1.6", 223 | "jsonc-parser": "^1.0.0", 224 | "vscode-languageserver-types": "^3.6.0-next.1" 225 | } 226 | }, 227 | "vscode-html-languageservice": { 228 | "version": "3.0.2", 229 | "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-3.0.2.tgz", 230 | "integrity": "sha512-MP9al7nk1SqQwW4GdDy6Ec3UU1GKy0Wf4pzo3nQ5lgdScb2pajV7iyXZIGJk7jQbifkZWnG0jB7CKecTNFynJw==", 231 | "requires": { 232 | "vscode-languageserver-types": "^3.15.0-next.2", 233 | "vscode-nls": "^4.1.1", 234 | "vscode-uri": "^2.0.1" 235 | }, 236 | "dependencies": { 237 | "vscode-languageserver-types": { 238 | "version": "3.15.0-next.2", 239 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.2.tgz", 240 | "integrity": "sha512-2JkrMWWUi2rlVLSo9OFR2PIGUzdiowEM8NgNYiwLKnXTjpwpjjIrJbNNxDik7Rv4oo9KtikcFQZKXbrKilL/MQ==" 241 | }, 242 | "vscode-nls": { 243 | "version": "4.1.1", 244 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 245 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 246 | }, 247 | "vscode-uri": { 248 | "version": "2.0.3", 249 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.0.3.tgz", 250 | "integrity": "sha512-4D3DI3F4uRy09WNtDGD93H9q034OHImxiIcSq664Hq1Y1AScehlP3qqZyTkX/RWxeu0MRMHGkrxYqm2qlDF/aw==" 251 | } 252 | } 253 | }, 254 | "vscode-jsonrpc": { 255 | "version": "3.6.0-next.1", 256 | "resolved": "http://registry.npm.taobao.org/vscode-jsonrpc/download/vscode-jsonrpc-3.6.0-next.1.tgz", 257 | "integrity": "sha1-PLRj3/5YQtauwWcYypJScIzWqr4=" 258 | }, 259 | "vscode-languageserver": { 260 | "version": "4.0.0-next.4", 261 | "resolved": "http://registry.npm.taobao.org/vscode-languageserver/download/vscode-languageserver-4.0.0-next.4.tgz", 262 | "integrity": "sha1-FiRAsVvtqrB+FnbwRuTZuFeLPZI=", 263 | "requires": { 264 | "vscode-languageserver-protocol": "^3.6.0-next.5", 265 | "vscode-uri": "^1.0.1" 266 | }, 267 | "dependencies": { 268 | "vscode-uri": { 269 | "version": "1.0.8", 270 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", 271 | "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" 272 | } 273 | } 274 | }, 275 | "vscode-languageserver-protocol": { 276 | "version": "3.6.0-next.5", 277 | "resolved": "http://registry.npm.taobao.org/vscode-languageserver-protocol/download/vscode-languageserver-protocol-3.6.0-next.5.tgz", 278 | "integrity": "sha1-7S7C23WYJvdTwKE5d9+yvtxNMbM=", 279 | "requires": { 280 | "vscode-jsonrpc": "^3.6.0-next.1", 281 | "vscode-languageserver-types": "^3.6.0-next.1" 282 | } 283 | }, 284 | "vscode-languageserver-types": { 285 | "version": "3.14.0", 286 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz", 287 | "integrity": "sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==" 288 | }, 289 | "vscode-nls": { 290 | "version": "4.1.1", 291 | "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", 292 | "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==" 293 | }, 294 | "vscode-uri": { 295 | "version": "1.0.8", 296 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", 297 | "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /tests/test.blade.php: -------------------------------------------------------------------------------- 1 |

Blade Syntax Highlighting Tests

2 | 3 | {{-- Displaying Data --}} 4 | Hello, {{ $name }}. 5 | The current UNIX timestamp is {{ time() }}. 6 | 7 | {{-- Escape Data --}} 8 | Hello, {{{ $name }}}. 9 | 10 | {{-- Echoing Data If It Exists --}} 11 | {{ isset($name) ? $name : 'Default' }} 12 | {{ $name or 'Default' }} 13 | 14 |
15 | 16 | {{-- Displaying Unescaped Data --}} 17 | Hello, {!! $name !!}. 18 | 19 | {{-- Rendering JSON --}} 20 | 24 | 25 | {{-- Blade & JavaScript Frameworks --}} 26 | Hello, @{{ name }}. 27 | 28 | {{-- Blade --}} 29 | @@json() 30 | 31 | 32 | @json() 33 | 34 | @verbatim 35 |
36 | Hello, {{ name }}. 37 |
38 | @endverbatim 39 | 40 | {{-- Control Structures --}} 41 | 42 | {{-- If Statements --}} 43 | @if (count($records) === 1) 44 | I have one record! 45 | @elseif (count($records) > 1) 46 | I have multiple records! 47 | @else 48 | I don't have any records! 49 | @endif 50 | 51 | @unless (Auth::check()) 52 | You are not signed in. 53 | @endunless 54 | 55 | 56 | @isset($records) 57 | // $records is defined and is not null... 58 | @endisset 59 | 60 | @empty($records) 61 | // $records is "empty"... 62 | @endempty 63 | 64 | 65 | {{-- Authentication Directives --}} 66 | 67 | @auth 68 | // The user is authenticated... 69 | @endauth 70 | 71 | @guest 72 | // The user is not authenticated... 73 | @endguest 74 | 75 | @auth('admin') 76 | // The user is authenticated... 77 | @endauth 78 | 79 | @guest('admin') 80 | // The user is not authenticated... 81 | @endguest 82 | 83 | 84 | {{-- Section Directives --}} 85 | 86 | @hasSection('navigation') 87 |
88 | @yield('navigation') 89 |
90 | 91 |
92 | @endif 93 | 94 | @sectionMissing('navigation') 95 |
96 | @include('default-navigation') 97 |
98 | @endif 99 | 100 | 101 | {{-- Environment Directives --}} 102 | 103 | @production 104 | // Production specific content... 105 | @endproduction 106 | 107 | @env('staging') 108 | // The application is running in "staging"... 109 | @endenv 110 | 111 | @env(['staging', 'production']) 112 | // The application is running in "staging" or "production"... 113 | @endenv 114 | 115 | 116 | {{-- Section Directives --}} 117 | 118 | @hasSection('navigation') 119 |
120 | @yield('navigation') 121 |
122 | 123 |
124 | @endif 125 | 126 | @sectionMissing('navigation') 127 |
128 | @include('default-navigation') 129 |
130 | @endif 131 | 132 | @session('status') 133 |
134 | {{ $value }} 135 |
136 | @endsession 137 | 138 | {{-- Switch Statements --}} 139 | 140 | @switch($i) 141 | @case(1) 142 | First case... 143 | @break 144 | 145 | @case(2) 146 | Second case... 147 | @break 148 | 149 | @default 150 | Default case... 151 | @endswitch 152 | 153 | 154 | {{-- Loops --}} 155 | 156 | @for ($i = 0; $i < 10; $i++) 157 | The current value is {{ $i }} 158 | @endfor 159 | 160 | @foreach ($users as $user) 161 |

This is user {{ $user->id }}

162 | @endforeach 163 | 164 | @forelse ($users as $user) 165 |
  • {{ $user->name }}
  • 166 | @empty 167 |

    No users

    168 | @endforelse 169 | 170 | @while (true) 171 |

    I'm looping forever.

    172 | @endwhile 173 | 174 | {{-- continue & break --}} 175 | @foreach ($users as $user) 176 | 177 | @if ($user->type == 1) 178 | @continue 179 | @endif 180 | 181 |
  • {{ $user->name }}
  • 182 | 183 | @if ($user->number == 5) 184 | @break 185 | @endif 186 | 187 | @continue($user->type == 1) 188 |
  • {{ $user->name }}
  • 189 | @break($user->number == 5) 190 | 191 | @endforeach 192 | 193 | @foreach ($users as $user) 194 | @continue($user->type == 1) 195 | 196 |
  • {{ $user->name }}
  • 197 | 198 | @break($user->number == 5) 199 | @endforeach 200 | 201 | 202 | {{-- The Loop Variable --}} 203 | 204 | @foreach ($users as $user) 205 | @if ($loop->first) 206 | This is the first iteration. 207 | @endif 208 | 209 | @if ($loop->last) 210 | This is the last iteration. 211 | @endif 212 | 213 |

    This is user {{ $user->id }}

    214 | 215 | {{-- $loop->parent --}} 216 | @foreach ($user->posts as $post) 217 | @if ($loop->parent->first) 218 | This is the first iteration of the parent loop. 219 | @endif 220 | @endforeach 221 | @endforeach 222 | 223 | 224 | {{-- Loop Variables --}} 225 | 226 | {{-- The index of the current loop iteration (starts at 0) --}} 227 | {{ $loop->index }} 228 | {{-- The current loop iteration (starts at 1) --}} 229 | {{ $loop->iteration }} 230 | {{-- The iteration remaining in the loop --}} 231 | {{ $loop->remaining }} 232 | {{-- The total number of items in the array being iterated --}} 233 | {{ $loop->count }} 234 | {{-- Whether this is the first iteration through the loop --}} 235 | {{ $loop->first }} 236 | {{-- Whether this is the last iteration through the loop --}} 237 | {{ $loop->last }} 238 | {{-- The nesting level of the current loop --}} 239 | {{ $loop->depth }} 240 | {{-- When in a nested loop, the parent's loop variable --}} 241 | {{ $loop->parent }} 242 | 243 | @endforeach 244 | 245 | {{-- Comments --}} 246 | {{-- This comment will not be present in the rendered HTML --}} 247 | 248 | {{-- 249 | This comment will not be in the rendered HTML 250 | This comment will not be in the rendered HTML 251 | This comment will not be in the rendered HTML 252 | --}} 253 | 254 | {{-- PHP --}} 255 | 256 | 257 | format('m/d/Y H:i'); ?> 258 | 259 | 264 | 265 | @php ($hello = "hello world") 266 | 267 | @php 268 | foreach (range(1, 10) as $number) { 269 | echo $number; 270 | } 271 | @endphp 272 | 273 | @use('App\Models\Flight') 274 | @use('App\Models\Flight', 'FlightModel') 275 | 276 | {{-- Conditional Classes : `@class` directive --}} 277 | 278 | @php 279 | $isActive = false; 280 | $hasError = true; 281 | @endphp 282 | 283 | $isActive, 286 | 'text-gray-500' => ! $isActive, 287 | 'bg-red' => $hasError, 288 | ])> 289 | 290 | 291 | 292 | {{-- Conditional Classes : `@style` directive --}} 293 | 294 | @php 295 | $isActive = true; 296 | @endphp 297 | 298 | $isActive, 301 | ])> 302 | 303 | 304 | 305 | 306 | {{-- The @once Directive --}} 307 | 308 | @once 309 | @push('scripts') 310 | 313 | @endpush 314 | @endonce 315 | 316 | {{-- The @pushOnce, @pushIf, @prependOnce Directive --}} 317 | 318 | @pushOnce('scripts') 319 | 322 | @endPushOnce 323 | 324 | @pushIf($condition, 'scripts') 325 | 328 | @endPushIf 329 | 330 | @prependOnce('scripts') 331 | 334 | @endPrependOnce 335 | 336 | {{-- Forms --}} 337 |
    338 | @csrf 339 | @method('PUT') 340 |
    341 | 342 | {{-- Validation Errors --}} 343 | 344 | 345 | 346 | 347 | 348 | @error('title') 349 |
    {{ $message }}
    350 | @enderror 351 | 352 | 353 | {{-- Components --}} 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | Server Error 365 | 366 | 367 | Whoops! Something went wrong! 368 | 369 | 370 | {{-- Component attribute expressions --}} 371 | 415 | 416 | 417 | {{-- Including Sub-Views --}} 418 |
    419 | @include('shared.errors') 420 |
    421 | 422 |
    423 |
    424 | 425 | @include('view.name') 426 | @include('view.name', ['some' => 'data']) 427 | @includeIf('view.name', ['some' => 'data']) 428 | @includeWhen($boolean, 'view.name', ['some' => 'data']) 429 | @includeUnless($boolean, 'view.name', ['some' => 'data']) 430 | @includeFirst(['custom.admin', 'admin'], ['some' => 'data']) 431 | 432 | {{-- Rendering Views For Collections --}} 433 | @each('view.name', $jobs, 'job') 434 | @each('view.name', $jobs, 'job', 'view.empty') 435 | 436 | {{-- Stacks --}} 437 | @stack('scripts') 438 | 439 | @push('scripts') 440 | 441 | @endpush 442 | 443 | @prepend('scripts') 444 | This will be first... 445 | @endprepend 446 | 447 | {{-- Service Injection --}} 448 | @inject('metrics', 'App\Services\MetricsService') 449 |
    450 | Monthly Revenue: {{ $metrics->monthlyRevenue() }}. 451 |
    452 | 453 | 454 | {{-- Retrieving Translation Strings --}} 455 | @lang('messages.welcome') 456 | 457 | {{-- 5.3 --}} 458 | {{ trans('messages.welcome') }} 459 | {{-- 5.4 --}} 460 | {{ __('messages.welcome') }} 461 | 462 | {{-- Pluralization --}} 463 | @choice('messages.apples', 10) 464 | 465 | {{-- 'apples' => '{0} There are none|[1,19] There are some|[20,Inf] There are many', --}} 466 | {{ trans_choice('messages.apples', 10) }} 467 | 468 | {{-- Replacing Parameters In Translation Strings --}} 469 | {{-- 'greeting' => 'Welcome, :name', --}} 470 | {{ __('messages.greeting', ['name' => 'Winnie']) }} 471 | 472 | 473 | {{-- Authorizing --}} 474 | 475 | @can('update', $post) 476 | 477 | @elsecan('create', App\Models\Post::class) 478 | 479 | @endcan 480 | 481 | @canany(['update'], $post) 482 | 483 | @elsecanany(['create'], App\Models\Post::class) 484 | 485 | @endcanany 486 | 487 | @cannot('update', $post) 488 | 489 | @elsecannot('create', App\Models\Post::class) 490 | 491 | @endcannot 492 | 493 | @if (Auth::user()->can('update', $post)) 494 | 495 | @endif 496 | 497 | @unless (Auth::user()->can('update', $post)) 498 | 499 | @endunless 500 | 501 | @can('create', App\Models\Post::class) 502 | 503 | @endcan 504 | 505 | @canany(['create'], App\Models\Post::class) 506 | 507 | @endcanany 508 | 509 | @cannot('create', App\Models\Post::class) 510 | 511 | @endcannot 512 | 513 | {{-- Retrieving Translation Strings --}} 514 | 515 | {{ __('messages.welcome') }} 516 | @lang('messages.welcome') 517 | 518 | @props(['type' => 'info', 'message']) 519 | 520 | 521 | {{-- Envoy --}} 522 | 523 | @setup 524 | require __DIR__.'/vendor/autoload.php'; 525 | $dotenv = new Dotenv\Dotenv(__DIR__); 526 | @endsetup 527 | 528 | @servers(['web' => $server]) 529 | 530 | @task('init') 531 | if [ ! -d {{ $path }}/current ]; then 532 | cd {{ $path }} 533 | @endtask 534 | 535 | @story('deploy') 536 | deployment_start 537 | deployment_composer 538 | deployment_finish 539 | @endstory 540 | 541 | {{-- Livewire --}} 542 | 543 | @livewireStyles 544 | @livewireScripts 545 | 546 | @livewire('user-profile', ['user' => $user], key($user->id)) 547 | 548 | {{-- Additional Attributes --}} 549 | {{-- Checked / Selected / Disabled Blade Directives (9.x) --}} 550 | 551 | active)) /> 555 | 556 | 563 | 564 | 567 | 568 | {{-- Readonly / Required Blade Directives (9.x) --}} 569 | 570 | isNotAdmin()) /> 574 | 575 | isAdmin()) /> 579 | 580 | {{-- Vite --}} 581 | 582 | @vite(['resources/css/app.css', 'resources/js/app.js']) 583 | @vite('resources/js/app.js') 584 | @vite('resources/js/app.js', 'vendor/courier/build') 585 | 586 | 589 | 592 | -------------------------------------------------------------------------------- /snippets/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Extending a layout */ 3 | "Extend layout": { 4 | "prefix": "b:extends", 5 | "body": "@extends('${1:name}')", 6 | "description": "extends view layout" 7 | }, 8 | "Yield content": { 9 | "prefix": "b:yield", 10 | "body": "@yield('${1:name}')", 11 | "description": "yield content section" 12 | }, 13 | "Content Section": { 14 | "prefix": "b:section", 15 | "body": [ 16 | "@section('${1:name}')", 17 | " $2", 18 | "@endsection" 19 | ], 20 | "description": "content section" 21 | }, 22 | "Content Section Show": { 23 | "prefix": "b:section-show", 24 | "body": [ 25 | "@section('$1')", 26 | " $2", 27 | "@show" 28 | ], 29 | "description": "content section show" 30 | }, 31 | /* Include sub-view */ 32 | "Include view": { 33 | "prefix": "b:include", 34 | "body": "@include('${1:name}')", 35 | "description": "include view" 36 | }, 37 | /* If Statements */ 38 | "If-block": { 39 | "prefix": "b:if", 40 | "body": [ 41 | "@if ($1)", 42 | " $2", 43 | "@endif" 44 | ], 45 | "description": "@if block" 46 | }, 47 | "If-else-block": { 48 | "prefix": "b:if-else", 49 | "body": [ 50 | "@if ($1)", 51 | " $2", 52 | "@else", 53 | " $3", 54 | "@endif" 55 | ], 56 | "description": "if-else block" 57 | }, 58 | "Has Section": { 59 | "prefix": "b:has-section", 60 | "body": [ 61 | "@hasSection ('${1:name}')", 62 | " $2", 63 | "@else", 64 | " $3", 65 | "@endif" 66 | ], 67 | "description": "@hasSection condition" 68 | }, 69 | "Unless-block": { 70 | "prefix": "b:unless", 71 | "body": [ 72 | "@unless ($1)", 73 | " $2", 74 | "@endunless" 75 | ], 76 | "description": "@unless block" 77 | }, 78 | /* Loops */ 79 | "For-block": { 80 | "prefix": "b:for", 81 | "body": [ 82 | "@for (\\$i = ${1:0}; \\$i < ${2:\\$count}; \\$i++)", 83 | " $3", 84 | "@endfor" 85 | ], 86 | "description": "@for block" 87 | }, 88 | "Foreach-block": { 89 | "prefix": "b:foreach", 90 | "body": [ 91 | "@foreach (${1:\\$collection} as ${2:\\$item})", 92 | " $3", 93 | "@endforeach" 94 | ], 95 | "description": "@foreach block" 96 | }, 97 | "forelse-block": { 98 | "prefix": "b:forelse", 99 | "body": [ 100 | "@forelse (${1:\\$collection} as ${2:\\$item})", 101 | " $3", 102 | "@empty", 103 | " $4", 104 | "@endforelse" 105 | ], 106 | "description": "@forelse block" 107 | }, 108 | "while-block": { 109 | "prefix": "b:while", 110 | "body": [ 111 | "@while ($1)", 112 | " $2", 113 | "@endwhile" 114 | ], 115 | "description": "@while block" 116 | }, 117 | /* Rendering views for collections */ 118 | "each loop": { 119 | "prefix": "b:each", 120 | "body": "@each('${1:view.name}', ${2:\\$collection}, '${3:variable}', '${4:view.empty}')", 121 | "description": "@each loop" 122 | }, 123 | /* Comments */ 124 | "blade comment": { 125 | "prefix": "b:comment", 126 | "body": "{{-- ${1:comment} --}}", 127 | "description": "comment block" 128 | }, 129 | /* Display Data */ 130 | "blade echo-data": { 131 | "prefix": "b:echo", 132 | "body": "{{ ${1:\\$data} }}", 133 | "description": "echo data" 134 | }, 135 | "blade echo-unescaped-data": { 136 | "prefix": "b:echo-html", 137 | "body": "{!! ${1:\\$html_data} !!}", 138 | "description": "echo unescaped data (allow html outputs)" 139 | }, 140 | "blade echo-untouch": { 141 | "prefix": "b:echo-raw", 142 | "body": "@{{ ${1:variable} }}", 143 | "description": "echo untouched data (allow javascript expression)" 144 | }, 145 | "blade verbatim": { 146 | "prefix": "b:verbatim", 147 | "body": [ 148 | "@verbatim", 149 | "{{ ${1:variable} }}", 150 | "@endverbatim" 151 | ], 152 | "description": "displaying JavaScript variables in a large portion of your template" 153 | }, 154 | /* Stacks */ 155 | "Push stack": { 156 | "prefix": "b:push", 157 | "body": [ 158 | "@push('${1:name}')", 159 | " $2", 160 | "@endpush" 161 | ], 162 | "description": "@push stack" 163 | }, 164 | "Stack": { 165 | "prefix": "b:stack", 166 | "body": "@stack('${1:name}')", 167 | "description": "@stack" 168 | }, 169 | /* Service Injection */ 170 | "inject service": { 171 | "prefix": "b:inject", 172 | "body": "@inject('${1:name}', '${2:class}')", 173 | "description": "@inject Service" 174 | }, 175 | /* Authorizing */ 176 | "can": { 177 | "prefix": "b:can", 178 | "body": [ 179 | "@can('${1:update}', ${2:\\$post})", 180 | " $3", 181 | "@endcan" 182 | ], 183 | "description": "display a portion of the page only if the user is authorized to perform a given action." 184 | }, 185 | "can-elsecan": { 186 | "prefix": "b:can-elsecan", 187 | "body": [ 188 | "@can('${1:update}', ${2:\\$post})", 189 | " $3", 190 | "@elsecan('create', App\\Models\\\\${4:Post}::class)", 191 | " $5", 192 | "@endcan" 193 | ], 194 | "description": "display a portion of the page only if the user is authorized to perform a given action." 195 | }, 196 | "canany": { 197 | "prefix": "b:canany", 198 | "body": [ 199 | "@canany(['update', 'view', 'delete'], ${1:\\$post})", 200 | " $2", 201 | "@endcanany" 202 | ], 203 | "description": "display a portion of the page only if the user is authorized to perform a given action." 204 | }, 205 | "canany-elsecanany": { 206 | "prefix": "b:canany-elsecanany", 207 | "body": [ 208 | "@canany(['update', 'view', 'delete'], ${1:\\$post})", 209 | " $2", 210 | "@elsecanany(['create'], App\\Models\\\\${3:Post}::class)", 211 | " $4", 212 | "@endcanany" 213 | ], 214 | "description": "display a portion of the page only if the user is authorized to perform a given action." 215 | }, 216 | "cannot": { 217 | "prefix": "b:cannot", 218 | "body": [ 219 | "@cannot('${1:update}', ${2:\\$post})", 220 | " $3", 221 | "@endcannot" 222 | ], 223 | "description": "display a portion of the page only if the user is authorized to perform a given action." 224 | }, 225 | "cannot-elsecannot": { 226 | "prefix": "b:cannot-elsecannot", 227 | "body": [ 228 | "@cannot('${1:update}', ${2:\\$post})", 229 | " $3", 230 | "@elsecannot('create', App\\Models\\\\${5:Post}::class)", 231 | " $6", 232 | "@endcannot" 233 | ], 234 | "description": "display a portion of the page only if the user is authorized to perform a given action." 235 | }, 236 | /* v5.3 - $loop variable */ 237 | "loop": { 238 | "prefix": "b:loop", 239 | "body": [ 240 | "\\$loop->${1:first}" 241 | ], 242 | "description": "$loop->(index|remaining|count|first|last|depth|parent)" 243 | }, 244 | "loop first": { 245 | "prefix": "b:loop-first", 246 | "body": [ 247 | "@if (\\$loop->first)", 248 | " ${1:{{-- This is the first iteration --\\}\\}}", 249 | "@endif" 250 | ], 251 | "description": "$loop->first" 252 | }, 253 | "loop last": { 254 | "prefix": "b:loop-last", 255 | "body": [ 256 | "@if (\\$loop->last)", 257 | " ${1:{{-- This is the last iteration --\\}\\}}", 258 | "@endif" 259 | ], 260 | "description": "$loop->last" 261 | }, 262 | "php": { 263 | "prefix": "b:php", 264 | "body": [ 265 | "@php", 266 | " $1", 267 | "@endphp" 268 | ], 269 | "description": "@php block code in view" 270 | }, 271 | "includeIf": { 272 | "prefix": "b:includeIf", 273 | "body": "@includeIf('${1:view.name}'${2:, ['some' => 'data']})", 274 | "description": "include a view that may or may not be present, you should use the @includeIf directive" 275 | }, 276 | /* v5.4 */ 277 | "component": { 278 | "prefix": "b:component", 279 | "body": [ 280 | "@component('$1')", 281 | " $2", 282 | "@endcomponent" 283 | ], 284 | "description": "component" 285 | }, 286 | "slot": { 287 | "prefix": "b:slot", 288 | "body": [ 289 | "@slot('$1')", 290 | " $2", 291 | "@endslot" 292 | ], 293 | "description": "slot" 294 | }, 295 | "isset": { 296 | "prefix": "b:isset", 297 | "body": [ 298 | "@isset(${1:\\$record})", 299 | " $2", 300 | "@endisset" 301 | ], 302 | "description": "isset" 303 | }, 304 | "empty": { 305 | "prefix": "b:empty", 306 | "body": [ 307 | "@empty(${1:\\$record})", 308 | " $2", 309 | "@endempty" 310 | ], 311 | "description": "empty" 312 | }, 313 | "error": { 314 | "prefix": "b:error", 315 | "body": [ 316 | "@error('${1:record}')", 317 | " $2", 318 | "@enderror" 319 | ], 320 | "description": "error" 321 | }, 322 | "includeWhen": { 323 | "prefix": "b:includeWhen", 324 | "body": "@includeWhen(${1:\\$boolean}, '${2:view.name}', [${3:'some' => 'data'}])", 325 | "description": "includeWhen" 326 | }, 327 | /* v5.5 */ 328 | "auth": { 329 | "prefix": "b:auth", 330 | "body": [ 331 | "@auth", 332 | " $1", 333 | "@endauth" 334 | ], 335 | "description": "auth" 336 | }, 337 | "guest": { 338 | "prefix": "b:guest", 339 | "body": [ 340 | "@guest", 341 | " $1", 342 | "@endguest" 343 | ], 344 | "description": "guest" 345 | }, 346 | "switch": { 347 | "prefix": "b:switch", 348 | "body": [ 349 | "@switch(${1:\\$type})", 350 | " @case(${2:1})", 351 | " $3", 352 | " @break", 353 | " @case(${4:2})", 354 | " $5", 355 | " @break", 356 | " @default", 357 | " $6", 358 | "@endswitch" 359 | ], 360 | "description": "switch" 361 | }, 362 | "includeFirst": { 363 | "prefix": "b:includeFirst", 364 | "body": "@includeFirst(['${1:view.name}', '${2:variable}'], [${3:'some' => 'data'}])", 365 | "description": "includeFirst" 366 | }, 367 | /* v5.6 */ 368 | "csrf": { 369 | "prefix": "b:csrf", 370 | "body": "@csrf", 371 | "description": "form csrf field" 372 | }, 373 | "method": { 374 | "prefix": "b:method", 375 | "body": "@method($1)", 376 | "description": "form method field" 377 | }, 378 | "dump": { 379 | "prefix": "b:dump", 380 | "body": "@dump($1)", 381 | "description": "dump" 382 | }, 383 | "dd": { 384 | "prefix": "b:dd", 385 | "body": "@dd($1)", 386 | "description": "dump and die" 387 | }, 388 | /* Retrieving Translation Strings */ 389 | "lang": { 390 | "prefix": "b:lang", 391 | "body": "@lang('${1:messages.welcome}')", 392 | "description": "lang" 393 | }, 394 | /* v6.x */ 395 | "includeUnless": { 396 | "prefix": "b:includeUnless", 397 | "body": "@includeUnless(${1:\\$boolean}, '${2:view.name}', [${3:'some' => 'data'}])", 398 | "description": "includeUnless" 399 | }, 400 | /* v7.x */ 401 | "props": { 402 | "prefix": "b:props", 403 | "body": "@props(['${1:propName}'])", 404 | "description": "Blade component data properties" 405 | }, 406 | "env": { 407 | "prefix": "b:env", 408 | "body": [ 409 | "@env('${1:staging}')", 410 | " $2", 411 | "@endenv" 412 | ], 413 | "description": "env" 414 | }, 415 | "production": { 416 | "prefix": "b:production", 417 | "body": [ 418 | "@production", 419 | " $1", 420 | "@endproduction" 421 | ], 422 | "description": "production" 423 | }, 424 | "once": { 425 | "prefix": "b:once", 426 | "body": [ 427 | "@once", 428 | " $1", 429 | "@endonce" 430 | ], 431 | "description": "define a portion of template that will only be evaluated once per rendering cycle" 432 | }, 433 | /* v8.x */ 434 | "aware": { 435 | "prefix": "b:aware", 436 | "body": "@aware(['${1:propName}'])", 437 | "description": "Accessing data from a parent component (Laravel 8.64)" 438 | }, 439 | "js": { 440 | "prefix": "b:js", 441 | "body": "@js(${1:\\$data})", 442 | "description": "This directive is useful to properly escape JSON within HTML quotes" 443 | }, 444 | "class": { 445 | "prefix": "b:class", 446 | "body": "@class(['${1:p-4}', ${2:'font-bold' => true}])", 447 | "description": "conditionally compiles a CSS class string. (Laravel 8.51)" 448 | }, 449 | /* v9.x */ 450 | "checked": { 451 | "prefix": "b:checked", 452 | "body": "@checked(${1:true})", 453 | "description": "This directive will echo checked if the provided condition evaluates to true (Laravel 9.x)" 454 | }, 455 | "selected": { 456 | "prefix": "b:selected", 457 | "body": "@selected(${1:true})", 458 | "description": "The @selected directive may be used to indicate if a given select option should be \"selected\" (Laravel 9.x)" 459 | }, 460 | "disabled": { 461 | "prefix": "b:disabled", 462 | "body": "@disabled(${1:true})", 463 | "description": "The @disabled directive may be used to indicate if a given element should be \"disabled\" (Laravel 9.x)" 464 | }, 465 | "style": { 466 | "prefix": "b:style", 467 | "body": "@style($1)", 468 | "description": "The @style directive may be used to conditionally add inline CSS styles to an HTML element (Laravel 9.x)" 469 | }, 470 | "readonly": { 471 | "prefix": "b:readonly", 472 | "body": "@readonly(${1:true})", 473 | "description": "The @readonly directive may be used to indicate if a given element should be \"readonly\" (Laravel 9.x)" 474 | }, 475 | "required": { 476 | "prefix": "b:required", 477 | "body": "@required(${1:true})", 478 | "description": "The @required directive may be used to indicate if a given element should be \"required\" (Laravel 9.x)" 479 | }, 480 | "pushOnce": { 481 | "prefix": "b:pushOnce", 482 | "body": [ 483 | "@pushOnce('${1:scripts}')", 484 | " $2", 485 | "@endPushOnce" 486 | ], 487 | "description": "Combine @once and @push for convenience (Laravel 9.x)" 488 | }, 489 | "pushIf": { 490 | "prefix": "b:pushIf", 491 | "body": [ 492 | "@pushIf(${1:\\$condition}, '${2:scripts}')", 493 | " $3", 494 | "@endPushIf" 495 | ], 496 | "description": "Combine @if and @push for convenience" 497 | }, 498 | "prepend": { 499 | "prefix": "b:prepend", 500 | "body": [ 501 | "@prepend('${1:scripts}')", 502 | " $2", 503 | "@endprepend" 504 | ], 505 | "description": "prepend content to a stack" 506 | }, 507 | "prependOnce": { 508 | "prefix": "b:prependOnce", 509 | "body": [ 510 | "@prependOnce('${1:scripts}')", 511 | " $2", 512 | "@endPrependOnce" 513 | ], 514 | "description": "Combine @once and @prepend for convenience (Laravel 9.x)" 515 | }, 516 | /* v10.x */ 517 | "session": { 518 | "prefix": "b:session", 519 | "body": [ 520 | "@session('${1:status}')", 521 | " $2", 522 | "@endsession" 523 | ], 524 | "description": "The @session directive may be used to determine if a session value exists (Laravel 10.x)" 525 | }, 526 | "use": { 527 | "prefix": "b:use", 528 | "body": "@use('${1:App\\Models\\Flight}')", 529 | "description": "The @use directive may be used to import a class (Laravel 10.x)" 530 | }, 531 | "use-alias": { 532 | "prefix": "b:use-alias", 533 | "body": "@use('${1:App\\Models\\Flight}', '${2:FlightModel}')", 534 | "description": "The @use directive may be used to import a class as specified alias name (Laravel 10.x)" 535 | } 536 | } 537 | -------------------------------------------------------------------------------- /server/src/modes/javascriptMode.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; 8 | import { SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions } from 'vscode-languageserver-types'; 9 | import { LanguageMode, Settings } from './languageModes'; 10 | import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings'; 11 | import { HTMLDocumentRegions } from './embeddedSupport'; 12 | 13 | import * as ts from 'typescript'; 14 | import { join } from 'path'; 15 | 16 | const FILE_NAME = 'vscode://javascript/1'; // the same 'file' is used for all contents 17 | const JQUERY_D_TS = join(__dirname, '../../lib/jquery.d.ts'); 18 | 19 | const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; 20 | 21 | export function getJavascriptMode(documentRegions: LanguageModelCache): LanguageMode { 22 | let jsDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('javascript')); 23 | 24 | let compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic }; 25 | let currentTextDocument: TextDocument; 26 | let scriptFileVersion: number = 0; 27 | function updateCurrentTextDocument(doc: TextDocument) { 28 | if (!currentTextDocument || doc.uri !== currentTextDocument.uri || doc.version !== currentTextDocument.version) { 29 | currentTextDocument = jsDocuments.get(doc); 30 | scriptFileVersion++; 31 | } 32 | } 33 | const host: ts.LanguageServiceHost = { 34 | getCompilationSettings: () => compilerOptions, 35 | getScriptFileNames: () => [FILE_NAME, JQUERY_D_TS], 36 | getScriptKind: () => ts.ScriptKind.JS, 37 | getScriptVersion: (fileName: string) => { 38 | if (fileName === FILE_NAME) { 39 | return String(scriptFileVersion); 40 | } 41 | return '1'; // default lib an jquery.d.ts are static 42 | }, 43 | getScriptSnapshot: (fileName: string) => { 44 | let text = ''; 45 | if (startsWith(fileName, 'vscode:')) { 46 | if (fileName === FILE_NAME) { 47 | text = currentTextDocument.getText(); 48 | } 49 | } else { 50 | text = ts.sys.readFile(fileName) || ''; 51 | } 52 | return { 53 | getText: (start, end) => text.substring(start, end), 54 | getLength: () => text.length, 55 | getChangeRange: () => void 0 56 | }; 57 | }, 58 | getCurrentDirectory: () => '', 59 | getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options) 60 | }; 61 | let jsLanguageService = ts.createLanguageService(host); 62 | 63 | let globalSettings: Settings = {}; 64 | 65 | return { 66 | getId() { 67 | return 'javascript'; 68 | }, 69 | configure(options: any) { 70 | globalSettings = options; 71 | }, 72 | doValidation(document: TextDocument): Diagnostic[] { 73 | updateCurrentTextDocument(document); 74 | const syntaxDiagnostics = jsLanguageService.getSyntacticDiagnostics(FILE_NAME); 75 | const semanticDiagnostics = jsLanguageService.getSemanticDiagnostics(FILE_NAME); 76 | return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => { 77 | return { 78 | range: convertRange(currentTextDocument, diag), 79 | severity: DiagnosticSeverity.Error, 80 | source: 'js', 81 | message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') 82 | }; 83 | }); 84 | }, 85 | doComplete(document: TextDocument, position: Position): CompletionList { 86 | updateCurrentTextDocument(document); 87 | let offset = currentTextDocument.offsetAt(position); 88 | let completions = jsLanguageService.getCompletionsAtPosition(FILE_NAME, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); 89 | if (!completions) { 90 | return { isIncomplete: false, items: [] }; 91 | } 92 | let replaceRange = convertRange(currentTextDocument, getWordAtText(currentTextDocument.getText(), offset, JS_WORD_REGEX)); 93 | return { 94 | isIncomplete: false, 95 | items: completions.entries.map(entry => { 96 | return { 97 | uri: document.uri, 98 | position: position, 99 | label: entry.name, 100 | sortText: entry.sortText, 101 | kind: convertKind(entry.kind), 102 | textEdit: TextEdit.replace(replaceRange, entry.name), 103 | data: { // data used for resolving item details (see 'doResolve') 104 | languageId: 'javascript', 105 | uri: document.uri, 106 | offset: offset 107 | } 108 | }; 109 | }) 110 | }; 111 | }, 112 | doResolve(document: TextDocument, item: CompletionItem): CompletionItem { 113 | updateCurrentTextDocument(document); 114 | let details = jsLanguageService.getCompletionEntryDetails(FILE_NAME, item.data.offset, item.label, undefined, undefined); 115 | if (details) { 116 | item.detail = ts.displayPartsToString(details.displayParts); 117 | item.documentation = ts.displayPartsToString(details.documentation); 118 | delete item.data; 119 | } 120 | return item; 121 | }, 122 | doHover(document: TextDocument, position: Position): Hover | null { 123 | updateCurrentTextDocument(document); 124 | let info = jsLanguageService.getQuickInfoAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); 125 | if (info) { 126 | let contents = ts.displayPartsToString(info.displayParts); 127 | return { 128 | range: convertRange(currentTextDocument, info.textSpan), 129 | contents: MarkedString.fromPlainText(contents) 130 | }; 131 | } 132 | return null; 133 | }, 134 | doSignatureHelp(document: TextDocument, position: Position): SignatureHelp | null { 135 | updateCurrentTextDocument(document); 136 | let signHelp = jsLanguageService.getSignatureHelpItems(FILE_NAME, currentTextDocument.offsetAt(position)); 137 | if (signHelp) { 138 | let ret: SignatureHelp = { 139 | activeSignature: signHelp.selectedItemIndex, 140 | activeParameter: signHelp.argumentIndex, 141 | signatures: [] 142 | }; 143 | signHelp.items.forEach(item => { 144 | 145 | let signature: SignatureInformation = { 146 | label: '', 147 | documentation: undefined, 148 | parameters: [] 149 | }; 150 | 151 | signature.label += ts.displayPartsToString(item.prefixDisplayParts); 152 | item.parameters.forEach((p, i, a) => { 153 | let label = ts.displayPartsToString(p.displayParts); 154 | let parameter: ParameterInformation = { 155 | label: label, 156 | documentation: ts.displayPartsToString(p.documentation) 157 | }; 158 | signature.label += label; 159 | signature.parameters!.push(parameter); 160 | if (i < a.length - 1) { 161 | signature.label += ts.displayPartsToString(item.separatorDisplayParts); 162 | } 163 | }); 164 | signature.label += ts.displayPartsToString(item.suffixDisplayParts); 165 | ret.signatures.push(signature); 166 | }); 167 | return ret; 168 | } 169 | return null; 170 | }, 171 | findDocumentHighlight(document: TextDocument, position: Position): DocumentHighlight[] { 172 | updateCurrentTextDocument(document); 173 | let occurrences = jsLanguageService.getOccurrencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); 174 | if (occurrences) { 175 | return occurrences.map(entry => { 176 | return { 177 | range: convertRange(currentTextDocument, entry.textSpan), 178 | kind: (entry.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Text) 179 | }; 180 | }); 181 | } 182 | return []; 183 | }, 184 | findDocumentSymbols(document: TextDocument): SymbolInformation[] { 185 | updateCurrentTextDocument(document); 186 | let items = jsLanguageService.getNavigationBarItems(FILE_NAME); 187 | if (items) { 188 | let result: SymbolInformation[] = []; 189 | let existing = Object.create(null); 190 | let collectSymbols = (item: ts.NavigationBarItem, containerLabel?: string) => { 191 | let sig = item.text + item.kind + item.spans[0].start; 192 | if (item.kind !== 'script' && !existing[sig]) { 193 | let symbol: SymbolInformation = { 194 | name: item.text, 195 | kind: convertSymbolKind(item.kind), 196 | location: { 197 | uri: document.uri, 198 | range: convertRange(currentTextDocument, item.spans[0]) 199 | }, 200 | containerName: containerLabel 201 | }; 202 | existing[sig] = true; 203 | result.push(symbol); 204 | containerLabel = item.text; 205 | } 206 | 207 | if (item.childItems && item.childItems.length > 0) { 208 | for (let child of item.childItems) { 209 | collectSymbols(child, containerLabel); 210 | } 211 | } 212 | 213 | }; 214 | 215 | items.forEach(item => collectSymbols(item)); 216 | return result; 217 | } 218 | return []; 219 | }, 220 | findDefinition(document: TextDocument, position: Position): Definition | null { 221 | updateCurrentTextDocument(document); 222 | let definition = jsLanguageService.getDefinitionAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); 223 | if (definition) { 224 | return definition.filter(d => d.fileName === FILE_NAME).map(d => { 225 | return { 226 | uri: document.uri, 227 | range: convertRange(currentTextDocument, d.textSpan) 228 | }; 229 | }); 230 | } 231 | return null; 232 | }, 233 | findReferences(document: TextDocument, position: Position): Location[] { 234 | updateCurrentTextDocument(document); 235 | let references = jsLanguageService.getReferencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position)); 236 | if (references) { 237 | return references.filter(d => d.fileName === FILE_NAME).map(d => { 238 | return { 239 | uri: document.uri, 240 | range: convertRange(currentTextDocument, d.textSpan) 241 | }; 242 | }); 243 | } 244 | return []; 245 | }, 246 | format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): TextEdit[] { 247 | currentTextDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true); 248 | scriptFileVersion++; 249 | 250 | let formatterSettings = settings && settings.javascript && settings.javascript.format; 251 | 252 | let initialIndentLevel = computeInitialIndent(document, range, formatParams); 253 | let formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1); 254 | let start = currentTextDocument.offsetAt(range.start); 255 | let end = currentTextDocument.offsetAt(range.end); 256 | let lastLineRange = null; 257 | if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(currentTextDocument.getText().substr(end - range.end.character, range.end.character)))) { 258 | end -= range.end.character; 259 | lastLineRange = Range.create(Position.create(range.end.line, 0), range.end); 260 | } 261 | let edits = jsLanguageService.getFormattingEditsForRange(FILE_NAME, start, end, formatSettings); 262 | if (edits) { 263 | let result = []; 264 | for (let edit of edits) { 265 | if (edit.span.start >= start && edit.span.start + edit.span.length <= end) { 266 | result.push({ 267 | range: convertRange(currentTextDocument, edit.span), 268 | newText: edit.newText 269 | }); 270 | } 271 | } 272 | if (lastLineRange) { 273 | result.push({ 274 | range: lastLineRange, 275 | newText: generateIndent(initialIndentLevel, formatParams) 276 | }); 277 | } 278 | return result; 279 | } 280 | return []; 281 | }, 282 | onDocumentRemoved(document: TextDocument) { 283 | jsDocuments.onDocumentRemoved(document); 284 | }, 285 | dispose() { 286 | jsLanguageService.dispose(); 287 | jsDocuments.dispose(); 288 | } 289 | }; 290 | } 291 | 292 | function convertRange(document: TextDocument, span: { start: number | undefined, length: number | undefined }): Range { 293 | if (typeof span.start === 'undefined') { 294 | const pos = document.positionAt(0); 295 | return Range.create(pos, pos); 296 | } 297 | const startPosition = document.positionAt(span.start); 298 | const endPosition = document.positionAt(span.start + (span.length || 0)); 299 | return Range.create(startPosition, endPosition); 300 | } 301 | 302 | function convertKind(kind: string): CompletionItemKind { 303 | switch (kind) { 304 | case 'primitive type': 305 | case 'keyword': 306 | return CompletionItemKind.Keyword; 307 | case 'var': 308 | case 'local var': 309 | return CompletionItemKind.Variable; 310 | case 'property': 311 | case 'getter': 312 | case 'setter': 313 | return CompletionItemKind.Field; 314 | case 'function': 315 | case 'method': 316 | case 'construct': 317 | case 'call': 318 | case 'index': 319 | return CompletionItemKind.Function; 320 | case 'enum': 321 | return CompletionItemKind.Enum; 322 | case 'module': 323 | return CompletionItemKind.Module; 324 | case 'class': 325 | return CompletionItemKind.Class; 326 | case 'interface': 327 | return CompletionItemKind.Interface; 328 | case 'warning': 329 | return CompletionItemKind.File; 330 | } 331 | 332 | return CompletionItemKind.Property; 333 | } 334 | 335 | function convertSymbolKind(kind: string): SymbolKind { 336 | switch (kind) { 337 | case 'var': 338 | case 'local var': 339 | case 'const': 340 | return SymbolKind.Variable; 341 | case 'function': 342 | case 'local function': 343 | return SymbolKind.Function; 344 | case 'enum': 345 | return SymbolKind.Enum; 346 | case 'module': 347 | return SymbolKind.Module; 348 | case 'class': 349 | return SymbolKind.Class; 350 | case 'interface': 351 | return SymbolKind.Interface; 352 | case 'method': 353 | return SymbolKind.Method; 354 | case 'property': 355 | case 'getter': 356 | case 'setter': 357 | return SymbolKind.Property; 358 | } 359 | return SymbolKind.Variable; 360 | } 361 | 362 | function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeOptions { 363 | return { 364 | ConvertTabsToSpaces: options.insertSpaces, 365 | TabSize: options.tabSize, 366 | IndentSize: options.tabSize, 367 | IndentStyle: ts.IndentStyle.Smart, 368 | NewLineCharacter: '\n', 369 | BaseIndentSize: options.tabSize * initialIndentLevel, 370 | InsertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter), 371 | InsertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements), 372 | InsertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators), 373 | InsertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements), 374 | InsertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions), 375 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), 376 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), 377 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), 378 | InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), 379 | PlaceOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions), 380 | PlaceOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks) 381 | }; 382 | } 383 | 384 | function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) { 385 | let lineStart = document.offsetAt(Position.create(range.start.line, 0)); 386 | let content = document.getText(); 387 | 388 | let i = lineStart; 389 | let nChars = 0; 390 | let tabSize = options.tabSize || 4; 391 | while (i < content.length) { 392 | let ch = content.charAt(i); 393 | if (ch === ' ') { 394 | nChars++; 395 | } else if (ch === '\t') { 396 | nChars += tabSize; 397 | } else { 398 | break; 399 | } 400 | i++; 401 | } 402 | return Math.floor(nChars / tabSize); 403 | } 404 | 405 | function generateIndent(level: number, options: FormattingOptions) { 406 | if (options.insertSpaces) { 407 | return repeat(' ', level * options.tabSize); 408 | } else { 409 | return repeat('\t', level); 410 | } 411 | } -------------------------------------------------------------------------------- /server/src/htmlServerMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType, DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities, Position, CompletionTriggerKind } from 'vscode-languageserver'; 8 | import { TextDocument, Diagnostic, DocumentLink, SymbolInformation, CompletionList } from 'vscode-languageserver-types'; 9 | import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes'; 10 | 11 | import { ConfigurationRequest, ConfigurationParams } from 'vscode-languageserver-protocol/lib/protocol.configuration.proposed'; 12 | import { DocumentColorRequest, ServerCapabilities as CPServerCapabilities, ColorInformation, ColorPresentationRequest } from 'vscode-languageserver-protocol/lib/protocol.colorProvider.proposed'; 13 | import { DidChangeWorkspaceFoldersNotification, WorkspaceFolder } from 'vscode-languageserver-protocol/lib/protocol.workspaceFolders.proposed'; 14 | 15 | import { format } from './modes/formatting'; 16 | import { pushAll } from './utils/arrays'; 17 | import { getDocumentContext } from './utils/documentContext'; 18 | import uri from 'vscode-uri'; 19 | import { formatError, runSafe } from './utils/errors'; 20 | import { doComplete as emmetDoComplete, updateExtensionsPath as updateEmmetExtensionsPath, getEmmetCompletionParticipants } from 'vscode-emmet-helper'; 21 | 22 | namespace TagCloseRequest { 23 | export const type: RequestType = new RequestType('html/tag'); 24 | } 25 | 26 | // Create a connection for the server 27 | let connection: IConnection = createConnection(); 28 | 29 | console.log = connection.console.log.bind(connection.console); 30 | console.error = connection.console.error.bind(connection.console); 31 | 32 | process.on('unhandledRejection', (e: any) => { 33 | connection.console.error(formatError(`Unhandled exception`, e)); 34 | }); 35 | 36 | // Create a simple text document manager. The text document manager 37 | // supports full document sync only 38 | let documents: TextDocuments = new TextDocuments(); 39 | // Make the text document manager listen on the connection 40 | // for open, change and close text document events 41 | documents.listen(connection); 42 | 43 | let workspaceFolders: WorkspaceFolder[] | undefined; 44 | 45 | var languageModes: LanguageModes; 46 | 47 | let clientSnippetSupport = false; 48 | let clientDynamicRegisterSupport = false; 49 | let scopedSettingsSupport = false; 50 | let workspaceFoldersSupport = false; 51 | 52 | var globalSettings: Settings = {}; 53 | let documentSettings: { [key: string]: Thenable } = {}; 54 | // remove document settings on close 55 | documents.onDidClose(e => { 56 | delete documentSettings[e.document.uri]; 57 | }); 58 | 59 | function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable { 60 | if (scopedSettingsSupport && needsDocumentSettings()) { 61 | let promise = documentSettings[textDocument.uri]; 62 | if (!promise) { 63 | let scopeUri = textDocument.uri; 64 | let configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] }; 65 | promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] })); 66 | documentSettings[textDocument.uri] = promise; 67 | } 68 | return promise; 69 | } 70 | return Promise.resolve(void 0); 71 | } 72 | 73 | let emmetSettings = {}; 74 | let currentEmmetExtensionsPath: string; 75 | const emmetTriggerCharacters = ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 76 | 77 | // After the server has started the client sends an initilize request. The server receives 78 | // in the passed params the rootPath of the workspace plus the client capabilites 79 | connection.onInitialize((params: InitializeParams): InitializeResult => { 80 | let initializationOptions = params.initializationOptions; 81 | 82 | workspaceFolders = (params).workspaceFolders; 83 | if (!Array.isArray(workspaceFolders)) { 84 | workspaceFolders = []; 85 | if (params.rootPath) { 86 | workspaceFolders.push({ name: '', uri: uri.file(params.rootPath).toString() }); 87 | } 88 | } 89 | 90 | languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }); 91 | documents.onDidClose(e => { 92 | languageModes.onDocumentRemoved(e.document); 93 | }); 94 | connection.onShutdown(() => { 95 | languageModes.dispose(); 96 | }); 97 | 98 | function hasClientCapability(...keys: string[]) { 99 | let c = params.capabilities; 100 | for (let i = 0; c && i < keys.length; i++) { 101 | c = c[keys[i]]; 102 | } 103 | return !!c; 104 | } 105 | 106 | clientSnippetSupport = hasClientCapability('textDocument', 'completion', 'completionItem', 'snippetSupport'); 107 | clientDynamicRegisterSupport = hasClientCapability('workspace', 'symbol', 'dynamicRegistration'); 108 | scopedSettingsSupport = hasClientCapability('workspace', 'configuration'); 109 | workspaceFoldersSupport = hasClientCapability('workspace', 'workspaceFolders'); 110 | let capabilities: ServerCapabilities & CPServerCapabilities = { 111 | // Tell the client that the server works in FULL text document sync mode 112 | textDocumentSync: documents.syncKind, 113 | completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: [...emmetTriggerCharacters, '.', ':', '<', '"', '=', '/'] } : undefined, 114 | hoverProvider: true, 115 | documentHighlightProvider: true, 116 | documentRangeFormattingProvider: false, 117 | documentSymbolProvider: true, 118 | definitionProvider: true, 119 | signatureHelpProvider: { triggerCharacters: ['('] }, 120 | referencesProvider: true, 121 | colorProvider: true 122 | }; 123 | return { capabilities }; 124 | }); 125 | 126 | connection.onInitialized((p) => { 127 | if (workspaceFoldersSupport) { 128 | connection.client.register(DidChangeWorkspaceFoldersNotification.type); 129 | 130 | connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => { 131 | let toAdd = e.event.added; 132 | let toRemove = e.event.removed; 133 | let updatedFolders = []; 134 | if (workspaceFolders) { 135 | for (let folder of workspaceFolders) { 136 | if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) { 137 | updatedFolders.push(folder); 138 | } 139 | } 140 | } 141 | workspaceFolders = updatedFolders.concat(toAdd); 142 | }); 143 | } 144 | }); 145 | 146 | let formatterRegistration: Thenable | null = null; 147 | 148 | // The settings have changed. Is send on server activation as well. 149 | connection.onDidChangeConfiguration((change) => { 150 | globalSettings = change.settings; 151 | 152 | documentSettings = {}; // reset all document settings 153 | languageModes.getAllModes().forEach(m => { 154 | if (m.configure) { 155 | m.configure(change.settings); 156 | } 157 | }); 158 | documents.all().forEach(triggerValidation); 159 | 160 | // dynamically enable & disable the formatter 161 | if (clientDynamicRegisterSupport) { 162 | let enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; 163 | if (enableFormatter) { 164 | if (!formatterRegistration) { 165 | let documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }]; // don't register razor, the formatter does more harm than good 166 | formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }); 167 | } 168 | } else if (formatterRegistration) { 169 | formatterRegistration.then(r => r.dispose()); 170 | formatterRegistration = null; 171 | } 172 | } 173 | 174 | emmetSettings = globalSettings.emmet; 175 | if (currentEmmetExtensionsPath !== emmetSettings['extensionsPath']) { 176 | currentEmmetExtensionsPath = emmetSettings['extensionsPath']; 177 | const workspaceUri = (workspaceFolders && workspaceFolders.length === 1) ? uri.parse(workspaceFolders[0].uri) : null; 178 | updateEmmetExtensionsPath(currentEmmetExtensionsPath, workspaceUri ? workspaceUri.fsPath : null); 179 | } 180 | }); 181 | 182 | let pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; 183 | const validationDelayMs = 500; 184 | 185 | // The content of a text document has changed. This event is emitted 186 | // when the text document first opened or when its content has changed. 187 | documents.onDidChangeContent(change => { 188 | triggerValidation(change.document); 189 | }); 190 | 191 | // a document has closed: clear all diagnostics 192 | documents.onDidClose(event => { 193 | cleanPendingValidation(event.document); 194 | connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); 195 | }); 196 | 197 | function cleanPendingValidation(textDocument: TextDocument): void { 198 | let request = pendingValidationRequests[textDocument.uri]; 199 | if (request) { 200 | clearTimeout(request); 201 | delete pendingValidationRequests[textDocument.uri]; 202 | } 203 | } 204 | 205 | function triggerValidation(textDocument: TextDocument): void { 206 | cleanPendingValidation(textDocument); 207 | pendingValidationRequests[textDocument.uri] = setTimeout(() => { 208 | delete pendingValidationRequests[textDocument.uri]; 209 | validateTextDocument(textDocument); 210 | }, validationDelayMs); 211 | } 212 | 213 | function isValidationEnabled(languageId: string, settings: Settings = globalSettings) { 214 | let validationSettings = settings && settings.html && settings.html.validate; 215 | if (validationSettings) { 216 | return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false; 217 | } 218 | return true; 219 | } 220 | 221 | async function validateTextDocument(textDocument: TextDocument) { 222 | try { 223 | let diagnostics: Diagnostic[] = []; 224 | if (textDocument.languageId === 'html') { 225 | let modes = languageModes.getAllModesInDocument(textDocument); 226 | let settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation)); 227 | modes.forEach(mode => { 228 | if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) { 229 | pushAll(diagnostics, mode.doValidation(textDocument, settings)); 230 | } 231 | }); 232 | } 233 | connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); 234 | } catch (e) { 235 | connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); 236 | } 237 | } 238 | 239 | let cachedCompletionList: CompletionList; 240 | const hexColorRegex = /^#[\d,a-f,A-F]{1,6}$/; 241 | connection.onCompletion(async textDocumentPosition => { 242 | return runSafe(async () => { 243 | let document = documents.get(textDocumentPosition.textDocument.uri); 244 | let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); 245 | if (!mode || !mode.doComplete) { 246 | return { isIncomplete: true, items: [] }; 247 | } 248 | 249 | if (cachedCompletionList 250 | && !cachedCompletionList.isIncomplete 251 | && (mode.getId() === 'html' || mode.getId() === 'css') 252 | && textDocumentPosition.context 253 | && textDocumentPosition.context.triggerKind === CompletionTriggerKind.TriggerForIncompleteCompletions 254 | ) { 255 | let result: CompletionList = emmetDoComplete(document, textDocumentPosition.position, mode.getId(), emmetSettings); 256 | if (result && result.items) { 257 | result.items.push(...cachedCompletionList.items); 258 | } else { 259 | result = cachedCompletionList; 260 | cachedCompletionList = null; 261 | } 262 | return result; 263 | } 264 | 265 | if (mode.getId() !== 'html') { 266 | connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } }); 267 | } 268 | 269 | cachedCompletionList = null; 270 | let emmetCompletionList: CompletionList = { 271 | isIncomplete: true, 272 | items: undefined 273 | }; 274 | if (mode.setCompletionParticipants) { 275 | const emmetCompletionParticipant = getEmmetCompletionParticipants(document, textDocumentPosition.position, mode.getId(), emmetSettings, emmetCompletionList); 276 | mode.setCompletionParticipants([emmetCompletionParticipant]); 277 | } 278 | 279 | let settings = await getDocumentSettings(document, () => mode.doComplete.length > 2); 280 | let result = mode.doComplete(document, textDocumentPosition.position, settings); 281 | if (emmetCompletionList && emmetCompletionList.items) { 282 | cachedCompletionList = result; 283 | if (emmetCompletionList.items.length && hexColorRegex.test(emmetCompletionList.items[0].label) && result.items.some(x => x.label === emmetCompletionList.items[0].label)) { 284 | emmetCompletionList.items.shift(); 285 | } 286 | return { isIncomplete: true, items: [...emmetCompletionList.items, ...result.items] }; 287 | } 288 | return result; 289 | }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`); 290 | }); 291 | 292 | connection.onCompletionResolve(item => { 293 | return runSafe(() => { 294 | let data = item.data; 295 | if (data && data.languageId && data.uri) { 296 | let mode = languageModes.getMode(data.languageId); 297 | let document = documents.get(data.uri); 298 | if (mode && mode.doResolve && document) { 299 | return mode.doResolve(document, item); 300 | } 301 | } 302 | return item; 303 | }, null, `Error while resolving completion proposal`); 304 | }); 305 | 306 | connection.onHover(textDocumentPosition => { 307 | return runSafe(() => { 308 | let document = documents.get(textDocumentPosition.textDocument.uri); 309 | let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); 310 | if (mode && mode.doHover) { 311 | return mode.doHover(document, textDocumentPosition.position); 312 | } 313 | return null; 314 | }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`); 315 | }); 316 | 317 | connection.onDocumentHighlight(documentHighlightParams => { 318 | return runSafe(() => { 319 | let document = documents.get(documentHighlightParams.textDocument.uri); 320 | let mode = languageModes.getModeAtPosition(document, documentHighlightParams.position); 321 | if (mode && mode.findDocumentHighlight) { 322 | return mode.findDocumentHighlight(document, documentHighlightParams.position); 323 | } 324 | return []; 325 | }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`); 326 | }); 327 | 328 | connection.onDefinition(definitionParams => { 329 | return runSafe(() => { 330 | let document = documents.get(definitionParams.textDocument.uri); 331 | let mode = languageModes.getModeAtPosition(document, definitionParams.position); 332 | if (mode && mode.findDefinition) { 333 | return mode.findDefinition(document, definitionParams.position); 334 | } 335 | return []; 336 | }, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`); 337 | }); 338 | 339 | connection.onReferences(referenceParams => { 340 | return runSafe(() => { 341 | let document = documents.get(referenceParams.textDocument.uri); 342 | let mode = languageModes.getModeAtPosition(document, referenceParams.position); 343 | if (mode && mode.findReferences) { 344 | return mode.findReferences(document, referenceParams.position); 345 | } 346 | return []; 347 | }, [], `Error while computing references for ${referenceParams.textDocument.uri}`); 348 | }); 349 | 350 | connection.onSignatureHelp(signatureHelpParms => { 351 | return runSafe(() => { 352 | let document = documents.get(signatureHelpParms.textDocument.uri); 353 | let mode = languageModes.getModeAtPosition(document, signatureHelpParms.position); 354 | if (mode && mode.doSignatureHelp) { 355 | return mode.doSignatureHelp(document, signatureHelpParms.position); 356 | } 357 | return null; 358 | }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`); 359 | }); 360 | 361 | connection.onDocumentRangeFormatting(async formatParams => { 362 | return runSafe(async () => { 363 | let document = documents.get(formatParams.textDocument.uri); 364 | let settings = await getDocumentSettings(document, () => true); 365 | if (!settings) { 366 | settings = globalSettings; 367 | } 368 | let unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; 369 | let enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; 370 | 371 | return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes); 372 | }, [], `Error while formatting range for ${formatParams.textDocument.uri}`); 373 | }); 374 | 375 | connection.onDocumentLinks(documentLinkParam => { 376 | return runSafe(() => { 377 | let document = documents.get(documentLinkParam.textDocument.uri); 378 | let links: DocumentLink[] = []; 379 | if (document) { 380 | let documentContext = getDocumentContext(document.uri, workspaceFolders); 381 | languageModes.getAllModesInDocument(document).forEach(m => { 382 | if (m.findDocumentLinks) { 383 | pushAll(links, m.findDocumentLinks(document, documentContext)); 384 | } 385 | }); 386 | } 387 | return links; 388 | }, [], `Error while document links for ${documentLinkParam.textDocument.uri}`); 389 | }); 390 | 391 | 392 | 393 | connection.onDocumentSymbol(documentSymbolParms => { 394 | return runSafe(() => { 395 | let document = documents.get(documentSymbolParms.textDocument.uri); 396 | let symbols: SymbolInformation[] = []; 397 | languageModes.getAllModesInDocument(document).forEach(m => { 398 | if (m.findDocumentSymbols) { 399 | pushAll(symbols, m.findDocumentSymbols(document)); 400 | } 401 | }); 402 | return symbols; 403 | }, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`); 404 | }); 405 | 406 | connection.onRequest(DocumentColorRequest.type, params => { 407 | return runSafe(() => { 408 | let infos: ColorInformation[] = []; 409 | let document = documents.get(params.textDocument.uri); 410 | if (document) { 411 | languageModes.getAllModesInDocument(document).forEach(m => { 412 | if (m.findDocumentColors) { 413 | pushAll(infos, m.findDocumentColors(document)); 414 | } 415 | }); 416 | } 417 | return infos; 418 | }, [], `Error while computing document colors for ${params.textDocument.uri}`); 419 | }); 420 | 421 | connection.onRequest(ColorPresentationRequest.type, params => { 422 | return runSafe(() => { 423 | let document = documents.get(params.textDocument.uri); 424 | if (document) { 425 | let mode = languageModes.getModeAtPosition(document, params.range.start); 426 | if (mode && mode.getColorPresentations) { 427 | return mode.getColorPresentations(document, params.color, params.range); 428 | } 429 | } 430 | return []; 431 | }, [], `Error while computing color presentations for ${params.textDocument.uri}`); 432 | }); 433 | 434 | connection.onRequest(TagCloseRequest.type, params => { 435 | return runSafe(() => { 436 | let document = documents.get(params.textDocument.uri); 437 | if (document) { 438 | let pos = params.position; 439 | if (pos.character > 0) { 440 | let mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); 441 | if (mode && mode.doAutoClose) { 442 | return mode.doAutoClose(document, pos); 443 | } 444 | } 445 | } 446 | return null; 447 | }, null, `Error while computing tag close actions for ${params.textDocument.uri}`); 448 | }); 449 | 450 | 451 | // Listen on the connection 452 | connection.listen(); --------------------------------------------------------------------------------