Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Position, Range, TextEdit, workspace, CompletionContext } from 'vscode';
import phpGlobals = require('./phpGlobals');
import phpGlobalFunctions = require('./phpGlobalFunctions');
export default class PHPCompletionItemProvider implements CompletionItemProvider {
public provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken, context: CompletionContext): Promise<CompletionItem[]> {
let result: CompletionItem[] = [];
let shouldProvideCompletionItems = workspace.getConfiguration('php').get<boolean>('suggest.basic', true);
if (!shouldProvideCompletionItems) {
return Promise.resolve(result);
}
let range = document.getWordRangeAtPosition(position);
let prefix = range ? document.getText(range) : '';
if (!range) {
range = new Range(position, position);
}
if (context.triggerCharacter === '>') {
const twoBeforeCursor = new Position(position.line, Math.max(0, position.character - 2));
const previousTwoChars = document.getText(new Range(twoBeforeCursor, position));
if (previousTwoChars !== '->') {
return Promise.resolve(result);
}
}
let added: any = {};
let createNewProposal = function (kind: CompletionItemKind, name: string, entry: phpGlobals.IEntry | null): CompletionItem {
let proposal: CompletionItem = new CompletionItem(name);
proposal.kind = kind;
if (entry) {
if (entry.description) {
proposal.documentation = entry.description;
}
if (entry.signature) {
proposal.detail = entry.signature;
}
}
return proposal;
};
let matches = (name: string) => {
return prefix.length === 0 || name.length >= prefix.length && name.substr(0, prefix.length) === prefix;
};
if (matches('php') && range.start.character >= 2) {
let twoBeforePosition = new Position(range.start.line, range.start.character - 2);
let beforeWord = document.getText(new Range(twoBeforePosition, range.start));
if (beforeWord === '<?') {
let proposal = createNewProposal(CompletionItemKind.Class, '<?php', null);
proposal.textEdit = new TextEdit(new Range(twoBeforePosition, position), '<?php');
result.push(proposal);
return Promise.resolve(result);
}
}
for (let globalvariables in phpGlobals.globalvariables) {
if (phpGlobals.globalvariables.hasOwnProperty(globalvariables) && matches(globalvariables)) {
added[globalvariables] = true;
result.push(createNewProposal(CompletionItemKind.Variable, globalvariables, phpGlobals.globalvariables[globalvariables]));
}
}
for (let globalfunctions in phpGlobalFunctions.globalfunctions) {
if (phpGlobalFunctions.globalfunctions.hasOwnProperty(globalfunctions) && matches(globalfunctions)) {
added[globalfunctions] = true;
result.push(createNewProposal(CompletionItemKind.Function, globalfunctions, phpGlobalFunctions.globalfunctions[globalfunctions]));
}
}
for (let compiletimeconstants in phpGlobals.compiletimeconstants) {
if (phpGlobals.compiletimeconstants.hasOwnProperty(compiletimeconstants) && matches(compiletimeconstants)) {
added[compiletimeconstants] = true;
result.push(createNewProposal(CompletionItemKind.Field, compiletimeconstants, phpGlobals.compiletimeconstants[compiletimeconstants]));
}
}
for (let keywords in phpGlobals.keywords) {
if (phpGlobals.keywords.hasOwnProperty(keywords) && matches(keywords)) {
added[keywords] = true;
result.push(createNewProposal(CompletionItemKind.Keyword, keywords, phpGlobals.keywords[keywords]));
}
}
let text = document.getText();
if (prefix[0] === '$') {
let variableMatch = /\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/g;
let match: RegExpExecArray | null = null;
while (match = variableMatch.exec(text)) {
let word = match[0];
if (!added[word]) {
added[word] = true;
result.push(createNewProposal(CompletionItemKind.Variable, word, null));
}
}
}
let functionMatch = /function\s+([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\(/g;
let match2: RegExpExecArray | null = null;
while (match2 = functionMatch.exec(text)) {
let word2 = match2[1];
if (!added[word2]) {
added[word2] = true;
result.push(createNewProposal(CompletionItemKind.Function, word2, null));
}
}
return Promise.resolve(result);
}
}

View File

@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { HoverProvider, Hover, MarkedString, TextDocument, CancellationToken, Position, workspace } from 'vscode';
import { textToMarkedString } from './utils/markedTextUtil';
import phpGlobals = require('./phpGlobals');
import phpGlobalFunctions = require('./phpGlobalFunctions');
export default class PHPHoverProvider implements HoverProvider {
public provideHover(document: TextDocument, position: Position, _token: CancellationToken): Hover | undefined {
let enable = workspace.getConfiguration('php').get<boolean>('suggest.basic', true);
if (!enable) {
return undefined;
}
let wordRange = document.getWordRangeAtPosition(position);
if (!wordRange) {
return undefined;
}
let name = document.getText(wordRange);
let entry = phpGlobalFunctions.globalfunctions[name] || phpGlobals.compiletimeconstants[name] || phpGlobals.globalvariables[name] || phpGlobals.keywords[name];
if (entry && entry.description) {
let signature = name + (entry.signature || '');
let contents: MarkedString[] = [textToMarkedString(entry.description), { language: 'php', value: signature }];
return new Hover(contents, wordRange);
}
return undefined;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,324 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// file generated from PHP53Schema.xml using php-exclude_generate_php_globals.js
export interface IEntry { description?: string; signature?: string; }
export interface IEntries { [name: string]: IEntry; }
export const globalvariables: IEntries = {
$GLOBALS: {
description: 'An associative array containing references to all variables which are currently defined in the global scope of the script. The variable names are the keys of the array.',
},
$_SERVER: {
description: '$_SERVER is an array containing information such as headers, paths, and script locations. The entries in this array are created by the web server. There is no guarantee that every web server will provide any of these; servers may omit some, or provide others not listed here. That said, a large number of these variables are accounted for in the CGI/1.1 specification, so you should be able to expect those.',
},
$_GET: {
description: 'An associative array of variables passed to the current script via the URL parameters.',
},
$_POST: {
description: 'An associative array of variables passed to the current script via the HTTP POST method.',
},
$_FILES: {
description: 'An associative array of items uploaded to the current script via the HTTP POST method.',
},
$_REQUEST: {
description: 'An associative array that by default contains the contents of $_GET, $_POST and $_COOKIE.',
},
$_SESSION: {
description: 'An associative array containing session variables available to the current script. See the Session functions documentation for more information on how this is used.',
},
$_ENV: {
description: 'An associative array of variables passed to the current script via the environment method. \r\n\r\nThese variables are imported into PHP\'s global namespace from the environment under which the PHP parser is running. Many are provided by the shell under which PHP is running and different systems are likely running different kinds of shells, a definitive list is impossible. Please see your shell\'s documentation for a list of defined environment variables. \r\n\r\nOther environment variables include the CGI variables, placed there regardless of whether PHP is running as a server module or CGI processor.',
},
$_COOKIE: {
description: 'An associative array of variables passed to the current script via HTTP Cookies.',
},
$php_errormsg: {
description: '$php_errormsg is a variable containing the text of the last error message generated by PHP. This variable will only be available within the scope in which the error occurred, and only if the track_errors configuration option is turned on (it defaults to off).',
},
$HTTP_RAW_POST_DATA: {
description: '$HTTP_RAW_POST_DATA contains the raw POST data. See always_populate_raw_post_data',
},
$http_response_header: {
description: 'The $http_response_header array is similar to the get_headers() function. When using the HTTP wrapper, $http_response_header will be populated with the HTTP response headers. $http_response_header will be created in the local scope.',
},
$argc: {
description: 'Contains the number of arguments passed to the current script when running from the command line.',
},
$argv: {
description: 'Contains an array of all the arguments passed to the script when running from the command line.',
},
$this: {
description: 'Refers to the current object',
},
};
export const compiletimeconstants: IEntries = {
__CLASS__: {
description: 'The class name. (Added in PHP 4.3.0) As of PHP 5 this constant returns the class name as it was declared (case-sensitive). In PHP 4 its value is always lowercased.',
},
__DIR__: {
description: 'The directory of the file. If used inside an include, the directory of the included file is returned. This is equivalent to dirname(__FILE__). This directory name does not have a trailing slash unless it is the root directory. (Added in PHP 5.3.0.)',
},
__FILE__: {
description: 'The full path and filename of the file. If used inside an include, the name of the included file is returned. Since PHP 4.0.2, __FILE__ always contains an absolute path with symlinks resolved whereas in older versions it contained relative path under some circumstances.',
},
__FUNCTION__: {
description: 'The function name. (Added in PHP 4.3.0) As of PHP 5 this constant returns the function name as it was declared (case-sensitive). In PHP 4 its value is always lowercased.',
},
__LINE__: {
description: 'The current line number of the file.',
},
__METHOD__: {
description: 'The class method name. (Added in PHP 5.0.0) The method name is returned as it was declared (case-sensitive).',
},
__NAMESPACE__: {
description: 'The name of the current namespace (case-sensitive). This constant is defined in compile-time (Added in PHP 5.3.0).',
},
TRUE: {
},
FALSE: {
},
NULL: {
},
M_PI: {
description: 'The constant Pi: 3.14159265358979323846',
},
M_E: {
description: 'The constant e: 2.7182818284590452354',
},
M_LOG2E: {
description: 'The constant log_2 e: 1.4426950408889634074',
},
M_LOG10E: {
description: 'The constant log_10 e: 0.43429448190325182765',
},
M_LN2: {
description: 'The constant log_e 2: 0.69314718055994530942',
},
M_LN10: {
description: 'The constant log_e 10: 2.30258509299404568402',
},
M_PI_2: {
description: 'The constant pi/2: 1.57079632679489661923',
},
M_PI_4: {
description: 'The constant pi/4: 0.78539816339744830962',
},
M_1_PI: {
description: 'The constant 1/pi: 0.31830988618379067154',
},
M_2_PI: {
description: 'The constant 2/pi: 0.63661977236758134308',
},
M_SQRTPI: {
description: 'The constant sqrt(pi): 1.77245385090551602729',
},
M_2_SQRTPI: {
description: 'The constant 2/sqrt(pi): 1.12837916709551257390',
},
M_SQRT2: {
description: 'The constant sqrt(2): 1.41421356237309504880',
},
M_SQRT3: {
description: 'The constant sqrt(3): 1.73205080756887729352',
},
M_SQRT1_2: {
description: 'The constant 1/sqrt(2): 0.7071067811865475244',
},
M_LNPI: {
description: 'The constant log_e(pi): 1.14472988584940017414',
},
M_EULER: {
description: 'Euler constant: 0.57721566490153286061',
},
PHP_ROUND_HALF_UP: {
description: 'Round halves up = 1',
},
PHP_ROUND_HALF_DOWN: {
description: 'Round halves down = 2',
},
PHP_ROUND_HALF_EVEN: {
description: 'Round halves to even numbers = 3',
},
PHP_ROUND_HALF_ODD: {
description: 'Round halvesto odd numbers = 4',
},
NAN: {
description: 'NAN (as a float): Not A Number',
},
INF: {
description: 'INF (as a float): The infinite',
},
PASSWORD_BCRYPT: {
description: 'PASSWORD_BCRYPT is used to create new password hashes using the CRYPT_BLOWFISH algorithm.',
},
PASSWORD_DEFAULT: {
description: 'The default algorithm to use for hashing if no algorithm is provided. This may change in newer PHP releases when newer, stronger hashing algorithms are supported.',
},
};
export const keywords: IEntries = {
define: {
description: 'Defines a named constant at runtime.',
signature: '( string $name , mixed $value [, bool $case_insensitive = false ] ): bool'
},
die: {
description: 'This language construct is equivalent to exit().',
},
echo: {
description: 'Outputs all parameters. \r\n\r\necho() is not actually a function (it is a language construct), so you are not required to use parentheses with it. echo() (unlike some other language constructs) does not behave like a function, so it cannot always be used in the context of a function. Additionally, if you want to pass more than one parameter to echo(), the parameters must not be enclosed within parentheses.\r\n\r\necho() also has a shortcut syntax, where you can immediately follow the opening tag with an equals sign. This short syntax only works with the short_open_tag configuration setting enabled.',
signature: '( string $arg1 [, string $... ] ): void'
},
empty: {
description: 'Determine whether a variable is considered to be empty.',
signature: '( mixed $var ): bool'
},
exit: {
description: 'Terminates execution of the script. Shutdown functions and object destructors will always be executed even if exit() is called.',
signature: '([ string $status ] )\r\nvoid exit ( int $status ): void'
},
eval: {
description: 'Evaluates the string given in code_str as PHP code. Among other things, this can be useful for storing code in a database text field for later execution.\r\nThere are some factors to keep in mind when using eval(). Remember that the string passed must be valid PHP code, including things like terminating statements with a semicolon so the parser doesn\'t die on the line after the eval(), and properly escaping things in code_str. To mix HTML output and PHP code you can use a closing PHP tag to leave PHP mode.\r\nAlso remember that variables given values under eval() will retain these values in the main script afterwards.',
signature: '( string $code_str ): mixed'
},
include: {
description: 'The include() statement includes and evaluates the specified file.',
},
include_once: {
description: 'The include_once() statement includes and evaluates the specified file during the execution of the script. This is a behavior similar to the include() statement, with the only difference being that if the code from a file has already been included, it will not be included again. As the name suggests, it will be included just once. \r\n\r\ninclude_once() may be used in cases where the same file might be included and evaluated more than once during a particular execution of a script, so in this case it may help avoid problems such as function redefinitions, variable value reassignments, etc.',
},
isset: {
description: 'Determine if a variable is set and is not NULL. \r\n\r\nIf a variable has been unset with unset(), it will no longer be set. isset() will return FALSE if testing a variable that has been set to NULL. Also note that a NULL byte is not equivalent to the PHP NULL constant. \r\n\r\nIf multiple parameters are supplied then isset() will return TRUE only if all of the parameters are set. Evaluation goes from left to right and stops as soon as an unset variable is encountered.',
signature: '( mixed $var [, mixed $... ] ): bool'
},
list: {
description: 'Like array(), this is not really a function, but a language construct. list() is used to assign a list of variables in one operation.',
signature: '( mixed $varname [, mixed $... ] ): array'
},
require: {
description: 'require() is identical to include() except upon failure it will also produce a fatal E_COMPILE_ERROR level error. In other words, it will halt the script whereas include() only emits a warning (E_WARNING) which allows the script to continue.',
},
require_once: {
description: 'The require_once() statement is identical to require() except PHP will check if the file has already been included, and if so, not include (require) it again.',
},
return: {
description: 'If called from within a function, the return() statement immediately ends execution of the current function, and returns its argument as the value of the function call. return() will also end the execution of an eval() statement or script file. \r\n\r\nIf called from the global scope, then execution of the current script file is ended. If the current script file was include()ed or require()ed, then control is passed back to the calling file. Furthermore, if the current script file was include()ed, then the value given to return() will be returned as the value of the include() call. If return() is called from within the main script file, then script execution ends. If the current script file was named by the auto_prepend_file or auto_append_file configuration options in php.ini, then that script file\'s execution is ended.',
},
print: {
description: 'Outputs arg. \r\n\r\nprint() is not actually a real function (it is a language construct) so you are not required to use parentheses with its argument list.',
signature: '( string $arg ): int'
},
unset: {
description: 'unset() destroys the specified variables. \r\n\r\nThe behavior of unset() inside of a function can vary depending on what type of variable you are attempting to destroy. \r\n\r\nIf a globalized variable is unset() inside of a function, only the local variable is destroyed. The variable in the calling environment will retain the same value as before unset() was called.',
signature: '( mixed $var [, mixed $... ] ): void'
},
yield: {
description: 'The heart of a generator function is the yield keyword. In its simplest form, a yield statement looks much like a return statement, except that instead of stopping execution of the function and returning, yield instead provides a value to the code looping over the generator and pauses execution of the generator function.',
},
abstract: {
},
and: {
},
array: {
},
as: {
},
break: {
},
case: {
},
catch: {
},
class: {
},
clone: {
},
const: {
},
continue: {
},
declare: {
},
default: {
},
do: {
},
else: {
},
elseif: {
},
enddeclare: {
},
endfor: {
},
endforeach: {
},
endif: {
},
endswitch: {
},
endwhile: {
},
extends: {
},
final: {
},
finally: {
},
for: {
},
foreach: {
},
function: {
},
global: {
},
goto: {
},
if: {
},
implements: {
},
interface: {
},
instanceof: {
},
insteadOf: {
},
namespace: {
},
new: {
},
or: {
},
parent: {
},
private: {
},
protected: {
},
public: {
},
self: {
},
static: {
},
switch: {
},
throw: {
},
trait: {
},
try: {
},
use: {
},
var: {
},
while: {
},
xor: {
},
};

View File

@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SignatureHelpProvider, SignatureHelp, SignatureInformation, CancellationToken, TextDocument, Position, workspace } from 'vscode';
import phpGlobals = require('./phpGlobals');
import phpGlobalFunctions = require('./phpGlobalFunctions');
const _NL = '\n'.charCodeAt(0);
const _TAB = '\t'.charCodeAt(0);
const _WSB = ' '.charCodeAt(0);
const _LBracket = '['.charCodeAt(0);
const _RBracket = ']'.charCodeAt(0);
const _LCurly = '{'.charCodeAt(0);
const _RCurly = '}'.charCodeAt(0);
const _LParent = '('.charCodeAt(0);
const _RParent = ')'.charCodeAt(0);
const _Comma = ','.charCodeAt(0);
const _Quote = '\''.charCodeAt(0);
const _DQuote = '"'.charCodeAt(0);
const _USC = '_'.charCodeAt(0);
const _a = 'a'.charCodeAt(0);
const _z = 'z'.charCodeAt(0);
const _A = 'A'.charCodeAt(0);
const _Z = 'Z'.charCodeAt(0);
const _0 = '0'.charCodeAt(0);
const _9 = '9'.charCodeAt(0);
const BOF = 0;
class BackwardIterator {
private lineNumber: number;
private offset: number;
private line: string;
private model: TextDocument;
constructor(model: TextDocument, offset: number, lineNumber: number) {
this.lineNumber = lineNumber;
this.offset = offset;
this.line = model.lineAt(this.lineNumber).text;
this.model = model;
}
public hasNext(): boolean {
return this.lineNumber >= 0;
}
public next(): number {
if (this.offset < 0) {
if (this.lineNumber > 0) {
this.lineNumber--;
this.line = this.model.lineAt(this.lineNumber).text;
this.offset = this.line.length - 1;
return _NL;
}
this.lineNumber = -1;
return BOF;
}
let ch = this.line.charCodeAt(this.offset);
this.offset--;
return ch;
}
}
export default class PHPSignatureHelpProvider implements SignatureHelpProvider {
public provideSignatureHelp(document: TextDocument, position: Position, _token: CancellationToken): Promise<SignatureHelp> | null {
let enable = workspace.getConfiguration('php').get<boolean>('suggest.basic', true);
if (!enable) {
return null;
}
let iterator = new BackwardIterator(document, position.character - 1, position.line);
let paramCount = this.readArguments(iterator);
if (paramCount < 0) {
return null;
}
let ident = this.readIdent(iterator);
if (!ident) {
return null;
}
let entry = phpGlobalFunctions.globalfunctions[ident] || phpGlobals.keywords[ident];
if (!entry || !entry.signature) {
return null;
}
let paramsString = entry.signature.substring(0, entry.signature.lastIndexOf(')') + 1);
let signatureInfo = new SignatureInformation(ident + paramsString, entry.description);
let re = /\w*\s+\&?\$[\w_\.]+|void/g;
let match: RegExpExecArray | null = null;
while ((match = re.exec(paramsString)) !== null) {
signatureInfo.parameters.push({ label: match[0], documentation: '' });
}
let ret = new SignatureHelp();
ret.signatures.push(signatureInfo);
ret.activeSignature = 0;
ret.activeParameter = Math.min(paramCount, signatureInfo.parameters.length - 1);
return Promise.resolve(ret);
}
private readArguments(iterator: BackwardIterator): number {
let parentNesting = 0;
let bracketNesting = 0;
let curlyNesting = 0;
let paramCount = 0;
while (iterator.hasNext()) {
let ch = iterator.next();
switch (ch) {
case _LParent:
parentNesting--;
if (parentNesting < 0) {
return paramCount;
}
break;
case _RParent: parentNesting++; break;
case _LCurly: curlyNesting--; break;
case _RCurly: curlyNesting++; break;
case _LBracket: bracketNesting--; break;
case _RBracket: bracketNesting++; break;
case _DQuote:
case _Quote:
while (iterator.hasNext() && ch !== iterator.next()) {
// find the closing quote or double quote
}
break;
case _Comma:
if (!parentNesting && !bracketNesting && !curlyNesting) {
paramCount++;
}
break;
}
}
return -1;
}
private isIdentPart(ch: number): boolean {
if (ch === _USC || // _
ch >= _a && ch <= _z || // a-z
ch >= _A && ch <= _Z || // A-Z
ch >= _0 && ch <= _9 || // 0/9
ch >= 0x80 && ch <= 0xFFFF) { // nonascii
return true;
}
return false;
}
private readIdent(iterator: BackwardIterator): string {
let identStarted = false;
let ident = '';
while (iterator.hasNext()) {
let ch = iterator.next();
if (!identStarted && (ch === _WSB || ch === _TAB || ch === _NL)) {
continue;
}
if (this.isIdentPart(ch)) {
identStarted = true;
ident = String.fromCharCode(ch) + ident;
} else if (identStarted) {
return ident;
}
}
return ident;
}
}

View File

@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ITask<T> {
(): T;
}
/**
* A helper to prevent accumulation of sequential async tasks.
*
* Imagine a mail man with the sole task of delivering letters. As soon as
* a letter submitted for delivery, he drives to the destination, delivers it
* and returns to his base. Imagine that during the trip, N more letters were submitted.
* When the mail man returns, he picks those N letters and delivers them all in a
* single trip. Even though N+1 submissions occurred, only 2 deliveries were made.
*
* The throttler implements this via the queue() method, by providing it a task
* factory. Following the example:
*
* var throttler = new Throttler();
* var letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* throttler.queue(() => { return makeTheTrip(); });
* }
*/
export class Throttler<T> {
private activePromise: Promise<T> | null;
private queuedPromise: Promise<T> | null;
private queuedPromiseFactory: ITask<Promise<T>> | null;
constructor() {
this.activePromise = null;
this.queuedPromise = null;
this.queuedPromiseFactory = null;
}
public queue(promiseFactory: ITask<Promise<T>>): Promise<T> {
if (this.activePromise) {
this.queuedPromiseFactory = promiseFactory;
if (!this.queuedPromise) {
let onComplete = () => {
this.queuedPromise = null;
let result = this.queue(this.queuedPromiseFactory!);
this.queuedPromiseFactory = null;
return result;
};
this.queuedPromise = new Promise<T>((resolve) => {
this.activePromise!.then(onComplete, onComplete).then(resolve);
});
}
return new Promise<T>((resolve, reject) => {
this.queuedPromise!.then(resolve, reject);
});
}
this.activePromise = promiseFactory();
return new Promise<T>((resolve, reject) => {
this.activePromise!.then((result: T) => {
this.activePromise = null;
resolve(result);
}, (err: any) => {
this.activePromise = null;
reject(err);
});
});
}
}
/**
* A helper to delay execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so the he decides not to make the trip
* as soon as a letter is submitted. Instead he waits a while, in case more
* letters are submitted. After said waiting period, if no letters were submitted, he
* decides to make the trip. Imagine that N more letters were submitted after the first
* one, all within a short period of time between each other. Even though N+1
* submissions occurred, only 1 delivery was made.
*
* The delayer offers this behavior via the trigger() method, into which both the task
* to be executed and the waiting period (delay) must be passed in as arguments. Following
* the example:
*
* var delayer = new Delayer(WAITING_PERIOD);
* var letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* delayer.trigger(() => { return makeTheTrip(); });
* }
*/
export class Delayer<T> {
public defaultDelay: number;
private timeout: NodeJS.Timer | null;
private completionPromise: Promise<T> | null;
private onResolve: ((value: T | PromiseLike<T> | undefined) => void) | null;
private task: ITask<T> | null;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onResolve = null;
this.task = null;
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T> {
this.task = task;
this.cancelTimeout();
if (!this.completionPromise) {
this.completionPromise = new Promise<T | undefined>((resolve) => {
this.onResolve = resolve;
}).then(() => {
this.completionPromise = null;
this.onResolve = null;
let result = this.task!();
this.task = null;
return result;
});
}
this.timeout = setTimeout(() => {
this.timeout = null;
this.onResolve!(undefined);
}, delay);
return this.completionPromise;
}
public isTriggered(): boolean {
return this.timeout !== null;
}
public cancel(): void {
this.cancelTimeout();
if (this.completionPromise) {
this.completionPromise = null;
}
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
/**
* A helper to delay execution of a task that is being requested often, while
* preventing accumulation of consecutive executions, while the task runs.
*
* Simply combine the two mail man strategies from the Throttler and Delayer
* helpers, for an analogy.
*/
export class ThrottledDelayer<T> extends Delayer<Promise<T>> {
private throttler: Throttler<T>;
constructor(defaultDelay: number) {
super(defaultDelay);
this.throttler = new Throttler<T>();
}
public trigger(promiseFactory: ITask<Promise<T>>, delay?: number): Promise<Promise<T>> {
return super.trigger(() => this.throttler.queue(promiseFactory), delay);
}
}

View File

@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MarkedString } from 'vscode';
export function textToMarkedString(text: string): MarkedString {
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
}

View File

@ -0,0 +1,314 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { StringDecoder } from 'string_decoder';
import * as vscode from 'vscode';
import { ThrottledDelayer } from './utils/async';
import * as nls from 'vscode-nls';
let localize = nls.loadMessageBundle();
const enum Setting {
Run = 'php.validate.run',
CheckedExecutablePath = 'php.validate.checkedExecutablePath',
Enable = 'php.validate.enable',
ExecutablePath = 'php.validate.executablePath',
}
export class LineDecoder {
private stringDecoder: StringDecoder;
private remaining: string | null;
constructor(encoding: string = 'utf8') {
this.stringDecoder = new StringDecoder(encoding);
this.remaining = null;
}
public write(buffer: Buffer): string[] {
let result: string[] = [];
let value = this.remaining
? this.remaining + this.stringDecoder.write(buffer)
: this.stringDecoder.write(buffer);
if (value.length < 1) {
return result;
}
let start = 0;
let ch: number;
while (start < value.length && ((ch = value.charCodeAt(start)) === 13 || ch === 10)) {
start++;
}
let idx = start;
while (idx < value.length) {
ch = value.charCodeAt(idx);
if (ch === 13 || ch === 10) {
result.push(value.substring(start, idx));
idx++;
while (idx < value.length && ((ch = value.charCodeAt(idx)) === 13 || ch === 10)) {
idx++;
}
start = idx;
} else {
idx++;
}
}
this.remaining = start < value.length ? value.substr(start) : null;
return result;
}
public end(): string | null {
return this.remaining;
}
}
enum RunTrigger {
onSave,
onType
}
namespace RunTrigger {
export let strings = {
onSave: 'onSave',
onType: 'onType'
};
export let from = function (value: string): RunTrigger {
if (value === 'onType') {
return RunTrigger.onType;
} else {
return RunTrigger.onSave;
}
};
}
export default class PHPValidationProvider {
private static MatchExpression: RegExp = /(?:(?:Parse|Fatal) error): (.*)(?: in )(.*?)(?: on line )(\d+)/;
private static BufferArgs: string[] = ['-l', '-n', '-d', 'display_errors=On', '-d', 'log_errors=Off'];
private static FileArgs: string[] = ['-l', '-n', '-d', 'display_errors=On', '-d', 'log_errors=Off', '-f'];
private validationEnabled: boolean;
private executableIsUserDefined: boolean | undefined;
private executable: string | undefined;
private trigger: RunTrigger;
private pauseValidation: boolean;
private documentListener: vscode.Disposable | null = null;
private diagnosticCollection?: vscode.DiagnosticCollection;
private delayers?: { [key: string]: ThrottledDelayer<void> };
constructor(private workspaceStore: vscode.Memento) {
this.executable = undefined;
this.validationEnabled = true;
this.trigger = RunTrigger.onSave;
this.pauseValidation = false;
}
public activate(subscriptions: vscode.Disposable[]) {
this.diagnosticCollection = vscode.languages.createDiagnosticCollection();
subscriptions.push(this);
vscode.workspace.onDidChangeConfiguration(this.loadConfiguration, this, subscriptions);
this.loadConfiguration();
vscode.workspace.onDidOpenTextDocument(this.triggerValidate, this, subscriptions);
vscode.workspace.onDidCloseTextDocument((textDocument) => {
this.diagnosticCollection!.delete(textDocument.uri);
delete this.delayers![textDocument.uri.toString()];
}, null, subscriptions);
subscriptions.push(vscode.commands.registerCommand('php.untrustValidationExecutable', this.untrustValidationExecutable, this));
}
public dispose(): void {
if (this.diagnosticCollection) {
this.diagnosticCollection.clear();
this.diagnosticCollection.dispose();
}
if (this.documentListener) {
this.documentListener.dispose();
this.documentListener = null;
}
}
private loadConfiguration(): void {
let section = vscode.workspace.getConfiguration();
let oldExecutable = this.executable;
if (section) {
this.validationEnabled = section.get<boolean>(Setting.Enable, true);
let inspect = section.inspect<string>(Setting.ExecutablePath);
if (inspect && inspect.workspaceValue) {
this.executable = inspect.workspaceValue;
this.executableIsUserDefined = false;
} else if (inspect && inspect.globalValue) {
this.executable = inspect.globalValue;
this.executableIsUserDefined = true;
} else {
this.executable = undefined;
this.executableIsUserDefined = undefined;
}
this.trigger = RunTrigger.from(section.get<string>(Setting.Run, RunTrigger.strings.onSave));
}
if (this.executableIsUserDefined !== true && this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined) !== undefined) {
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true);
}
this.delayers = Object.create(null);
if (this.pauseValidation) {
this.pauseValidation = oldExecutable === this.executable;
}
if (this.documentListener) {
this.documentListener.dispose();
this.documentListener = null;
}
this.diagnosticCollection!.clear();
if (this.validationEnabled) {
if (this.trigger === RunTrigger.onType) {
this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => {
this.triggerValidate(e.document);
});
} else {
this.documentListener = vscode.workspace.onDidSaveTextDocument(this.triggerValidate, this);
}
// Configuration has changed. Reevaluate all documents.
vscode.workspace.textDocuments.forEach(this.triggerValidate, this);
}
}
private untrustValidationExecutable() {
this.workspaceStore.update(Setting.CheckedExecutablePath, undefined);
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', false);
}
private triggerValidate(textDocument: vscode.TextDocument): void {
if (textDocument.languageId !== 'php' || this.pauseValidation || !this.validationEnabled) {
return;
}
interface MessageItem extends vscode.MessageItem {
id: string;
}
let trigger = () => {
let key = textDocument.uri.toString();
let delayer = this.delayers![key];
if (!delayer) {
delayer = new ThrottledDelayer<void>(this.trigger === RunTrigger.onType ? 250 : 0);
this.delayers![key] = delayer;
}
delayer.trigger(() => this.doValidate(textDocument));
};
if (this.executableIsUserDefined !== undefined && !this.executableIsUserDefined) {
let checkedExecutablePath = this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined);
if (!checkedExecutablePath || checkedExecutablePath !== this.executable) {
vscode.window.showInformationMessage<MessageItem>(
localize('php.useExecutablePath', 'Do you allow {0} (defined as a workspace setting) to be executed to lint PHP files?', this.executable),
{
title: localize('php.yes', 'Allow'),
id: 'yes'
},
{
title: localize('php.no', 'Disallow'),
isCloseAffordance: true,
id: 'no'
}
).then(selected => {
if (!selected || selected.id === 'no') {
this.pauseValidation = true;
} else if (selected.id === 'yes') {
this.workspaceStore.update(Setting.CheckedExecutablePath, this.executable);
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true);
trigger();
}
});
return;
}
}
trigger();
}
private doValidate(textDocument: vscode.TextDocument): Promise<void> {
return new Promise<void>((resolve) => {
let executable = this.executable || 'php';
let decoder = new LineDecoder();
let diagnostics: vscode.Diagnostic[] = [];
let processLine = (line: string) => {
let matches = line.match(PHPValidationProvider.MatchExpression);
if (matches) {
let message = matches[1];
let line = parseInt(matches[3]) - 1;
let diagnostic: vscode.Diagnostic = new vscode.Diagnostic(
new vscode.Range(line, 0, line, Number.MAX_VALUE),
message
);
diagnostics.push(diagnostic);
}
};
let options = (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]) ? { cwd: vscode.workspace.workspaceFolders[0].uri.fsPath } : undefined;
let args: string[];
if (this.trigger === RunTrigger.onSave) {
args = PHPValidationProvider.FileArgs.slice(0);
args.push(textDocument.fileName);
} else {
args = PHPValidationProvider.BufferArgs;
}
try {
let childProcess = cp.spawn(executable, args, options);
childProcess.on('error', (error: Error) => {
if (this.pauseValidation) {
resolve();
return;
}
this.showError(error, executable);
this.pauseValidation = true;
resolve();
});
if (childProcess.pid) {
if (this.trigger === RunTrigger.onType) {
childProcess.stdin.write(textDocument.getText());
childProcess.stdin.end();
}
childProcess.stdout.on('data', (data: Buffer) => {
decoder.write(data).forEach(processLine);
});
childProcess.stdout.on('end', () => {
let line = decoder.end();
if (line) {
processLine(line);
}
this.diagnosticCollection!.set(textDocument.uri, diagnostics);
resolve();
});
} else {
resolve();
}
} catch (error) {
this.showError(error, executable);
}
});
}
private async showError(error: any, executable: string): Promise<void> {
let message: string | null = null;
if (error.code === 'ENOENT') {
if (this.executable) {
message = localize('wrongExecutable', 'Cannot validate since {0} is not a valid php executable. Use the setting \'php.validate.executablePath\' to configure the PHP executable.', executable);
} else {
message = localize('noExecutable', 'Cannot validate since no PHP executable is set. Use the setting \'php.validate.executablePath\' to configure the PHP executable.');
}
} else {
message = error.message ? error.message : localize('unknownReason', 'Failed to run php using path: {0}. Reason is unknown.', executable);
}
if (!message) {
return;
}
const openSettings = localize('goToSetting', 'Open Settings');
if (await vscode.window.showInformationMessage(message, openSettings) === openSettings) {
vscode.commands.executeCommand('workbench.action.openSettings', Setting.ExecutablePath);
}
}
}

