Fix #46148 - split PHP extension

This commit is contained in:
Rob Lourens
2018-03-22 23:19:10 -07:00
parent 213dcf8f87
commit ab867d907f
19 changed files with 95 additions and 77 deletions

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Position, Range, TextEdit, workspace, CompletionContext } from 'vscode';
import phpGlobals = require('./phpGlobals');
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);
}
var range = document.getWordRangeAtPosition(position);
var 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);
}
}
var added: any = {};
var createNewProposal = function (kind: CompletionItemKind, name: string, entry: phpGlobals.IEntry | null): CompletionItem {
var 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;
};
var 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 (var globalvariables in phpGlobals.globalvariables) {
if (phpGlobals.globalvariables.hasOwnProperty(globalvariables) && matches(globalvariables)) {
added[globalvariables] = true;
result.push(createNewProposal(CompletionItemKind.Variable, globalvariables, phpGlobals.globalvariables[globalvariables]));
}
}
for (var globalfunctions in phpGlobals.globalfunctions) {
if (phpGlobals.globalfunctions.hasOwnProperty(globalfunctions) && matches(globalfunctions)) {
added[globalfunctions] = true;
result.push(createNewProposal(CompletionItemKind.Function, globalfunctions, phpGlobals.globalfunctions[globalfunctions]));
}
}
for (var compiletimeconstants in phpGlobals.compiletimeconstants) {
if (phpGlobals.compiletimeconstants.hasOwnProperty(compiletimeconstants) && matches(compiletimeconstants)) {
added[compiletimeconstants] = true;
result.push(createNewProposal(CompletionItemKind.Field, compiletimeconstants, phpGlobals.compiletimeconstants[compiletimeconstants]));
}
}
for (var keywords in phpGlobals.keywords) {
if (phpGlobals.keywords.hasOwnProperty(keywords) && matches(keywords)) {
added[keywords] = true;
result.push(createNewProposal(CompletionItemKind.Keyword, keywords, phpGlobals.keywords[keywords]));
}
}
var text = document.getText();
if (prefix[0] === '$') {
var variableMatch = /\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/g;
var match: RegExpExecArray | null = null;
while (match = variableMatch.exec(text)) {
var word = match[0];
if (!added[word]) {
added[word] = true;
result.push(createNewProposal(CompletionItemKind.Variable, word, null));
}
}
}
var functionMatch = /function\s+([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\(/g;
var match2: RegExpExecArray | null = null;
while (match2 = functionMatch.exec(text)) {
var 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,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { HoverProvider, Hover, MarkedString, TextDocument, CancellationToken, Position, workspace } from 'vscode';
import phpGlobals = require('./phpGlobals');
import { textToMarkedString } from './utils/markedTextUtil';
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);
var entry = phpGlobals.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,174 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { SignatureHelpProvider, SignatureHelp, SignatureInformation, CancellationToken, TextDocument, Position, workspace } from 'vscode';
import phpGlobals = require('./phpGlobals');
var _NL = '\n'.charCodeAt(0);
var _TAB = '\t'.charCodeAt(0);
var _WSB = ' '.charCodeAt(0);
var _LBracket = '['.charCodeAt(0);
var _RBracket = ']'.charCodeAt(0);
var _LCurly = '{'.charCodeAt(0);
var _RCurly = '}'.charCodeAt(0);
var _LParent = '('.charCodeAt(0);
var _RParent = ')'.charCodeAt(0);
var _Comma = ','.charCodeAt(0);
var _Quote = '\''.charCodeAt(0);
var _DQuote = '"'.charCodeAt(0);
var _USC = '_'.charCodeAt(0);
var _a = 'a'.charCodeAt(0);
var _z = 'z'.charCodeAt(0);
var _A = 'A'.charCodeAt(0);
var _Z = 'Z'.charCodeAt(0);
var _0 = '0'.charCodeAt(0);
var _9 = '9'.charCodeAt(0);
var 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;
}
var 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;
}
var iterator = new BackwardIterator(document, position.character - 1, position.line);
var paramCount = this.readArguments(iterator);
if (paramCount < 0) {
return null;
}
var ident = this.readIdent(iterator);
if (!ident) {
return null;
}
var entry = phpGlobals.globalfunctions[ident] || phpGlobals.keywords[ident];
if (!entry || !entry.signature) {
return null;
}
var paramsString = entry.signature.substring(0, entry.signature.lastIndexOf(')') + 1);
let signatureInfo = new SignatureInformation(ident + paramsString, entry.description);
var re = /\w*\s+\&?\$[\w_\.]+|void/g;
var 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 {
var parentNesting = 0;
var bracketNesting = 0;
var curlyNesting = 0;
var paramCount = 0;
while (iterator.hasNext()) {
var 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 {
var identStarted = false;
var ident = '';
while (iterator.hasNext()) {
var 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) {
var onComplete = () => {
this.queuedPromise = null;
var 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 | Thenable<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>((resolve) => {
this.onResolve = resolve;
}).then(() => {
this.completionPromise = null;
this.onResolve = null;
var 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,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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,305 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as cp from 'child_process';
import { NodeStringDecoder, StringDecoder } from 'string_decoder';
import * as vscode from 'vscode';
import { ThrottledDelayer } from './utils/async';
import * as nls from 'vscode-nls';
let localize = nls.loadMessageBundle();
export class LineDecoder {
private stringDecoder: NodeStringDecoder;
private remaining: string | null;
constructor(encoding: string = 'utf8') {
this.stringDecoder = new StringDecoder(encoding);
this.remaining = null;
}
public write(buffer: NodeBuffer): string[] {
var result: string[] = [];
var value = this.remaining
? this.remaining + this.stringDecoder.write(buffer)
: this.stringDecoder.write(buffer);
if (value.length < 1) {
return result;
}
var start = 0;
var ch: number;
while (start < value.length && ((ch = value.charCodeAt(start)) === 13 || ch === 10)) {
start++;
}
var 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;
}
};
}
const CheckedExecutablePath = 'php.validate.checkedExecutablePath';
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('php');
let oldExecutable = this.executable;
if (section) {
this.validationEnabled = section.get<boolean>('validate.enable', true);
let inspect = section.inspect<string>('validate.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>('validate.run', RunTrigger.strings.onSave));
}
if (this.executableIsUserDefined !== true && this.workspaceStore.get<string | undefined>(CheckedExecutablePath, undefined) !== void 0) {
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(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 !== void 0 && !this.executableIsUserDefined) {
let checkedExecutablePath = this.workspaceStore.get<string | undefined>(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(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.rootPath ? { cwd: vscode.workspace.rootPath } : 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 showError(error: any, executable: string): 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) {
vscode.window.showInformationMessage(message);
}
}
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
var testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: true // colored output from test results
});
module.exports = testRunner;
*/

View File

@@ -0,0 +1,251 @@
import assert = require('assert');
import {CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Range, Position, Uri, workspace, window} from 'vscode';
import PHPCompletionItemProvider from '../features/completionItemProvider';
import HoverProvider from '../features/hoverProvider';
import SignatureHelpProvider from '../features/signatureHelpProvider';
var phpCompletionProvider = new PHPCompletionItemProvider();
var testSuggestionsFor = function(value:string, stringBefore:string):Thenable<CompletionItem[]> {
return workspace.openTextDocument(Uri.parse("untitled:/foo/new.js")).then(document => {
return window.showTextDocument(document).then(textEditor => {
return textEditor.edit(editBuilder => {
var lastLineLength = document.lineAt(document.lineCount - 1).text.length;
editBuilder.replace(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(textEditor.document.lineCount - 1, lastLineLength)), value);
}).then(() => {
var idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
var position = new Position(0, idx);
return phpCompletionProvider.provideCompletionItems(document, position, null);
})
})
});
};
var assertSuggestion= function(completions:CompletionItem[], label:string, kind: CompletionItemKind) {
var entries = completions.filter(suggestion => {
return suggestion.label === label;
});
assert.equal(entries.length, 1);
assert.equal(entries[0].kind, kind);
};
suite("PHP", () => {
test("Intellisense", (testDone:(err?:any) => void) => {
Promise.all([
testSuggestionsFor('<?php ', 'php ').then(completions => {
assertSuggestion(completions, '__CLASS__', CompletionItemKind.Field);
assertSuggestion(completions, 'mysql_thread_id', CompletionItemKind.Function);
assertSuggestion(completions, '$argc', CompletionItemKind.Variable);
}),
testSuggestionsFor('<?php mysql_', 'mysql_').then(completions => {
assertSuggestion(completions, 'mysql_db_name', CompletionItemKind.Function);
assertSuggestion(completions, 'mysql_list_tables', CompletionItemKind.Function);
})
]).then(() => testDone(), (errors:any[]) => {
testDone(errors.reduce((e1, e2) => e1 || e2));
});
});
});
// /*---------------------------------------------------------
// * Copyright (C) Microsoft Corporation. All rights reserved.
// *--------------------------------------------------------*/
// 'use strict';
// import assert = require('assert');
// import {CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Range, Position} from 'vscode';
// var mockPHPWorkerEnv = function (url:Network.URL, content: string) : { worker: phpWorker.PHPWorker; model: mm.MirrorModel } {
// var resourceService = new ResourceService.ResourceService();
// var model = mm.createMirrorModelFromString(null, 0, content, modesUtil.createMockMode('mock.mode.id', /(-?\d*\.\d\w*)|(\$[\w-]*)|([\w-]+)/g), url);
// resourceService.insert(url, model);
// let services = servicesUtil2.createMockEditorWorkerServices({
// resourceService: resourceService,
// });
// var worker = new phpWorker.PHPWorker(modesUtil.createMockMode('mock.mode.id'), [],
// services.resourceService, services.markerService);
// return { worker: worker, model: model };
// };
// var testComputeInfo = function(value:string, stringBefore:string):WinJS.TPromise<Modes.IComputeExtraInfoResult> {
// var url = new Network.URL('test://1');
// var env = mockPHPWorkerEnv(url, value);
// var idx = stringBefore ? value.indexOf(stringBefore) : value.length;
// var position = env.model.getPositionFromOffset(idx);
// return env.worker.computeInfo(url, position);
// };
// var testSuggestionsFor = function(value:string, stringBefore:string):WinJS.TPromise<Modes.ISuggestions> {
// var url = new Network.URL('test://1');
// var env = mockPHPWorkerEnv(url, value);
// var idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
// var position = env.model.getPositionFromOffset(idx);
// return env.worker.suggest(url, position).then(result => result[0]);
// };
// var testParameterHintsFor = function(value:string, stringBefore:string):WinJS.TPromise<Modes.IParameterHints> {
// var url = new Network.URL('test://1');
// var env = mockPHPWorkerEnv(url, value);
// var idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : value.length;
// var position = env.model.getPositionFromOffset(idx);
// return env.worker.getParameterHints(url, position);
// };
// var assertSuggestion= function(completion:Modes.ISuggestions, label:string, type: string) {
// var entries = completion.suggestions.filter(function(suggestion: Modes.ISuggestion) {
// return suggestion.label === label;
// });
// assert.equal(entries.length, 1);
// assert.equal(entries[0].type, type);
// };
// var assertParameterHints= function(hints:Modes.IParameterHints, paramNames: string[], currentParameter: number) {
// assert.equal(hints.signatures.length, 1);
// assert.equal(hints.signatures[0].parameters.length, paramNames.length);
// var i= 0;
// var label = hints.signatures[0].label;
// hints.signatures[0].parameters.forEach(function(param) {
// assert.equal(param.label, paramNames[i++]);
// assert.equal(param.label, label.substring(param.signatureLabelOffset, param.signatureLabelEnd));
// });
// assert.equal(hints.currentParameter, currentParameter);
// };
// suite('PHP - Intellisense', () => {
// test('Globals', function(testDone):any {
// Promise.join([
// testSuggestionsFor('<?php ', 'php ').then(function(completion:Modes.ISuggestions):void {
// assert.equal(completion.currentWord, '');
// assertSuggestion(completion, '__CLASS__', 'field');
// assertSuggestion(completion, 'mysql_thread_id', 'function');
// assertSuggestion(completion, '$argc', 'variable');
// }),
// testSuggestionsFor('<?php mysql_', 'mysql_').then(function(completion:Modes.ISuggestions):void {
// assert.equal(completion.currentWord, 'mysql_');
// assertSuggestion(completion, 'mysql_db_name', 'function');
// assertSuggestion(completion, 'mysql_list_tables', 'function');
// })
// ]).done(() => testDone(), (errors:any[]) => {
// testDone(errors.reduce((e1, e2) => e1 || e2));
// });
// });
// test('Variables', function(testDone):any {
// WinJS.Promise.join([
// testSuggestionsFor('<?php $a = 1; $', '$a = 1; $').then(function(completion:Modes.ISuggestions):void {
// assert.equal(completion.currentWord, '$');
// assertSuggestion(completion, '$a', 'variable');
// })
// ]).done(() => testDone(), (errors:any[]) => {
// testDone(errors.reduce((e1, e2) => e1 || e2));
// });
// });
// });
// suite('PHP - Parameter hints', () => {
// test('Globals', function(testDone): any {
// WinJS.Promise.join([
// testParameterHintsFor('<?php mysql_data_seek(', null).then(function(hints: Modes.IParameterHints): void {
// assertParameterHints(hints, ['$result', '$row_number'], 0);
// }),
// testParameterHintsFor('<?php password_hash(', null).then(function(hints: Modes.IParameterHints): void {
// assertParameterHints(hints, ['$password', '$algo', '$options'], 0);
// }),
// testParameterHintsFor('<?php localtime(', null).then(function(hints: Modes.IParameterHints): void {
// assertParameterHints(hints, ['$timestamp', '$is_associative'], 0);
// }),
// testParameterHintsFor('<?php is_callable(', null).then(function(hints: Modes.IParameterHints): void {
// assertParameterHints(hints, ['$name', '$syntax_only', '&$callable_name'], 0);
// }),
// testParameterHintsFor('<?php array_unshift(', null).then(function(hints: Modes.IParameterHints): void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 0);
// })
// ]).done(() => testDone(), (errors:any[]) => {
// testDone(errors.reduce((e1, e2) => e1 || e2));
// });
// });
// test('With arguments', function(testDone): any {
// WinJS.Promise.join([
// testParameterHintsFor('<?php array_unshift(foo', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 0);
// }),
// testParameterHintsFor('<?php array_unshift(foo, ', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 1);
// }),
// testParameterHintsFor('<?php array_unshift(foo, f[]', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 1);
// }),
// testParameterHintsFor('<?php array_unshift(foo, [a, 2]', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 1);
// }),
// testParameterHintsFor('<?php array_unshift(foo, [a, 2], x', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 2);
// }),
// testParameterHintsFor('<?php array_unshift(foo, [a, 2], x, y', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 2);
// }),
// testParameterHintsFor('<?php array_unshift(foo, f(a, 2), x, y', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 2);
// }),
// testParameterHintsFor('<?php array_unshift(foo, "f(a, 2"', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 1);
// }),
// testParameterHintsFor('<?php array_unshift(foo, \'f(a, 2\'', null).then(function(hints:Modes.IParameterHints):void {
// assertParameterHints(hints, ['&$array', '$var', '$...'], 1);
// })
// ]).done(() => testDone(), (errors:any[]) => {
// testDone(errors.reduce((e1, e2) => e1 || e2));
// });
// });
// test('all function', function(testDone):any {
// var testFunc = function(hints:Modes.IParameterHints):void {
// assert.ok(hints !== null);
// assert.equal(1, hints.signatures.length, name);
// var res= hints.signatures[0].parameters.map(function(param) { return param.label; } ).join(',');
// assert.ok(hints.signatures[0].parameters.length > 0, 'parameters for ' + hints.signatures[0].label + ': ' + res);
// };
// var promises : WinJS.Promise[] = []
// for (var name in phpGlobals.globalfunctions) {
// if (phpGlobals.globalfunctions.hasOwnProperty(name)) {
// var entry = phpGlobals.globalfunctions[name];
// if (entry.signature) {
// promises.push(testParameterHintsFor('<?php ' + name + '(', null).then(testFunc));
// }
// }
// }
// WinJS.Promise.join(promises).done(() => testDone(), (errors) => {
// testDone(errors[0]);
// });
// });
// });
// suite('PHP - compute info', () => {
// test('Globals', function(testDone):any {
// WinJS.Promise.join([
// testComputeInfo('<?php $file=fopen("welcome.txt","r"); ?>', 'fopen').then((hints:Modes.IComputeExtraInfoResult) => {
// assert.ok(!!(hints.value || hints.htmlContent));
// }),
// testComputeInfo('<?php $file=fopen("welcome.txt","r"); ?>', 'welcome').then((hints:Modes.IComputeExtraInfoResult) => {
// assert.ok(hints === null);
// })
// ]).done(() => testDone(), (errors:any[]) => {
// testDone(errors.reduce((e1, e2) => e1 || e2));
// });
// });
// });

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'/>