Skip to content

Commit 82783ed

Browse files
authored
Merge pull request #13 from admvx/features
Add enitity definition support
2 parents 191bd4d + 1c562ec commit 82783ed

File tree

6 files changed

+136
-44
lines changed

6 files changed

+136
-44
lines changed

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ https://github.com/admvx/as2-language-support
44

55
## Features
66
- Syntax highlighting for ActionScript 2.0 (`*.as`)
7-
- LSP-based code completion
7+
- LSP-based assistance with the following features:
8+
- Completion
9+
- Hover info
10+
- Method signatures
11+
- Definition resolution (jump to definition, peek definition)
812
- Intrinsic language features + local code parsing
913

10-
## Coming soon
11-
- Go-to / peek definition support
12-
- Contextual suggestion filtering
13-
- Performance improvements via WASM regex implementation
14-
1514
## Release Notes
1615

16+
### 1.2.0
17+
Add definition support (jump to definition, peek definition)
18+
1719
### 1.1.0
1820
- Removes the requirement that the active vscode workspace directory must match the root class-path of any open files
1921
- Improves handling of wildcard imports

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": "git",
77
"url": "git@github.com:admvx/as2-language-support.git"
88
},
9-
"version": "1.1.0",
9+
"version": "1.2.0",
1010
"author": "Adam Vernon",
1111
"publisher": "admvx",
1212
"icon": "icon.png",

server/src/action-context.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { classAmbients, methodAmbients, generalAmbients } from './ambient-symbol
33
import { ActionParser, recursiveReplace } from './action-parser';
44
import { LoadQueue, DirectoryUtility } from './file-system-utilities';
55
import { logIt, LogLevel, ActionConfig } from './config';
6-
import { CompletionItem, CompletionItemKind, CompletionParams, CancellationToken, CompletionTriggerKind, TextDocumentPositionParams, SignatureHelp, Hover } from 'vscode-languageserver';
6+
import { CompletionItem, CompletionItemKind, CompletionParams, CancellationToken, CompletionTriggerKind, TextDocumentPositionParams, SignatureHelp, Hover, Location } from 'vscode-languageserver';
77

