diff --git a/README.md b/README.md index e1aa6c8..7fe5a10 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,12 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph --- -## What's new in v0.3.2 (latest release) +## What's new in v0.3.3 (latest release) +- Document symbol provider - view top level symbols in the current file +- Workspace symbol provider - view top level symbols throughout the workspace +- Performance improvements + +## What's new in v0.3.2 - **Added go to definition on classes, traits & interfaces** - Fix several bugs introduced in v0.3.1 - Namespace insert text should be prefixed with a backslash _(Thanks @TheColorRed for pointing out this mistake!)_ @@ -60,6 +65,7 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph - Optionally for built-in PHP functions and classes (such as PDO) - **Go to definition** on classes, interfaces and traits - Peek definition on classes, interfaces and traits +- Document & workspace symbol providers ## Planned Features: @@ -67,7 +73,6 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph - Signature provider to show method parameter suggestions - Hover provider to show information about symbol under the cursor - Full go-to/Peek definition on variables, methods, properties, etc -- List symbols - PhpDoc support (both for reading and writing documentation) ## User Feedback diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md index 066f953..8c9331e 100644 --- a/client/CHANGELOG.md +++ b/client/CHANGELOG.md @@ -1,4 +1,9 @@ -# v0.3.2 (latest release) +# v0.3.3 (latest release) +- Document symbol provider - view top level symbols in the current file +- Workspace symbol provider - view top level symbols throughout the workspace +- Performance improvements + +# v0.3.2 - **Added go to definition on classes, traits & interfaces** - Fix several bugs introduced in v0.3.1 - Namespace insert text should be prefixed with a backslash _(Thanks @TheColorRed for pointing out this mistake!)_ diff --git a/client/README.md b/client/README.md index e1aa6c8..7fe5a10 100644 --- a/client/README.md +++ b/client/README.md @@ -24,7 +24,12 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph --- -## What's new in v0.3.2 (latest release) +## What's new in v0.3.3 (latest release) +- Document symbol provider - view top level symbols in the current file +- Workspace symbol provider - view top level symbols throughout the workspace +- Performance improvements + +## What's new in v0.3.2 - **Added go to definition on classes, traits & interfaces** - Fix several bugs introduced in v0.3.1 - Namespace insert text should be prefixed with a backslash _(Thanks @TheColorRed for pointing out this mistake!)_ @@ -60,6 +65,7 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph - Optionally for built-in PHP functions and classes (such as PDO) - **Go to definition** on classes, interfaces and traits - Peek definition on classes, interfaces and traits +- Document & workspace symbol providers ## Planned Features: @@ -67,7 +73,6 @@ You can also set `php.suggest.basic` to `false` to disable VS Code's built-in ph - Signature provider to show method parameter suggestions - Hover provider to show information about symbol under the cursor - Full go-to/Peek definition on variables, methods, properties, etc -- List symbols - PhpDoc support (both for reading and writing documentation) ## User Feedback diff --git a/client/package.json b/client/package.json index 0f2dbf7..4514ba9 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ }, "icon": "images/php-256.png", "license": "MIT", - "version": "0.3.2", + "version": "0.3.3", "publisher": "HvyIndustries", "engines": { "vscode": "^1.8.0" diff --git a/client/phpTest/demo/documentSymbol.php b/client/phpTest/demo/documentSymbol.php new file mode 100644 index 0000000..ed53986 --- /dev/null +++ b/client/phpTest/demo/documentSymbol.php @@ -0,0 +1,71 @@ + }; private todoCommentDecoration: vscode.TextEditorDecorationType; constructor() { + this.delayers = Object.create(null); + let subscriptions: vscode.Disposable[] = []; vscode.workspace.onDidChangeTextDocument((e) => this.onChangeTextHandler(e.document), null, subscriptions); + vscode.workspace.onDidCloseTextDocument((textDocument)=> { delete this.delayers[textDocument.uri.toString()]; }, null, subscriptions); vscode.window.onDidChangeActiveTextEditor(editor => { this.onChangeEditorHandler(editor) }, null, subscriptions); this.disposable = vscode.Disposable.from(...subscriptions); this.todoCommentDecoration = vscode.window.createTextEditorDecorationType({ overviewRulerLane: vscode.OverviewRulerLane.Right, color: "rgba(91, 199, 235, 1)", - overviewRulerColor: 'rgba(144, 195, 212, 0.7)' // Light Blue + overviewRulerColor: 'rgba(144, 195, 212, 0.7)', // Light Blue + isWholeLine: false, + backgroundColor: 'rgba(91, 199, 235, 0.1)' }); this.styleTodoComments(); @@ -32,40 +38,54 @@ export default class QualityOfLife private onChangeEditorHandler(editor: vscode.TextEditor) { + // Only process PHP files + if (editor.document.languageId != "php") return; + this.styleTodoComments(); } - private onChangeTextHandler(textDocument: vscode.TextDocument) - { - // Style todo comments as blue (+ add marker in sidebar) - this.styleTodoComments(); + private onChangeTextHandler(textDocument: vscode.TextDocument) { + // Only process PHP files + if (textDocument.languageId != "php") return; + + let key = textDocument.uri.toString(); + let delayer = this.delayers[key]; + + if (!delayer) { + delayer = new ThrottledDelayer(200); + this.delayers[key] = delayer; + } + + delayer.trigger(() => this.styleTodoComments()); } private styleTodoComments() { - var editor = vscode.window.activeTextEditor; - if (editor == null) return; + return new Promise((resolve, reject) => { + var editor = vscode.window.activeTextEditor; + if (editor == null) return; - // Reset any existing todo style decorations - editor.setDecorations(this.todoCommentDecoration, []); + // Reset any existing todo style decorations + editor.setDecorations(this.todoCommentDecoration, []); - var matchedLines = []; + var matchedLines = []; - // Parse document searching for regex match - for (var i = 0; i < editor.document.lineCount; i++) { - var line = editor.document.lineAt(i); + // Parse document searching for regex match + for (var i = 0; i < Math.min(3000, editor.document.lineCount); i++) { + var line = editor.document.lineAt(i); - var regex = /(\/\/|#)(\stodo|todo)/ig; - var result = regex.exec(line.text); + var regex = /(\/\/|#)(\stodo|todo)/ig; + var result = regex.exec(line.text); - if (result != null) - { - var lineOption = { range: new vscode.Range(i, result.index, i, 99999) }; - matchedLines.push(lineOption); + if (result != null) { + var lineOption = { range: new vscode.Range(i, result.index, i, line.range.end.character) }; + matchedLines.push(lineOption); + } } - } - editor.setDecorations(this.todoCommentDecoration, matchedLines); + editor.setDecorations(this.todoCommentDecoration, matchedLines); + resolve(); + }); } dispose() diff --git a/server/package.json b/server/package.json index f07cae1..f74e836 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "name": "crane-lang-server", "description": "The language server for Crane", - "version": "1.1.2", + "version": "1.1.3", "author": "HVY Industries", "license": "MIT", "engines": { diff --git a/server/src/hvy/treeBuilderV2.ts b/server/src/hvy/treeBuilderV2.ts index 963331e..e178d6e 100644 --- a/server/src/hvy/treeBuilderV2.ts +++ b/server/src/hvy/treeBuilderV2.ts @@ -40,7 +40,7 @@ export class TreeBuilderV2 } else { switch (branch.kind) { case "namespace": - tree.namespaces.push(new NamespaceNode(branch.name)); + this.buildNamespaceDeclaration(branch, tree); this.processBranch(branch.children, tree, branch); break; @@ -145,6 +145,14 @@ export class TreeBuilderV2 } } + private buildNamespaceDeclaration(branch, context: FileNode) + { + let namespace = new NamespaceNode(branch.name); + namespace.startPos = this.buildPosition(branch.loc.start); + namespace.endPos = this.buildPosition(branch.loc.end); + context.namespaces.push(namespace); + } + private buildfileInclude(branch, context: FileNode) { switch (branch.target.kind) { diff --git a/server/src/providers/definition.ts b/server/src/providers/definition.ts index eeb170e..e03b5b5 100644 --- a/server/src/providers/definition.ts +++ b/server/src/providers/definition.ts @@ -119,8 +119,11 @@ export class DefinitionProvider var toReturn = []; - this.workspaceTree.forEach(filenode => { - filenode.classes.forEach((classNode) => { + for (var i = 0, l = this.workspaceTree.length; i < l; i++) { + var filenode = this.workspaceTree[i]; + + for (var j = 0, sl = filenode.classes.length; j < sl; j++) { + var classNode = filenode.classes[j]; if ( classNode.name.toLowerCase() == rawClassname.toLowerCase() && classNode.namespace == namespace @@ -132,8 +135,10 @@ export class DefinitionProvider toReturn.push(nodeInfo); } - }); - filenode.traits.forEach((traitNode) => { + } + + for (var j = 0, sl = filenode.traits.length; j < sl; j++) { + var traitNode = filenode.traits[j]; if ( traitNode.name.toLowerCase() == rawClassname.toLowerCase() && traitNode.namespace == namespace @@ -145,8 +150,10 @@ export class DefinitionProvider toReturn.push(nodeInfo); } - }); - filenode.interfaces.forEach((interfaceNode) => { + } + + for (var j = 0, sl = filenode.interfaces.length; j < sl; j++) { + var interfaceNode = filenode.interfaces[j]; if ( interfaceNode.name.toLowerCase() == rawClassname.toLowerCase() && interfaceNode.namespace == namespace @@ -158,8 +165,8 @@ export class DefinitionProvider toReturn.push(nodeInfo); } - }); - }); + } + } return toReturn; } @@ -168,7 +175,9 @@ export class DefinitionProvider { var toReturn: Location[] = []; - nodes.forEach(item => { + for (var i = 0, l = nodes.length; i < l; i++) { + var item = nodes[i]; + let location: Location = { uri: Files.getUriFromPath(item.path), range: { @@ -184,7 +193,7 @@ export class DefinitionProvider }; toReturn.push(location); - }); + } return toReturn; } diff --git a/server/src/providers/documentSymbol.ts b/server/src/providers/documentSymbol.ts new file mode 100644 index 0000000..0e5a576 --- /dev/null +++ b/server/src/providers/documentSymbol.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Hvy Industries. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * "HVY", "HVY Industries" and "Hvy Industries" are trading names of JCKD (UK) Ltd + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Location, SymbolInformation, SymbolKind } from 'vscode-languageserver'; +import { FileNode, BaseNode, PositionInfo } from "../hvy/nodes"; +import { Files } from "../util/Files"; + +const fs = require('fs'); + +export class DocumentSymbolProvider +{ + private tree: FileNode; + private query: string; + + constructor(tree: FileNode, query: string = null) + { + if (query == "") { + query = null; + } + + if (query != null) { + query = query.toLowerCase(); + } + + this.query = query; + this.tree = tree; + } + + public findSymbols(): SymbolInformation[] + { + // Loop round current filenode + return this.buildSymbolInformation(); + } + + private buildSymbolInformation(): SymbolInformation[] + { + var toReturn: SymbolInformation[] = []; + + for (let i = 0, l = this.tree.functions.length; i < l; i++) { + var functionItem = this.tree.functions[i]; + this.addSymbol(toReturn, functionItem, SymbolKind.Function, null); + } + + for (let i = 0, l = this.tree.namespaces.length; i < l; i++) { + var item = this.tree.namespaces[i]; + this.addSymbol(toReturn, item, SymbolKind.Namespace, null); + } + + for (let i = 0, l = this.tree.classes.length; i < l; i++) { + var classItem = this.tree.classes[i]; + this.addSymbol(toReturn, classItem, SymbolKind.Class, classItem.namespace); + this.buildClassTraitInterfaceBody(classItem, toReturn); + } + + for (let i = 0, l = this.tree.traits.length; i < l; i++) { + var traitItem = this.tree.traits[i]; + this.addSymbol(toReturn, traitItem, SymbolKind.Class, traitItem.namespace); + this.buildClassTraitInterfaceBody(traitItem, toReturn); + } + + for (let i = 0, l = this.tree.interfaces.length; i < l; i++) { + var interfaceItem = this.tree.interfaces[i]; + this.addSymbol(toReturn, interfaceItem, SymbolKind.Interface, interfaceItem.namespace); + this.buildClassTraitInterfaceBody(interfaceItem, toReturn); + } + + return toReturn; + } + + private buildClassTraitInterfaceBody(item, toReturn) + { + if (item.constants) { + for (let i = 0, l = item.constants.length; i < l; i++) { + var constant = item.constants[i]; + this.addSymbol(toReturn, constant, SymbolKind.Constant, item.name); + } + } + + if (item.properties) { + for (let i = 0, l = item.properties.length; i < l; i++) { + var property = item.properties[i]; + this.addSymbol(toReturn, property, SymbolKind.Property, item.name, "$" + property.name); + } + } + + if (item.methods) { + for (let i = 0, l = item.methods.length; i < l; i++) { + var method = item.methods[i]; + this.addSymbol(toReturn, method, SymbolKind.Method, item.name); + } + } + + if (item.construct) { + this.addSymbol(toReturn, item.construct, SymbolKind.Constructor, item.name); + } + } + + private queryMatch(name: string) + { + if (this.query == null) { + return true; + } + + name = name.toLowerCase(); + + if (name == this.query || name.indexOf(this.query) > -1) { + return true; + } + + // Support fuzzy searching + // If the name contains all of the chars in the query, also return true + let nameChars = name.split(""); + let queryChars = this.query.split(""); + + // Note the "!" to reverse the result of some() + // some() will return true if a query char is not found in the name + let matchFound = !queryChars.some(char => { + return nameChars.indexOf(char) == -1; + }); + + return matchFound; + } + + private addSymbol(toReturn:SymbolInformation[], item: BaseNode, kind: SymbolKind, parent: string, name: string = null) + { + if (!name) { + name = item.name + } + + if (!this.queryMatch(name)) { + return; + } + + toReturn.push({ + name: name, + containerName: parent, + kind: kind, + location: this.buildLocation(item.startPos, item.endPos) + }); + } + + private buildLocation(startPos: PositionInfo, endPos: PositionInfo): Location + { + // Handle rare cases where there is no end position + if (endPos == null) { + endPos = startPos; + } + + return { + uri: Files.getUriFromPath(this.tree.path), + range: { + start: { + line: startPos.line, + character: startPos.col + }, + end: { + line: endPos.line, + character: endPos.col + } + } + }; + } +} diff --git a/server/src/providers/workspaceSymbol.ts b/server/src/providers/workspaceSymbol.ts new file mode 100644 index 0000000..29996af --- /dev/null +++ b/server/src/providers/workspaceSymbol.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Hvy Industries. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * "HVY", "HVY Industries" and "Hvy Industries" are trading names of JCKD (UK) Ltd + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { SymbolInformation } from 'vscode-languageserver'; +import { FileNode } from "../hvy/nodes"; +import { DocumentSymbolProvider } from "./documentSymbol"; + +const fs = require('fs'); + +export class WorkspaceSymbolProvider +{ + private workspaceTree: FileNode[]; + private query: string; + + constructor(workspaceTree: FileNode[], query: string) + { + this.workspaceTree = workspaceTree; + this.query = query; + } + + public findSymbols(): SymbolInformation[] + { + var symbols: SymbolInformation[] = []; + + // Execute document symbol provider against every file in the tree + for (var i = 0, l = this.workspaceTree.length; i < l; i++) { + var fileNode = this.workspaceTree[i]; + + let documentSymbolProvider = new DocumentSymbolProvider(fileNode, this.query); + let fileSymbols = documentSymbolProvider.findSymbols(); + symbols = symbols.concat(fileSymbols); + } + + return symbols; + } +} diff --git a/server/src/server.ts b/server/src/server.ts index 0a80c8d..80c3a7d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -23,6 +23,8 @@ import { DefinitionProvider } from "./providers/definition"; import Storage from './util/Storage'; import { Files } from "./util/Files"; +import { DocumentSymbolProvider } from "./providers/documentSymbol"; +import { WorkspaceSymbolProvider } from "./providers/workspaceSymbol"; const util = require('util'); @@ -64,7 +66,9 @@ connection.onInitialize((params): InitializeResult => resolveProvider: true, triggerCharacters: ['.', ':', '$', '>', "\\"] }, - definitionProvider: true + definitionProvider: true, + documentSymbolProvider: true, + workspaceSymbolProvider: true } } }); @@ -131,16 +135,34 @@ connection.onCompletionResolve((item: CompletionItem): CompletionItem => return item; }); -connection.onDefinition((position, cancellationToken) => { +connection.onDefinition((params, cancellationToken) => { return new Promise((resolve, reject) => { - let path = Files.getPathFromUri(position.textDocument.uri); + let path = Files.getPathFromUri(params.textDocument.uri); let filenode = getFileNodeFromPath(path); - let definitionProvider = new DefinitionProvider(position, path, filenode, workspaceTree); + let definitionProvider = new DefinitionProvider(params, path, filenode, workspaceTree); let locations = definitionProvider.findDefinition(); resolve(locations); }); }); +connection.onDocumentSymbol((params, cancellationToken) => { + return new Promise((resolve, reject) => { + let path = Files.getPathFromUri(params.textDocument.uri); + let filenode = getFileNodeFromPath(path); + let documentSymbolProvider = new DocumentSymbolProvider(filenode); + let symbols = documentSymbolProvider.findSymbols(); + resolve(symbols); + }); +}); + +connection.onWorkspaceSymbol((params, cancellationToken) => { + return new Promise((resolve, reject) => { + let workspaceSymbolProvider = new WorkspaceSymbolProvider(workspaceTree, params.query); + let symbols = workspaceSymbolProvider.findSymbols(); + resolve(symbols); + }); +}); + var buildObjectTreeForDocument: RequestType<{path:string,text:string}, any, any, any> = new RequestType("buildObjectTreeForDocument"); connection.onRequest(buildObjectTreeForDocument, (requestObj) => { @@ -335,13 +357,17 @@ function getClassNodeFromTree(className:string): ClassNode { var toReturn = null; - var fileNode = workspaceTree.forEach((fileNode) => { - fileNode.classes.forEach((classNode) => { + for (var i = 0, l = workspaceTree.length; i < l; i++) { + var fileNode = workspaceTree[i]; + + for (var j = 0, sl = fileNode.classes.length; j < sl; j++) { + var classNode = fileNode.classes[j]; + if (classNode.name.toLowerCase() == className.toLowerCase()) { toReturn = classNode; } - }) - }); + } + } return toReturn; } @@ -350,13 +376,17 @@ function getTraitNodeFromTree(traitName: string): ClassNode { var toReturn = null; - var fileNode = workspaceTree.forEach((fileNode) => { - fileNode.traits.forEach((traitNode) => { + for (var i = 0, l = workspaceTree.length; i < l; i++) { + var fileNode = workspaceTree[i]; + + for (var j = 0, sl = fileNode.traits.length; j < sl; j++) { + var traitNode = fileNode.traits[j]; + if (traitNode.name.toLowerCase() == traitName.toLowerCase()) { toReturn = traitNode; } - }) - }); + } + } return toReturn; } @@ -364,11 +394,13 @@ function getTraitNodeFromTree(traitName: string): ClassNode function getFileNodeFromPath(path: string): FileNode { var returnNode = null; - workspaceTree.forEach(fileNode => { + for (var i = 0, l = workspaceTree.length; i < l; i++) { + var fileNode = workspaceTree[i]; + if (fileNode.path == path) { returnNode = fileNode; } - }); + } return returnNode; } diff --git a/server/src/suggestionBuilder.ts b/server/src/suggestionBuilder.ts index 7957781..9bcf0a8 100644 --- a/server/src/suggestionBuilder.ts +++ b/server/src/suggestionBuilder.ts @@ -84,21 +84,29 @@ export class SuggestionBuilder } else if (this.lastChar == ":") { if (this.isSelf()) { // Accessing via self:: or static:: - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l = this.currentFileNode.classes.length; i < l; i++) { + var classNode = this.currentFileNode.classes[i]; + if (this.withinBlock(classNode)) { // Add static members for this class toReturn = toReturn.concat(this.addClassMembers(classNode, true, true, true)); } - }); + } } else { // Probably accessing via [ClassName]:: - var classNames = this.currentLine.trim().match(/\S(\B[a-z]+?)(?=::)/ig); - if (classNames && classNames.length > 0) { - var className = classNames[classNames.length - 1]; - var classNode = this.getClassNodeFromTree(className); - if (classNode != null) { - // Add static members for this class - toReturn = toReturn.concat(this.addClassMembers(classNode, true, false, false)); + if (this.currentLine.indexOf("::") > -1) { + var classNames = this.currentLine.trim().match(/\S(\B[\\a-z0-9]+)/ig); + if (classNames && classNames.length > 0) { + var determinedClassname = classNames[classNames.length - 1]; + if (determinedClassname.indexOf("\\") > -1) { + determinedClassname = "\\" + determinedClassname; + } + var className = Namespaces.getFQNFromClassname(determinedClassname, this.currentFileNode); + var classNode = this.getClassNodeFromTree(className); + if (classNode != null) { + // Add static members for this class + toReturn = toReturn.concat(this.addClassMembers(classNode, true, false, false)); + } } } } @@ -238,18 +246,24 @@ export class SuggestionBuilder // Remove duplicated (overwritten) items var filtered = []; - toReturn.forEach(item => { + + for (var i = 0, l = toReturn.length; i < l; i++) { + var item = toReturn[i]; + var found = false; - filtered.forEach(subItem => { + + for (var j = 0, sl = filtered.length; j < sl; j++) { + var subItem = filtered[j]; + if (subItem.label == item.label) { found = true; } - }); + } if (!found) { filtered.push(item); } - }); + } return filtered; } @@ -257,9 +271,12 @@ export class SuggestionBuilder private buildSuggestionsForNamespaceOrUseStatement(namespaceOnly = false): CompletionItem[] { let namespaces: NamespacePart[] = []; - this.workspaceTree.forEach(fileNode => { + + for (var i = 0, l = this.workspaceTree.length; i < l; i++) { + var fileNode = this.workspaceTree[i]; + namespaces = namespaces.concat(fileNode.namespaceParts); - }); + } let line = this.currentLine; @@ -294,45 +311,64 @@ export class SuggestionBuilder let parent = namespaces; - lineParts.forEach(part => { + for (var i = 0, l = lineParts.length; i < l; i++) { + var part = lineParts[i]; + let needChildren = false; - parent.forEach(namespace => { + + for (var j = 0, sl = parent.length; j < sl; j++) { + var namespace = parent[j]; + if (namespace.name == part) { parent = namespace.children; needChildren = true; - return; + break; } - }); + } if (!needChildren) { - parent.forEach(item => { + for (var j = 0, sl = parent.length; j < sl; j++) { + var item = parent[j]; + suggestions.push({ label: item.name, kind: CompletionItemKind.Module, detail: "(namespace)" }); - }); + } } - }); + } // TODO -- update the code below to include classes, traits an interfaces as required (introduce new bool params) // Get namespace-aware suggestions for classes, traits and interfaces if (!namespaceOnly) { let namespaceToSearch = line.slice(0, line.length - 1); - this.workspaceTree.forEach(fileNode => { - fileNode.classes.forEach(classNode => { + + for (var i = 0, l = this.workspaceTree.length; i < l; i++) { + var fileNode = this.workspaceTree[i]; + + + for (var j = 0, sl = fileNode.classes.length; j < sl; j++) { + var classNode = fileNode.classes[j]; + if (classNode.namespace == namespaceToSearch) { suggestions.push({ label: classNode.name, kind: CompletionItemKind.Class, detail: "(class)" }); } - }); - fileNode.traits.forEach(traitNode => { + } + + for (var j = 0, sl = fileNode.traits.length; j < sl; j++) { + var traitNode = fileNode.traits[j]; + if (traitNode.namespace == namespaceToSearch) { suggestions.push({ label: traitNode.name, kind: CompletionItemKind.Class, detail: "(trait)" }); } - }); - fileNode.interfaces.forEach(interfaceNode => { + } + + for (var j = 0, sl = fileNode.interfaces.length; j < sl; j++) { + var interfaceNode = fileNode.interfaces[j]; + if (interfaceNode.namespace == namespaceToSearch) { suggestions.push({ label: interfaceNode.name, kind: CompletionItemKind.Interface, detail: "(interface)" }); } - }); - }); + } + } } return suggestions; @@ -347,27 +383,33 @@ export class SuggestionBuilder // TODO -- Check we're on a line below where they're defined // TODO -- Include these if the file is included in the current file if (options.topConstants) { - this.currentFileNode.constants.forEach(item => { + for (var i = 0, l = this.currentFileNode.constants.length; i < l; i++) { + let item = this.currentFileNode.constants[i]; + let value = item.value; if (item.type == "string") { value = "\"" + value + "\""; } toReturn.push({ label: item.name, kind: CompletionItemKind.Value, detail: `(constant) : ${item.type} : ${value}` }); - }); + } } if (options.topVariables) { - this.currentFileNode.topLevelVariables.forEach(item => { + for (var i = 0, l = this.currentFileNode.topLevelVariables.length; i < l; i++) { + let item = this.currentFileNode.topLevelVariables[i]; + toReturn.push({ label: item.name, kind: CompletionItemKind.Variable, detail: `(variable) : ${item.type}` }); - }); + } } if (options.classes && !options.noNamespaceOnly) { - this.currentFileNode.namespaceUsings.forEach(item => { + for (var i = 0, l = this.currentFileNode.namespaceUsings.length; i < l; i++) { + let item = this.currentFileNode.namespaceUsings[i]; + if (item.alias != null) { toReturn.push({ label: item.alias, kind: CompletionItemKind.Class, detail: "(class) " + item.name }); } - }); + } } if (options.localVariables || options.parameters || options.globalVariables) { @@ -378,7 +420,9 @@ export class SuggestionBuilder })); // Find out which method call/constructor we're in - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l = this.currentFileNode.classes.length; i < l; i++) { + let classNode = this.currentFileNode.classes[i]; + funcs = funcs.concat(classNode.methods.filter(item => { return this.withinBlock(item); })); @@ -386,40 +430,52 @@ export class SuggestionBuilder if (classNode.construct != null && this.withinBlock(classNode.construct)) { funcs.push(classNode.construct); } - }); + } // Find out which trait we're in - this.currentFileNode.traits.forEach(traitNode => { + for (var i = 0, l = this.currentFileNode.traits.length; i < l; i++) { + let traitNode = this.currentFileNode.traits[i]; + funcs = funcs.concat(traitNode.methods.filter(item => { return this.withinBlock(item); })); - }); + } if (funcs.length > 0) { if (options.localVariables) { - funcs[0].scopeVariables.forEach(item => { + for (var i = 0, l:number = funcs[0].scopeVariables.length; i < l; i++) { + let item = funcs[0].scopeVariables[i]; + toReturn.push({ label: item.name, kind: CompletionItemKind.Variable, detail: `(variable) : ${item.type}` }); - }); + } } if (options.parameters) { - funcs[0].params.forEach(item => { + for (var i = 0, l:number = funcs[0].params.length; i < l; i++) { + let item = funcs[0].params[i]; + toReturn.push({ label: item.name, kind: CompletionItemKind.Property, detail: `(parameter) : ${item.type}` }); - }); + } } if (options.globalVariables) { - funcs[0].globalVariables.forEach(item => { + for (var i = 0, l:number = funcs[0].globalVariables.length; i < l; i++) { + let item = funcs[0].globalVariables[i]; + // TODO -- look up original variable to find the type toReturn.push({ label: item, kind: CompletionItemKind.Variable, detail: `(imported global) : mixed` }); - }); + } } } } - this.workspaceTree.forEach(fileNode => { + for (var i = 0, l:number = this.workspaceTree.length; i < l; i++) { + let fileNode = this.workspaceTree[i]; + if (options.classes) { - fileNode.classes.forEach(item => { + for (var j = 0, sl:number = fileNode.classes.length; j < sl; j++) { + let item = fileNode.classes[j]; + let include = true; if (options.noNamespaceOnly) { if (item.namespace) { @@ -435,11 +491,13 @@ export class SuggestionBuilder insertText: this.getInsertTextWithNamespace(item, options) }); } - }); + } } if (options.interfaces) { - fileNode.interfaces.forEach(item => { + for (var j = 0, sl:number = fileNode.interfaces.length; j < sl; j++) { + let item = fileNode.interfaces[j]; + let include = true; if (options.noNamespaceOnly) { if (item.namespace) { @@ -455,11 +513,13 @@ export class SuggestionBuilder insertText: this.getInsertTextWithNamespace(item, options) }); } - }); + } } if (options.traits) { - fileNode.traits.forEach(item => { + for (var j = 0, sl:number = fileNode.traits.length; j < sl; j++) { + let item = fileNode.traits[j]; + let include = true; if (options.noNamespaceOnly) { if (item.namespace) { @@ -475,21 +535,25 @@ export class SuggestionBuilder insertText: this.getInsertTextWithNamespace(item, options) }); } - }); + } } if (options.topFunctions) { - fileNode.functions.forEach(item => { + for (var j = 0, sl:number = fileNode.functions.length; j < sl; j++) { + let item = fileNode.functions[j]; + toReturn.push({ label: item.name, kind: CompletionItemKind.Function, detail: `(function) : ${item.returns}`, insertText: this.getFunctionInsertText(item) }); - }); + } } if (options.namespaces) { - fileNode.namespaces.forEach(item => { + for (var j = 0, sl:number = fileNode.namespaces.length; j < sl; j++) { + let item = fileNode.namespaces[j]; + toReturn.push({ label: item.name, kind: CompletionItemKind.Module, detail: `(namespace)` }); - }); + } } - }); + } return toReturn; } @@ -501,19 +565,23 @@ export class SuggestionBuilder let namespaceSearch = node.namespace + "\\" + node.name; let found = false; - this.currentFileNode.namespaceUsings.forEach(item => { + for (var i = 0, l:number = this.currentFileNode.namespaceUsings.length; i < l; i++) { + let item = this.currentFileNode.namespaceUsings[i]; + if (item.name == namespaceSearch) { found = true; return null; } - }); + } + + for (var i = 0, l:number = this.currentFileNode.namespaces.length; i < l; i++) { + let item = this.currentFileNode.namespaces[i]; - this.currentFileNode.namespaces.forEach(item => { if (item.name == namespace) { found = true; return null; } - }); + } if (!found) { return "\\" + namespaceSearch; @@ -552,74 +620,72 @@ export class SuggestionBuilder private getScope() : Scope { - var scope = null; - // Are we inside a class? - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l:number = this.currentFileNode.classes.length; i < l; i++) { + let classNode = this.currentFileNode.classes[i]; + if (this.withinBlock(classNode)) { if (classNode.construct != null) { if (this.withinBlock(classNode.construct)) { - scope = new Scope(ScopeLevel.Class, "constructor", classNode.name); - return; + return new Scope(ScopeLevel.Class, "constructor", classNode.name); } } - classNode.methods.forEach(method => { + + for (var j = 0, sl:number = classNode.methods.length; j < sl; j++) { + let method = classNode.methods[j]; + if (this.withinBlock(method)) { - scope = new Scope(ScopeLevel.Class, method.name, classNode.name); - return; + return new Scope(ScopeLevel.Class, method.name, classNode.name); } - }); - if (scope == null) { - scope = new Scope(ScopeLevel.Class, null, classNode.name); - return; } + + return new Scope(ScopeLevel.Class, null, classNode.name); } - }); + } // Are we inside a trait? - this.currentFileNode.traits.forEach(trait => { + for (var i = 0, l:number = this.currentFileNode.traits.length; i < l; i++) { + let trait = this.currentFileNode.traits[i]; + if (this.withinBlock(trait)) { if (trait.construct != null) { if (this.withinBlock(trait.construct)) { - scope = new Scope(ScopeLevel.Trait, "constructor", trait.name); - return; + return new Scope(ScopeLevel.Trait, "constructor", trait.name); } } - trait.methods.forEach(method => { + + for (var j = 0, sl:number = trait.methods.length; j < sl; j++) { + let method = trait.methods[j]; + if (this.withinBlock(method)) { - scope = new Scope(ScopeLevel.Trait, method.name, trait.name); - return; + return new Scope(ScopeLevel.Trait, method.name, trait.name); } - }); - if (scope == null) { - scope = new Scope(ScopeLevel.Trait, null, trait.name); - return; } + + return new Scope(ScopeLevel.Trait, null, trait.name); } - }); + } // Are we inside an interface? - this.currentFileNode.interfaces.forEach(item => { + for (var i = 0, l:number = this.currentFileNode.interfaces.length; i < l; i++) { + let item = this.currentFileNode.interfaces[i]; + if (this.withinBlock(item)) { - scope = new Scope(ScopeLevel.Interface, null, item.name); - return; + return new Scope(ScopeLevel.Interface, null, item.name); } - }); + } // Are we inside a top level function? - this.currentFileNode.functions.forEach(func => { + for (var i = 0, l:number = this.currentFileNode.functions.length; i < l; i++) { + let func = this.currentFileNode.functions[i]; + if (this.withinBlock(func)) { - scope = new Scope(ScopeLevel.Root, func.name, null); - return; + return new Scope(ScopeLevel.Root, func.name, null); } - }); - - if (scope == null) { - // Must be at the top level of a file - return new Scope(ScopeLevel.Root, null, null); - } else { - return scope; } + + // Must be at the top level of a file + return new Scope(ScopeLevel.Root, null, null); } private withinBlock(block) : boolean @@ -638,46 +704,46 @@ export class SuggestionBuilder private getClassNodeFromTree(className: string) : ClassNode { - var toReturn = null; - let namespaceInfo = Namespaces.getNamespaceInfoFromFQNClassname(className); var namespace = namespaceInfo.namespace; var rawClassname = namespaceInfo.classname - this.workspaceTree.forEach((fileNode) => { - fileNode.classes.forEach((classNode) => { + for (var i = 0, l:number = this.workspaceTree.length; i < l; i++) { + let fileNode = this.workspaceTree[i]; + + for (var j = 0, sl:number = fileNode.classes.length; j < sl; j++) { + let classNode = fileNode.classes[j]; + if ( classNode.name.toLowerCase() == rawClassname.toLowerCase() && classNode.namespace == namespace ) { - toReturn = classNode; + return classNode; } - }); - }); - - return toReturn; + } + } } private getTraitNodeFromTree(traitName: string) : TraitNode { - var toReturn = null; - let namespaceInfo = Namespaces.getNamespaceInfoFromFQNClassname(traitName); var namespace = namespaceInfo.namespace; var rawTraitname = namespaceInfo.classname - var fileNode = this.workspaceTree.forEach((fileNode) => { - fileNode.traits.forEach((traitNode) => { + for (var i = 0, l:number = this.workspaceTree.length; i < l; i++) { + let fileNode = this.workspaceTree[i]; + + for (var j = 0, sl:number = fileNode.traits.length; j < sl; j++) { + let traitNode = fileNode.traits[j]; + if ( traitNode.name.toLowerCase() == rawTraitname.toLowerCase() && traitNode.namespace == namespace ) { - toReturn = traitNode; + return traitNode; } - }); - }); - - return toReturn; + } + } } private buildAccessModifierText(modifier: number) : string @@ -699,7 +765,6 @@ export class SuggestionBuilder private checkAccessorAndAddMembers(scope: Scope) : CompletionItem[] { - var toReturn: CompletionItem[] = []; var rawParts = this.currentLine.trim().match(/\$\S*(?=->)/gm); var parts: string[] = []; @@ -709,12 +774,17 @@ export class SuggestionBuilder var rawLast = rawParts.length - 1; if (rawParts[rawLast].indexOf("->") > -1) { - rawParts.forEach(part => { + for (var i = 0, l:number = rawParts.length; i < l; i++) { + let part = rawParts[i]; + var splitParts = part.split("->"); - splitParts.forEach(splitPart => { + + for (var j = 0, sl:number = splitParts.length; j < sl; j++) { + let splitPart = splitParts[j]; + parts.push(splitPart); - }); - }); + } + } } else { parts = rawParts; } @@ -726,28 +796,29 @@ export class SuggestionBuilder if (parts[last].indexOf("$this", parts[last].length - 5) > -1) { // We're referencing the current class; show everything - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l:number = this.currentFileNode.classes.length; i < l; i++) { + let classNode = this.currentFileNode.classes[i]; + if (this.withinBlock(classNode)) { - toReturn = this.addClassMembers(classNode, false, true, true); + return this.addClassMembers(classNode, false, true, true); } - }); - this.currentFileNode.traits.forEach(traitNode => { + } + + for (var i = 0, l:number = this.currentFileNode.traits.length; i < l; i++) { + let traitNode = this.currentFileNode.traits[i]; if (this.withinBlock(traitNode)) { - toReturn = this.addClassMembers(traitNode, false, true, true); + return this.addClassMembers(traitNode, false, true, true); } - }); - } else { - // We're probably calling from a instantiated variable - // Check the variable is in scope to work out which suggestions to provide - toReturn = this.checkForInstantiatedVariableAndAddSuggestions(parts[last], scope); + } } - return toReturn; + // We're probably calling from a instantiated variable + // Check the variable is in scope to work out which suggestions to provide + return this.checkForInstantiatedVariableAndAddSuggestions(parts[last], scope); } private checkForInstantiatedVariableAndAddSuggestions(variableName: string, scope: Scope) : CompletionItem[] { - var toReturn = []; var variablesFound = []; // Check the scope paramater to find out where we're calling from @@ -760,7 +831,9 @@ export class SuggestionBuilder }); } else { // Top level function - this.currentFileNode.functions.forEach(func => { + for (var i = 0, l:number = this.currentFileNode.functions.length; i < l; i++) { + let func = this.currentFileNode.functions[i]; + if (func.name == scope.name) { variablesFound = variablesFound.concat(func.params.filter(item => { return item.name == variableName; @@ -770,7 +843,7 @@ export class SuggestionBuilder })); // TODO -- Add global variables } - }); + } } break; @@ -781,7 +854,9 @@ export class SuggestionBuilder } else { if (scope.name == "constructor") { // Within constructor - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l:number = this.currentFileNode.classes.length; i < l; i++) { + let classNode = this.currentFileNode.classes[i]; + if (classNode.name == scope.parent) { variablesFound = variablesFound.concat(classNode.construct.params.filter(item => { return item.name == variableName; @@ -790,12 +865,16 @@ export class SuggestionBuilder return item.name == variableName; })); } - }); + } } else { // Within method - this.currentFileNode.classes.forEach(classNode => { + for (var i = 0, l:number = this.currentFileNode.classes.length; i < l; i++) { + let classNode = this.currentFileNode.classes[i]; + if (classNode.name == scope.parent) { - classNode.methods.forEach(method => { + for (var j = 0, sl:number = classNode.methods.length; j < sl; j++) { + let method = classNode.methods[j]; + if (method.name == scope.name) { variablesFound = variablesFound.concat(method.params.filter(item => { return item.name == variableName; @@ -804,9 +883,9 @@ export class SuggestionBuilder return item.name == variableName; })); } - }); + } } - }); + } } } break; @@ -827,11 +906,11 @@ export class SuggestionBuilder var classNode = this.getClassNodeFromTree(className); if (classNode != null) { - toReturn = this.addClassMembers(classNode, false, false, false); + return this.addClassMembers(classNode, false, false, false); } } - return toReturn; + return []; } private addClassMembers(classNode: ClassNode, staticOnly: boolean, includePrivate: boolean, includeProtected: boolean) @@ -839,16 +918,20 @@ export class SuggestionBuilder var toReturn = []; if (staticOnly == true) { - classNode.constants.forEach((subNode) => { + for (var i = 0, l:number = classNode.constants.length; i < l; i++) { + let subNode = classNode.constants[i]; + let value = subNode.value; if (subNode.type == "string") { value = "\"" + value + "\""; } toReturn.push({ label: subNode.name, kind: CompletionItemKind.Value, detail: `(constant) : ${subNode.type} : ${value}` }); - }); + } } - classNode.methods.forEach((subNode) => { + for (var i = 0, l:number = classNode.methods.length; i < l; i++) { + let subNode = classNode.methods[i]; + if (subNode.isStatic == staticOnly) { var accessModifier = "(" + this.buildAccessModifierText(subNode.accessModifier); var insertText = this.getFunctionInsertText(subNode); @@ -865,9 +948,11 @@ export class SuggestionBuilder toReturn.push({ label: subNode.name, kind: CompletionItemKind.Function, detail: accessModifier, insertText: insertText }); } } - }); + } + + for (var i = 0, l:number = classNode.properties.length; i < l; i++) { + let subNode = classNode.properties[i]; - classNode.properties.forEach((subNode) => { if (subNode.isStatic == staticOnly) { var accessModifier = "(" + this.buildAccessModifierText(subNode.accessModifier) + ` property) : ${subNode.type}`; var insertText = subNode.name; @@ -887,16 +972,18 @@ export class SuggestionBuilder toReturn.push({ label: subNode.name, kind: CompletionItemKind.Property, detail: accessModifier, insertText: insertText }); } } - }); + } // Add items from included traits - classNode.traits.forEach((traitName) => { + for (var i = 0, l:number = classNode.traits.length; i < l; i++) { + let traitName = classNode.traits[i]; + // Look up the trait node in the tree var traitNode = this.getTraitNodeFromTree(traitName); if (traitNode != null) { toReturn = toReturn.concat(this.addClassMembers(traitNode, staticOnly, true, true)); } - }); + } // Add items from parent(s) if (classNode.extends != null && classNode.extends != "") { @@ -909,18 +996,22 @@ export class SuggestionBuilder // Remove duplicated (overwritten) items var filtered = []; - toReturn.forEach(item => { + for (var i = 0, l:number = toReturn.length; i < l; i++) { + let item = toReturn[i]; + var found = false; - filtered.forEach(subItem => { + for (var j = 0, sl:number = filtered.length; j < sl; j++) { + let subItem = filtered[j]; + if (subItem.label == item.label) { found = true; } - }); + } if (!found) { filtered.push(item); } - }); + } return filtered; } diff --git a/server/src/util/Namespaces.ts b/server/src/util/Namespaces.ts index c6fe5c7..1f989b7 100644 --- a/server/src/util/Namespaces.ts +++ b/server/src/util/Namespaces.ts @@ -53,7 +53,9 @@ export class Namespaces nameFound = true; } else { // Check if we are "use"ing this class - tree.namespaceUsings.forEach(item => { + for (var i = 0, l = tree.namespaceUsings.length; i < l; i++) { + var item = tree.namespaceUsings[i]; + if ( item.name.endsWith("\\" + classname) || item.alias == classname @@ -61,9 +63,9 @@ export class Namespaces // Class found, add namespace to name (handling alias) type = item.name; nameFound = true; - return; + break; } - }); + } if (!nameFound && tree.namespaces.length > 0) { type = "\\" + tree.namespaces[0].name + "\\" + classname;