View File

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import PHPCompletionItemProvider from './features/completionItemProvider';
import PHPHoverProvider from './features/hoverProvider';
import PHPSignatureHelpProvider from './features/signatureHelpProvider';
import PHPValidationProvider from './features/validationProvider';
export function activate(context: vscode.ExtensionContext): any {
let validator = new PHPValidationProvider(context.workspaceState);
validator.activate(context.subscriptions);
// add providers
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('php', new PHPCompletionItemProvider(), '>', '$'));
context.subscriptions.push(vscode.languages.registerHoverProvider('php', new PHPHoverProvider()));
context.subscriptions.push(vscode.languages.registerSignatureHelpProvider('php', new PHPSignatureHelpProvider(), '(', ','));
// need to set in the extension host as well as the completion provider uses it.
vscode.languages.setLanguageConfiguration('php', {
wordPattern: /(-?\d*\.\d\w*)|([^\-\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
onEnterRules: [
{
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: ' * ' }
},
{
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction: vscode.IndentAction.None, appendText: ' * ' }
},
{
// e.g. * ...|
beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
action: { indentAction: vscode.IndentAction.None, appendText: '* ' }
},
{
// e.g. */|
beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 }
},
{
// e.g. *-----*/|
beforeText: /^(\t|(\ \ ))*\ \*[^/]*\*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 }
}
]
});
}

View File

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer;
declare function clearTimeout(timeoutId: NodeJS.Timer): void;
declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer;
declare function clearInterval(intervalId: NodeJS.Timer): void;
declare function setImmediate(callback: (...args: any[]) => void, ...args: any[]): any;
declare function clearImmediate(immediateId: any): void;

View File

@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference types='@types/node'/>