88
//TODO: use onigasm for regex instead
99
const tokenSplitter = /([\w\$]+)/g; //Captures symbol names
@@ -29,7 +29,7 @@ export class ActionContext {
2929
//--Completions--//
3030
//---------------//
3131
public static async getCompletions(textDocumentPosition: CompletionParams, cancellationToken: CancellationToken): Promise<CompletionItem[]> {
32-
ActionConfig.LOG_LEVEL !== LogLevel.NONE && logIt({ level: LogLevel.VERBOSE, message: `Hooray: ${JSON.stringify(textDocumentPosition)}` });
32+
ActionConfig.LOG_LEVEL !== LogLevel.NONE && logIt({ level: LogLevel.VERBOSE, message: `Completion request: ${JSON.stringify(textDocumentPosition)}` });
3333

3434
let parsedClass = this._classLookup[textDocumentPosition.textDocument.uri];
3535
if (! parsedClass) return [];
@@ -183,6 +183,47 @@ export class ActionContext {
183183
};
184184
}
185185

186+
public static async getDefinition(textDocumentPosition: TextDocumentPositionParams): Promise<Location> {
187+
ActionConfig.LOG_LEVEL !== LogLevel.NONE && logIt({ level: LogLevel.VERBOSE, message: `Get definition: ${JSON.stringify(textDocumentPosition)}` });
188+
189+
let ambientClass = this._classLookup[textDocumentPosition.textDocument.uri];
190+
if (! ambientClass) return null;
191+
192+
let lineIndex = textDocumentPosition.position.line;
193+
let charIndex = textDocumentPosition.position.character;
194+
let fullLine = ambientClass.lines[lineIndex];
195+
if (!fullLine.charAt(charIndex).match(symbolMatcher)) charIndex --;
196+
if (!fullLine.charAt(charIndex).match(symbolMatcher)) return null;
197+
198+
let line = fullLine.substr(0, charIndex + 1).trim();
199+
if (this.positionInsideStringLiteral(line)) return null;
200+
201+
firstSymbolMatcher.lastIndex = 0;
202+
let result = firstSymbolMatcher.exec(fullLine.substr(charIndex + 1));
203+
if (result) {
204+
line += result[1];
205+
}
206+
207+
let symbolChain = this.getSymbolChainFromLine(line);
208+
let memberAndClass = await this.traverseSymbolChainToMember(symbolChain, ambientClass, lineIndex, true);
209+
let member = memberAndClass && memberAndClass[0];
210+
let actionClass = memberAndClass && memberAndClass[1];
211+
if (! member) {
212+
if (!actionClass || !actionClass.fileUri || !actionClass.locationRange) return null;
213+
return {
214+
uri: actionClass.fileUri,
215+
range: actionClass.locationRange
216+
};
217+
}
218+
219+
if (!member.owningClass || !member.owningClass.fileUri || !member.locationRange) return null;
220+
221+
return {
222+
uri: member.owningClass.fileUri,
223+
range: member.locationRange
224+
};
225+
}
226+
186227
private static getSymbolChainFromLine(line: string, expectGarbageAtEol?: boolean): SymbolChainLink[] {
187228
let tokens = line.split(tokenSplitter); //Odd-numbered array of tokens and delimiters, where the first and last elements are zero-or-longer delimiter strings
188229
if (expectGarbageAtEol && tokens[tokens.length - 1] !== '.') {

server/src/action-elements.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ActionContext } from './action-context';
22
import { logIt, LogLevel, ActionConfig } from './config';
3-
import { SignatureInformation } from 'vscode-languageserver';
3+
import { SignatureInformation, Range } from 'vscode-languageserver';
44
import { DirectoryUtility } from './file-system-utilities';
55

66
export class ActionClass {
@@ -14,6 +14,7 @@ export class ActionClass {
1414
public parentPackage: string;
1515
public lines: string[];
1616
public constructorMethod: ActionMethod;
17+
public locationRange: Range;
1718

1819
private _description: string;
1920
private _memberLookup: { [propName: string]: ActionParameter } = Object.create(null);
@@ -194,6 +195,8 @@ export class ActionClass {
194195
parentPackage: this.parentPackage,
195196
_imports: this._imports,
196197
_members: this._members.filter(member => member.name !== 'this' && member.name !== 'super')
198+
}, (key, value) => {
199+
if (key !== 'owningClass') return value; //Skip this property to avoid circular references
197200
});
198201
}
199202

@@ -235,14 +238,15 @@ export interface PickledClass {
235238
_members: (ActionParameter|ActionMethod)[];
236239
}
237240

238-
//Method + constructor params; function-scoped vars
241+
//Class properties; method + constructor params; function-scoped vars
239242
export interface ActionParameter {
240243
name: string;
241244
returnType?: string;
242-
owner?: string;
245+
owningClass?: ActionClass;
243246
isInline?: boolean;
244247
isArgument?: boolean;
245248
documentation?: string;
249+
locationRange?: Range;
246250
description?: string; //Should be present on all but only after instantiation
247251
isPublic?: boolean;
248252
isStatic?: boolean;
@@ -251,7 +255,7 @@ export interface ActionParameter {
251255
isGlobal?: boolean;
252256
}
253257

254-
//Class level properties
258+
//Class methods
255259
export interface ActionMethod extends ActionParameter {
256260
isMethod: true;
257261
isConstructor: boolean;

server/src/action-parser.ts

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ActionClass, ActionParameter, ActionMethod } from './action-elements';
22
import { logIt, LogLevel, ActionConfig } from './config';
33
import { ActionContext } from './action-context';
4+
import { Range } from 'vscode-languageserver';
45
//import { loadWASM } from 'onigasm';
56

67
enum ActionScope {
@@ -24,14 +25,14 @@ const stripComments = (str: string) => str.replace(commentMatcher, (match: strin
2425
//TODO: use onigasm for regex instead
2526
const Patterns = {
2627
IMPORT: /\bimport\s+([\w$\.\*]+)/,
27-
CLASS: /\bclass\s+([\w$\.]+)/,
28+
CLASS: /(\bclass\s+)([\w$\.]+)/,
2829
CLASS_EXTENDS: /\bextends\s+([\w$\.]+)/,
29-
CLASS_VAR: /\bvar\s+([\w$]+)(?:\s*:\s*([\w$\.]+))?/, //Group 1: var name; capture group 2 (optional): type
30+
CLASS_VAR: /(\bvar\s+)([\w$]+)(?:\s*:\s*([\w$\.]+))?/, //Group 2: var name; group 3 (optional): type
3031
PRIVATE: /\bprivate\b/,
3132
STATIC: /\bstatic\b/,
32-
METHOD: /\bfunction\s+([\w$]+)\s*\((.*)\)(?:\s*:\s*([\w$\.]+))?/, //Group 1: method name; group 2 (optional): arg list; group 3 (optional): return type
33-
IDENTIFIER_AND_TYPE: /\s*([\w$]+)(?:\s*:\s*([\w$\.]+))?/, //Group 1: var/argument name; capture group 2 (optional): type
34-
LOCAL_VARS: /\bvar\b([\s\w$:,=.\[\]()-+*/]+)/,
33+
METHOD: /(\bfunction\s+)([\w$]+)(\s*\()(.*)\)(?:\s*:\s*([\w$\.]+))?/, //Group 2: method name; group 4 (optional): arg list; group 5 (optional): return type
34+
IDENTIFIER_AND_TYPE: /(\s*)([\w$]+)(\s*)(?:\:\s*([\w$\.]+)\s*)?/, //Group 2: var/argument name; group 4 (optional): type
35+
LOCAL_VARS: /(\bvar\b)([\s\w$:,=.\[\]()-+*/]+)/, //Group 2: content
3536
BRACES: /[{}]/g,
3637
MATCHED_BRACES: /{(?:(?!{).)*?}/g,
3738
MATCHED_BRACKETS: /\[(?:(?!\[).)*?\]/g,
@@ -56,6 +57,7 @@ export class ActionParser {
5657
this.wipClass.fileUri = fileUri;
5758
this.wipClass.lines = fileContent.split(/\r?\n/);
5859

60+
let includeLocations: boolean = !! fileUri;
5961
let imports: string[] = [];
6062

6163
let scopeStack: ActionScope[] = [ActionScope.BASE];
@@ -76,7 +78,11 @@ export class ActionParser {
7678
Patterns.CLASS.lastIndex = 0;
7779
result = Patterns.CLASS.exec(line);
7880
if (result) {
79-
fullType = result[1];
81+
fullType = result[2];
82+
if (includeLocations) {
83+
let charStart = result.index + result[1].length;
84+
this.wipClass.locationRange = { start: { line: i, character: charStart }, end: { line: i, character: charStart + result[2].length } };
85+
}
8086
shortType = ActionContext.fullTypeToShortType(fullType);
8187
this.wipClass.fullType = fullType;
8288
this.wipClass.shortType = shortType;
@@ -102,12 +108,18 @@ export class ActionParser {
102108
Patterns.PRIVATE.lastIndex = Patterns.STATIC.lastIndex = 0;
103109
let privateRes = Patterns.PRIVATE.exec(line);
104110
let staticRes = Patterns.STATIC.exec(line);
105-
this.wipClass.registerMember({
106-
name: result[1],
107-
returnType: result[2] || undefined,
111+
let member: ActionParameter = {
112+
name: result[2],
113+
returnType: result[3] || undefined,
108114
isPublic: ! (privateRes && privateRes.index < result.index),
109-
isStatic: !! (staticRes && staticRes.index < result.index)
110-
});
115+
isStatic: !! (staticRes && staticRes.index < result.index),
116+
owningClass: this.wipClass
117+
};
118+
if (includeLocations) {
119+
let charStart = result.index + result[1].length;
120+
member.locationRange = { start: { line: i, character: charStart }, end: { line: i, character: charStart + result[2].length } };
121+
}
122+
this.wipClass.registerMember(member);
111123
break;
112124
}
113125

@@ -118,24 +130,35 @@ export class ActionParser {
118130
Patterns.PRIVATE.lastIndex = Patterns.STATIC.lastIndex = 0;
119131
let privateRes = Patterns.PRIVATE.exec(line);
120132
let staticRes = Patterns.STATIC.exec(line);
121-
let isConstructor = result[1] && result[1] === this.wipClass.shortType;
133+
let isConstructor = result[2] && result[2] === this.wipClass.shortType;
134+
135+
let locationRange: Range, argsStart: number;
136+
if (includeLocations) {
137+
let charStart = result.index + result[1].length;
138+
locationRange = { start: { line: i, character: charStart }, end: { line: i, character: charStart + result[2].length } };
139+
argsStart = locationRange.end.character + result[3].length;
140+
}
122141

123142
let method: ActionMethod = {
124143
isMethod: true,
125144
isConstructor: isConstructor,
126-
name: result[1],
127-
parameters: this.getParameterArrayFromString(result[2], true),
145+
name: result[2],
146+
parameters: this.getParameterArrayFromString(result[4], true, includeLocations, i, argsStart),
147+
locationRange: locationRange,
128148
locals: [],
129-
returnType: result[3] || (isConstructor ? result[1] : null),
149+
returnType: result[5] || (isConstructor ? result[2] : null),
130150
isPublic: !(privateRes && privateRes.index < result.index),
131151
isStatic: !!(staticRes && staticRes.index < result.index),
132-
firstLineNumber: i + 1
152+
owningClass: this.wipClass
133153
};
134154

135155
let methodLines = this.wipClass.lines.slice(i+1);
136156
methodLines.unshift(line.substr(result.index + result[0].length));
137-
let [methodLength, locals] = this.extractMethod(methodLines, deep);
138-
method.lastLineNumber = method.firstLineNumber + methodLength - 1;
157+
let [methodLength, locals] = this.extractMethod(methodLines, deep, includeLocations, i);
158+
if (includeLocations) {
159+
method.firstLineNumber = i + 1;
160+
method.lastLineNumber = method.firstLineNumber + methodLength - 1;
161+
}
139162
method.locals = locals;
140163
method.scopedMembers = method.parameters.concat(method.locals);
141164

@@ -167,17 +190,24 @@ export class ActionParser {
167190
return this.wipClass;
168191
}
169192

170-
private static getParameterArrayFromString(paramString: string, isArgument = false): ActionParameter[] {
193+
private static getParameterArrayFromString(paramString: string, isArgument = false, includeLocations = false, lineNumber?: number, charIndex?: number): ActionParameter[] {
171194
let parsedParams: ActionParameter[] = [];
172195
if ((! paramString) || paramString.trim() === '') return parsedParams;
173196

174-
let rawParams = paramString.split(',');
197+
let rawParams = paramString.split(','), charStart: number;
175198
let result: RegExpExecArray;
176199
for (let i = 0, l = rawParams.length; i < l; i++) {
177-
Patterns.IDENTIFIER_AND_TYPE.lastIndex = 0;
200+
Patterns.IDENTIFIER_AND_TYPE.lastIndex = 0;
178201
result = Patterns.IDENTIFIER_AND_TYPE.exec(rawParams[i]);
179-
if (result && result[1]) {
180-
parsedParams.push({ name: result[1], returnType: result[2], isArgument: isArgument, isInline: ! isArgument });
202+
if (result && result[2]) {
203+
let param: ActionParameter = { name: result[2], returnType: result[4], isArgument: isArgument, isInline: ! isArgument };
204+
if (includeLocations) {
205+
charStart = charIndex + result.index + result[1].length;
206+
param.locationRange = { start: { line: lineNumber, character: charStart }, end: { line: lineNumber, character: charStart + result[2].length } };
207+
param.owningClass = this.wipClass;
208+
charIndex += rawParams[i].length + 1;
209+
}
210+
parsedParams.push(param);
181211
}
182212
}
183213
return parsedParams;
@@ -186,7 +216,7 @@ export class ActionParser {
186216
/**
187217
* Modifies the passed array to remove the method contents, returns an array with the local vars if desired; otherwise an empty array
188218
*/
189-
private static extractMethod(lines: string[], collectLocals = false): [number, ActionParameter[]] {
219+
private static extractMethod(lines: string[], collectLocals = false, includeLocations = false, lineNumber?: number): [number, ActionParameter[]] {
190220
let locals: ActionParameter[] = [];
191221
let lineCount = -1;
192222
let searchCount = -1; //Denotes start not yet found
@@ -211,11 +241,11 @@ export class ActionParser {
211241
if (collectLocals) {
212242
Patterns.LOCAL_VARS.lastIndex = 0;
213243
result = Patterns.LOCAL_VARS.exec(line);
214-
if (result && result[1]) {
215-
let localString = recursiveReplace(result[1], Patterns.MATCHED_BRACES);
216-
localString = recursiveReplace(localString, Patterns.MATCHED_BRACKETS);
217-
localString = recursiveReplace(localString, Patterns.MATCHED_PARENS);
218-
locals = locals.concat(this.getParameterArrayFromString(localString, false));
244+
if (result && result[2]) {
245+
let localString = recursiveReplaceAndPreserveLength(result[2], Patterns.MATCHED_BRACES);
246+
localString = recursiveReplaceAndPreserveLength(localString, Patterns.MATCHED_BRACKETS);
247+
localString = recursiveReplaceAndPreserveLength(localString, Patterns.MATCHED_PARENS);
248+
locals = locals.concat(this.getParameterArrayFromString(localString, false, includeLocations, lineNumber + lineCount, result.index + result[1].length));
219249
}
220250
}
221251
//Find end / other opening braces
@@ -241,3 +271,16 @@ export function recursiveReplace(input: string, matcher: string | RegExp, replac
241271
} while (output !== input);
242272
return output;
243273
}
274+
275+
export function recursiveReplaceAndPreserveLength(input: string, matcher: RegExp, replacement = ' '): string {
276+
let output = input;
277+
let result: RegExpExecArray;
278+
do {
279+
input = output;
280+
matcher.lastIndex = 0;
281+
result = matcher.exec(input);
282+
if (! result || !result[0]) break;
283+
output = input.replace(result[0], replacement.repeat(result[0].length));
284+
} while (output !== input);
285+
return output;
286+
}

server/src/server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ connection.onInitialize((params) => {
2020
textDocumentSync: documents.syncKind,
2121
completionProvider: { resolveProvider: false, triggerCharacters: ['.'] },
2222
signatureHelpProvider: { triggerCharacters: ['(', ','] },
23-
hoverProvider: true
23+
hoverProvider: true,
24+
definitionProvider: true
2425
}
2526
};
2627
});
@@ -37,5 +38,6 @@ ActionParser.initialise()
3738
documents.onDidChangeContent(change => ActionContext.registerClass(ActionParser.parseFile(change.document.uri, change.document.getText(), true)));
3839
connection.onCompletion((docPos, token) => ActionContext.getCompletions(docPos, token));
3940
connection.onSignatureHelp((docPos, token) => ActionContext.getSignatureHelp(docPos, token));
40-
connection.onHover((docPos) => ActionContext.getHoverInfo(docPos));
41+
connection.onHover(docPos => ActionContext.getHoverInfo(docPos));
42+
connection.onDefinition(docPos => ActionContext.getDefinition(docPos));
4143
});

0 commit comments

Comments
 (0)