diff --git a/.vscode/launch.json b/.vscode/launch.json index 8bb6590..fb4651f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,18 @@ "port": 6011, "sourceMaps": true, "outFiles": ["${workspaceFolder}/server/out/**/*.js"] - } + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/client/out/test/index" + ], + "outFiles": ["${workspaceFolder}client/out/test/**/*.js"] + } ], "compounds": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index 0141ca3..ce8044a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.6 (May 06, 2020) +- Performance update + +## 0.1.5 (May 04, 2020) +- Node package issue + ## 0.1.4 (May 02, 2020) - Opening links with capital letters issue fixed - Duplicate label statement warning diff --git a/README.md b/README.md index 5eb9b07..067f49b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ drawing +![release](https://img.shields.io/github/v/release/iSorp/macro-executor) ![maintained](https://img.shields.io/maintenance/yes/2020.svg) [![open issues](https://img.shields.io/github/issues/iSorp/macro-executor.svg?)](https://github.com/iSorp/macro-executor/issues) [![license](https://img.shields.io/github/license/iSorp/macro-executor)](https://opensource.org/licenses/MIT) @@ -38,8 +39,13 @@ Fanuc Macro Executor syntax highlighting, validating and project building ![Definition](./resources/codelens.png) ## Required file extension -* Macro files `.src` +* Macro files`.src` * Include files `.def` +* Link files `.lnk` + +## Coding conventions +* Uppercase for constants: `@MY_CONSTANT` 100 +* Space between statements: `N9000 G01 X1` ## Default Commands @@ -95,13 +101,13 @@ project │ file2.src │ └───def -│ file1.def -│ file1.def +│ file1.def +│ file1.def │ └───lnk file1.lnk file1.lnk - F30iA_01.MEX + F30iA_01.MEX ``` diff --git a/TODO b/TODO index 52dba10..c39fae8 100644 --- a/TODO +++ b/TODO @@ -2,3 +2,5 @@ - Symbol search for NC Statemets - Parsing of .lnk files (read declarated "FILE=" properties validate only these files) - Improve bild system (read properties of .lnk files) +- Improve lint: constant checking,.. +- GOTO -> goto implementation diff --git a/client/package.json b/client/package.json index 070e749..bef7573 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,7 @@ "description": "Fanuc Macro-Executor Programming Language", "author": "iSorp", "license": "MIT", - "version": "0.0.1", + "version": "0.1.0", "publisher": "iSorp", "repository": { "type": "git", diff --git a/client/src/extension.ts b/client/src/extension.ts index 9e09995..a2cafd5 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { ExtensionContext, workspace, commands, window, Selection } from 'vscode'; import { LanguageClient, LanguageClientOptions, - ServerOptions, TransportKind, RevealOutputChannelOn, Position, + ServerOptions, TransportKind, RevealOutputChannelOn, } from 'vscode-languageclient'; import registerCommands from './common/commands'; @@ -45,7 +45,7 @@ export function activate(context: ExtensionContext) { synchronize: { // Notify the server about file changes - fileEvents: workspace.createFileSystemWatcher('**/*.{src,def}') + fileEvents: workspace.createFileSystemWatcher('**/*.{src,def,lnk}') }, diagnosticCollectionName: 'macro', progressOnInitialization: true, diff --git a/client/src/test/completion.test.ts b/client/src/test/completion.test.ts deleted file mode 100644 index f355078..0000000 --- a/client/src/test/completion.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as vscode from 'vscode'; -import * as assert from 'assert'; -import { getDocUri, activate } from './helper'; - -suite('Should do completion', () => { - const docUri = getDocUri('completion.txt'); - - test('Completes JS/TS in txt file', async () => { - await testCompletion(docUri, new vscode.Position(0, 0), { - items: [ - { label: 'JavaScript', kind: vscode.CompletionItemKind.Text }, - { label: 'TypeScript', kind: vscode.CompletionItemKind.Text } - ] - }); - }); -}); - -async function testCompletion( - docUri: vscode.Uri, - position: vscode.Position, - expectedCompletionList: vscode.CompletionList -) { - await activate(docUri); - - // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion - const actualCompletionList = (await vscode.commands.executeCommand( - 'vscode.executeCompletionItemProvider', - docUri, - position - )) as vscode.CompletionList; - - assert.ok(actualCompletionList.items.length >= 2); - expectedCompletionList.items.forEach((expectedItem, i) => { - const actualItem = actualCompletionList.items[i]; - assert.equal(actualItem.label, expectedItem.label); - assert.equal(actualItem.kind, expectedItem.kind); - }); -} diff --git a/client/src/test/diagnostics.test.ts b/client/src/test/diagnostics.test.ts index 1aa8a36..ec5b0a2 100644 --- a/client/src/test/diagnostics.test.ts +++ b/client/src/test/diagnostics.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { getDocUri, activate } from './helper'; suite('Should get diagnostics', () => { - const docUri = getDocUri('diagnostics.txt'); + const docUri = getDocUri('src/test.src'); test('Diagnoses uppercase texts', async () => { await testDiagnostics(docUri, [ diff --git a/client/src/test/helper.ts b/client/src/test/helper.ts index 6e6724d..fa991ec 100644 --- a/client/src/test/helper.ts +++ b/client/src/test/helper.ts @@ -16,7 +16,7 @@ export let platformEol: string; */ export async function activate(docUri: vscode.Uri) { // The extensionId is `publisher.name` from package.json - const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!; + const ext = vscode.extensions.getExtension('iSorp.macro-executor')!; await ext.activate(); try { doc = await vscode.workspace.openTextDocument(docUri); @@ -32,7 +32,7 @@ async function sleep(ms: number) { } export const getDocPath = (p: string) => { - return path.resolve(__dirname, '../../testFixture', p); + return path.resolve(__dirname, '../../testSource', p); }; export const getDocUri = (p: string) => { return vscode.Uri.file(getDocPath(p)); diff --git a/client/testSource/src/Test.src b/client/src/test/testSource/src/test.src similarity index 53% rename from client/testSource/src/Test.src rename to client/src/test/testSource/src/test.src index 6509998..82e8a39 100644 --- a/client/testSource/src/Test.src +++ b/client/src/test/testSource/src/test.src @@ -1,13 +1,24 @@ -$INCLUDE def/Test.def +@CONST 1000 +@adr 1000 +@var #1000 ->Label 10 -@functions 1 -@operators 2 -@conditionals 3 -@statements 4 +@CALL M98P +@RETURN M99 -O functions - #c1=ATAN[#j]/[#k] +>LOOP_I 10 + +@FUNCTIONS 1 +@OPERATORS 2 +@CONDITIONALS 3 +@STATEMENTS 4 + +@i 200 +@k 200 +@j 200 + + +O FUNCTIONS + #i=ATAN[#j]/[#k] #i=ABS[#j] #i=BIN[#j] #i=BCD[#j] @@ -33,7 +44,7 @@ O functions ENDIF -O operators +O OPERATORS #i=#j #i=#j+#k #i=#j-#k @@ -45,22 +56,24 @@ O operators #i=#j MOD #k -O conditionals - Label +O CONDITIONALS + >L_Label 10 + + L_Label - GOTO Label + GOTO L_Label - IF [#VAR] THEN #VAR = 1 - ELSE #VAR = 1 + IF [#var] THEN #var = 1 + ELSE #var = 1 - IF [#VAR] THEN - #VAR = 1 - ELSE #VAR = 1 + IF [#var] THEN + #var = 1 + ELSE #var = 1 - IF [#VAR] THEN #VAR = 1 + IF [#var] THEN #var = 1 ELSE - #VAR = 1 + #var = 1 ENDIF IF [#k] THEN @@ -69,13 +82,13 @@ O conditionals ENDIF ENDIF - WHILE [#VAR LT CONST] DO LOOP_I + WHILE [#var LT CONST] DO LOOP_I GOTO L_Label END LOOP_I -O statements - N100 G01 G4.1 G[1] G#VAR X1 Y-[#1+1] F360. - N100 G01 X-#1 F360.1 +O STATEMENTS + N100 G01 G4.1 G[1] G#var X1 Y-[#1+1] F360. + N110 G01 X-#1 F360.1 RETURN \ No newline at end of file diff --git a/client/testSource/def/Test.def b/client/testSource/def/Test.def deleted file mode 100644 index caceee5..0000000 --- a/client/testSource/def/Test.def +++ /dev/null @@ -1,14 +0,0 @@ -$NOLIST - -@CONST 1000 - -@ADR #1000 -@VAR 1000 - -@Sub1 9000 -@Sub2 9001 - - -@CALL M98P - -$LIST diff --git a/client/testSource/lnk/Test.lnk b/client/testSource/lnk/Test.lnk deleted file mode 100644 index 65c0d0c..0000000 --- a/client/testSource/lnk/Test.lnk +++ /dev/null @@ -1,5 +0,0 @@ -/* Macro Library file's -CNC=..\lnk\xxx.MEX - -/* src file's -FILE=Test diff --git a/package-lock.json b/package-lock.json index 32c7655..9f81108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "macro-executor", - "version": "0.1.3", + "version": "0.1.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 31bf321..45cbd5c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "macro-executor", "displayName": "Macro Executor Language", "description": "Fanuc Macro-Executor Programming Language", - "version": "0.1.4", + "version": "0.1.6", "author": "iSorp", "publisher": "iSorp", "license": "MIT", @@ -41,7 +41,8 @@ ], "extensions": [ ".src", - ".def" + ".def", + ".lnk" ], "configuration": "./syntaxes/macro.configuration.json" } @@ -208,8 +209,8 @@ "@types/mocha": "^7.0.1", "@types/node": "^12.11.7", "@typescript-eslint/parser": "^2.3.0", + "eslint": "^6.4.0", "mocha": "^7.0.1", - "typescript": "^3.8.3", - "eslint": "^6.4.0" + "typescript": "^3.8.3" } } diff --git a/scripts/e2e.sh b/scripts/e2e.sh index 860c62e..0bf18cb 100644 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash export CODE_TESTS_PATH="$(pwd)/client/out/test" -export CODE_TESTS_WORKSPACE="$(pwd)/client/testFixture" +export CODE_TESTS_WORKSPACE="$(pwd)/client/testSource" node "$(pwd)/client/out/test/runTest" \ No newline at end of file diff --git a/server/package.json b/server/package.json index 2ba589b..a57878d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "name": "lsp-macro-server", "description": "Fanuc Macro-Executor Programming Language", - "version": "0.0.1", + "version": "0.1.0", "author": "iSorp", "publisher": "iSorp", "license": "MIT", diff --git a/server/src/macroLanguageService/macroLanguageService.ts b/server/src/macroLanguageService/macroLanguageService.ts index 51840bd..8223ee8 100644 --- a/server/src/macroLanguageService/macroLanguageService.ts +++ b/server/src/macroLanguageService/macroLanguageService.ts @@ -26,9 +26,9 @@ export interface LanguageService { findDefinition(document: TextDocument, position: Position, macroFile: Macrofile): Location | null; findReferences(document: TextDocument, position: Position, macroFile: Macrofile): Location[]; findImplementations(document: TextDocument, position: Position, macroFile: Macrofile): Location[]; - findDocumentLinks(document: TextDocument, stylesheet: Macrofile, documentContext: DocumentContext): DocumentLink[]; - findDocumentSymbols(document: TextDocument, stylesheet: Macrofile): SymbolInformation[]; - findCodeLenses(document: TextDocument, stylesheet: Macrofile): CodeLens[]; + findDocumentLinks(document: TextDocument, macrofile: Macrofile, documentContext: DocumentContext): DocumentLink[]; + findDocumentSymbols(document: TextDocument, macrofile: Macrofile): SymbolInformation[]; + findCodeLenses(document: TextDocument, macrofile: Macrofile): CodeLens[]; } function createFacade(parser: Parser, hover: MacroHover, navigation: MacroNavigation, validation: MacroValidation): LanguageService { @@ -41,11 +41,11 @@ function createFacade(parser: Parser, hover: MacroHover, navigation: MacroNaviga findImplementations: navigation.findImplementations.bind(navigation), findDocumentLinks: navigation.findDocumentLinks.bind(navigation), findDocumentSymbols: navigation.findDocumentSymbols.bind(navigation), - findCodeLenses: navigation.findCodeLenses.bind(navigation), + findCodeLenses: navigation.findCodeLenses.bind(navigation) }; } -export function getMacroLanguageService(options?: LanguageServiceOptions): LanguageService { +export function getMacroLanguageService(options: LanguageServiceOptions): LanguageService { return createFacade( new Parser(options && options.fileProvider), diff --git a/server/src/macroLanguageService/macroLanguageTypes.ts b/server/src/macroLanguageService/macroLanguageTypes.ts index f612f0b..edc8649 100644 --- a/server/src/macroLanguageService/macroLanguageTypes.ts +++ b/server/src/macroLanguageService/macroLanguageTypes.ts @@ -32,7 +32,7 @@ export interface FindDocumentLinks { export interface LanguageServiceOptions { - fileProvider?: MacroFileProvider; + fileProvider: MacroFileProvider; } export interface MacroFileType { diff --git a/server/src/macroLanguageService/parser/macroErrors.ts b/server/src/macroLanguageService/parser/macroErrors.ts index 92c876d..fb21822 100644 --- a/server/src/macroLanguageService/parser/macroErrors.ts +++ b/server/src/macroLanguageService/parser/macroErrors.ts @@ -34,6 +34,7 @@ export const ParseError = { OperatorExpected: new MacroIssueType('macro-operatorexpected', localize('expected.operator', 'operator expected')), IdentifierExpected: new MacroIssueType('macro-identifierexpected', localize('expected.ident', 'identifier expected')), AddressExpected: new MacroIssueType('macro-addressexpected', localize('expected.address', 'address expected')), + ValueExpected: new MacroIssueType('macro-valueexpected', localize('expected.value', 'value expected')), BodyExpected: new MacroIssueType('macro-bodyexpected', localize('expected.body', 'body expected')), LabelExpected: new MacroIssueType('macro-labelexpected', localize('expected.label', 'label expected')), TermExpected: new MacroIssueType('macro-termexpected', localize('expected.term', 'term expected')), diff --git a/server/src/macroLanguageService/parser/macroNodes.ts b/server/src/macroLanguageService/parser/macroNodes.ts index 9168247..10e734b 100644 --- a/server/src/macroLanguageService/parser/macroNodes.ts +++ b/server/src/macroLanguageService/parser/macroNodes.ts @@ -421,6 +421,8 @@ export enum NodeType { Undefined, MacroFile, DefFile, + LnkFile, + LinkNode, Include, StringLiteral, Declarations, @@ -453,6 +455,38 @@ export enum NodeType { SequenceNumber, } +export class LnkFile extends Node { + + constructor(offset: number, length: number) { + super(offset, length); + } + + public get type(): NodeType { + return NodeType.LnkFile; + } +} + +export class LinkNode extends Node { + + file?: Node; + + constructor(offset: number, length: number) { + super(offset, length); + } + + public get type(): NodeType { + return NodeType.LinkNode; + } + + public setFile(node: Node | null): node is Node { + return this.setNode('file', node, 0); + } + + public getFile(): Node | undefined { + return this.file; + } +} + export class MacroFile extends Node { constructor(offset: number, length: number) { @@ -932,6 +966,7 @@ export class Assignment extends Node { export enum ValueType { String = 'string', Numeric = 'numeric', + Constant = 'constant', // same as numeric in capital MacroValue = 'value', Address = 'address', MFunc= 'm-function', diff --git a/server/src/macroLanguageService/parser/macroParser.ts b/server/src/macroLanguageService/parser/macroParser.ts index ecdc72b..e43b335 100644 --- a/server/src/macroLanguageService/parser/macroParser.ts +++ b/server/src/macroLanguageService/parser/macroParser.ts @@ -20,17 +20,15 @@ export interface IMark { export class Parser { - private scanner: Scanner = new Scanner(); private textProvider?: nodes.ITextProvider; private token: IToken; private prevToken?: IToken; private lastErrorToken?: IToken; - - private imports:string[] = []; private declarations:Map = new Map() + private includes:string[] = [] - constructor(private fileProvider?: MacroFileProvider) { + constructor(private fileProvider: MacroFileProvider) { this.token = { type: TokenType.EOF, offset: -1, len: 0, text: '' }; this.prevToken = undefined; } @@ -303,12 +301,13 @@ export class Parser { //#region handle declaraions private resolveIncludes(node:nodes.Include) { let uri = node.getData('uri'); - if (!uri) {return;} + if (!uri) { + return; + } - this.imports.push(uri); let declaration = this.fileProvider?.get(uri); - if (declaration) { + this.includes.push(declaration.document.uri); (declaration?.macrofile).accept(candidate => { let found = false; if (candidate.type === nodes.NodeType.VariableDef || candidate.type === nodes.NodeType.labelDef) { @@ -350,6 +349,7 @@ export class Parser { const symbol = this.create(nodes.Symbol); symbol.referenceTypes = [nodes.ReferenceType.Variable]; + const isUpperCase = this.token.text == this.token.text.toUpperCase(); if (!this.accept(TokenType.Symbol)){ return this.finish(node, ParseError.IdentifierExpected); @@ -358,9 +358,27 @@ export class Parser { const value = this.createNode(nodes.NodeType.DeclarationValue); - - if (node.setValue(this.parseNumeric())){ - node.valueType = nodes.ValueType.Numeric; + if (this.peekDelim('+') || this.peekDelim('-')) { + this.consumeToken(); + if (node.setValue(this.parseNumeric())){ + if (isUpperCase){ + node.valueType = nodes.ValueType.Constant; + } + else { + node.valueType = nodes.ValueType.Numeric; + } + } + else{ + return this.finish(node, ParseError.ValueExpected); + } + } + else if (node.setValue(this.parseNumeric())) { + if (isUpperCase){ + node.valueType = nodes.ValueType.Constant; + } + else { + node.valueType = nodes.ValueType.Numeric; + } } else if (this.accept(TokenType.Hash)){ if (node.setValue(this.parseNumeric())){ @@ -437,22 +455,28 @@ export class Parser { // #region Global scope public parseMacroFile(textDocument: TextDocument): nodes.MacroFile { + this.declarations.clear(); + this.includes = []; const versionId = textDocument.version; const text = textDocument.getText(); this.textProvider = (offset: number, length: number) => { - // TODO no idea why the versions are not equivalent - /*if (textDocument.version !== versionId) { + if (textDocument.version !== versionId) { throw new Error('Underlying model has changed, AST is no longer valid'); - }*/ + } return text.substr(offset, length); }; - let type = textDocument.uri.split('.').pop(); - if (type?.toLocaleLowerCase() === 'def'){ + let type = textDocument.uri.split('.').pop()?.toLocaleLowerCase() ; + if (type === 'def'){ return this.internalParse(text, this._parseDefFile, this.textProvider); } - else{ + else if (type === 'src'){ return this.internalParse(text, this._parseMacroFile, this.textProvider); + } + else if (type === 'lnk'){ + return this.internalParse(text, this._parseLnkFile, this.textProvider); } + + return this.createNode(nodes.NodeType.Undefined); } public internalParse(input: string, parseFunc: () => U, textProvider?: nodes.ITextProvider): U { @@ -469,6 +493,59 @@ export class Parser { return node; } + public _parseLnkFile(): nodes.MacroFile { + const node = this.createNode(nodes.NodeType.DefFile); + let hasMatch = false; + do { + do { + let child = null; + hasMatch = false; + child = this._parseLinkNode(); + if (child){ + node.addChild(child); + hasMatch = true; + } + } while (hasMatch); + + if (this.peek(TokenType.EOF)) { + break; + } + + /*let child = this.parseUnexpected(); + if (child){ + node.addChild(child); + hasMatch = true; + }*/ + this.consumeToken(); + + } while (!this.peek(TokenType.EOF)); + return this.finish(node); + } + + public _parseLinkNode() : nodes.LinkNode | null { + let node = this.create(nodes.LinkNode); + + if (!this.peekKeyword('file')){ + return null; + } + + this.consumeToken(); + + if (!this.acceptDelim('=')) { + this.finish(node, ParseError.EqualExpected, [TokenType.NewLine]); + } + + if (this.peek(TokenType.Symbol)){ + let file = this.create(nodes.Node); + this.acceptUnquotedString(); + node.setFile(this.finish(file)); + }else{ + this.finish(node, ParseError.EqualExpected, [TokenType.NewLine]); + } + + return this.finish(node); + } + public _parseDefFile(): nodes.MacroFile { const node = this.createNode(nodes.NodeType.DefFile); let hasMatch = false; @@ -544,6 +621,8 @@ export class Parser { this.consumeToken(); } while (!this.peek(TokenType.EOF)); + + node.setData('includes', this.includes); return this.finish(node); } diff --git a/server/src/macroLanguageService/services/lint.ts b/server/src/macroLanguageService/services/lint.ts index 214158f..32d4a26 100644 --- a/server/src/macroLanguageService/services/lint.ts +++ b/server/src/macroLanguageService/services/lint.ts @@ -15,9 +15,8 @@ const _dot = '.'.charCodeAt(0); export class LintVisitor implements nodes.IVisitor { - static entries(macrofile: nodes.Node, document: TextDocument, fileProvider?: MacroFileProvider): nodes.IMarker[] { + static entries(macrofile: nodes.Node, document: TextDocument, fileProvider: MacroFileProvider): nodes.IMarker[] { const visitor = new LintVisitor(macrofile, fileProvider); - visitor.LoadIncludes(macrofile); macrofile.acceptVisitor(visitor); return visitor.getEntries(); } @@ -25,11 +24,12 @@ export class LintVisitor implements nodes.IVisitor { private declarations:Map = new Map() private sequenceNumbers:FunctionMap = new FunctionMap(); private labelList:FunctionMap = new FunctionMap(); + private imports: string[] = []; private rules: nodes.IMarker[] = []; - private imports:string[] = []; + private functionList = new Array(); - private constructor(private macrofile: nodes.Node, private fileProvider?: MacroFileProvider) { } + private constructor(private macrofile: nodes.Node, private fileProvider: MacroFileProvider) { } public getEntries(filter: number = (nodes.Level.Warning | nodes.Level.Error)): nodes.IMarker[] { return this.rules.filter(entry => { @@ -46,6 +46,8 @@ export class LintVisitor implements nodes.IVisitor { switch (node.type) { case nodes.NodeType.MacroFile: return this.visitGlobalScope(node); + case nodes.NodeType.Include: + return this.visitIncludes(node); case nodes.NodeType.Symbol: return this.visitSymbols(node); case nodes.NodeType.Variable: @@ -66,28 +68,11 @@ export class LintVisitor implements nodes.IVisitor { return true; } - private LoadIncludes(node:nodes.MacroFile) { - node.accept(candidate => { - if (candidate.type === nodes.NodeType.Include) { - this.visitInclude(candidate); - return false; - } - return true; - }); - } - - private visitGlobalScope(node: nodes.Node) : boolean { - for (const element of node.getChildren()) { - if (element.type === nodes.NodeType.Symbol){ - this.addEntry(element, Rules.IllegalStatement); - } - } - return true; - } - - private visitInclude(node: nodes.Include) { + private visitIncludes(node: nodes.Include) : boolean { let uri = node.getChild(0); - if (!uri) {return;} + if (!uri) { + return false; + } if (this.imports.indexOf(uri?.getText()) > -1){ this.addEntry(node, Rules.DuplicateDeclarations); @@ -108,12 +93,20 @@ export class LintVisitor implements nodes.IVisitor { } else { this.addEntry(node, Rules.IncludeNotFound); - return; } } + return false; + } + + private visitGlobalScope(node: nodes.Node) : boolean { + for (const element of node.getChildren()) { + if (element.type === nodes.NodeType.Symbol){ + this.addEntry(element, Rules.IllegalStatement); + } + } + return true; } - private functionList = new Array(); private visitFunction(node: nodes.Function): boolean { let ident = node.getIdentifier(); @@ -184,11 +177,11 @@ export class LintVisitor implements nodes.IVisitor { this.addEntry(def, Rules.DuplicateDeclarations); } - for (const element of this.declarations.values()){ + /*for (const element of this.declarations.values()){ if (element.getValue()?.getText() === def.getValue()?.getText()){ - //this.addEntry(def, Rules.DuplicateAddress); + this.addEntry(def, Rules.DuplicateAddress); } - } + }*/ if (node.type === nodes.NodeType.VariableDef) { this.declarations.set(ident, node); diff --git a/server/src/macroLanguageService/services/macroHover.ts b/server/src/macroLanguageService/services/macroHover.ts index 9b865f1..1fdb3c4 100644 --- a/server/src/macroLanguageService/services/macroHover.ts +++ b/server/src/macroLanguageService/services/macroHover.ts @@ -12,7 +12,7 @@ import { TextDocument, Range, Position, Location, Hover, MarkedString, MarkupCon export class MacroHover { - constructor(private fileProvider?: MacroFileProvider | undefined) { } + constructor(private fileProvider: MacroFileProvider) { } public doHover(document: TextDocument, position: Position, macroFile: nodes.MacroFile): Hover | null { function getRange(node: nodes.Node) { diff --git a/server/src/macroLanguageService/services/macroNavigation.ts b/server/src/macroLanguageService/services/macroNavigation.ts index ba2117d..32f8c69 100644 --- a/server/src/macroLanguageService/services/macroNavigation.ts +++ b/server/src/macroLanguageService/services/macroNavigation.ts @@ -14,16 +14,26 @@ import { Symbols, Symbol } from '../parser/macroSymbolScope'; import { CodeLens } from 'vscode-languageserver'; +class FunctionMap { + private elements:Map = new Map(); + public add(key:string, value:string){ + if (!this.elements.has(key)){ + this.elements.set(key, new Array()); + } + this.elements.get(key)?.push(value); + } + + public get(key:string) : string[] | undefined { + return this.elements.get(key); + } +} + export class MacroNavigation { - constructor(private fileProvider?: MacroFileProvider){} + constructor(private fileProvider: MacroFileProvider){} public findDefinition(document: TextDocument, position: Position, macroFile: nodes.Node): Location | null { - let includes:string[] = []; - if (this.fileProvider){ - includes = this.getIncludeUris(macroFile, this.fileProvider); - } - + const includes = this.getIncludeUris(macroFile, this.fileProvider); includes.push(document.uri); const offset = document.offsetAt(position); const node = nodes.getNodeAtOffset(macroFile, offset); @@ -168,24 +178,24 @@ export class MacroNavigation { return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); } - let codeLenses: CodeLens[] = []; - let declarations:nodes.Node[] = []; - - if (!this.fileProvider){ - return codeLenses; - } + const codeLenses: CodeLens[] = []; + const declarations:FunctionMap = new FunctionMap(); // local search if (macroFile.type === nodes.NodeType.MacroFile) { macroFile.accept(candidate => { if (candidate.type === nodes.NodeType.Variable || candidate.type === nodes.NodeType.label) { - declarations.push(candidate); - } + const node = (candidate).getSymbol(); + if (node) { + declarations.add(node.getText(), node.getText()); + } + return false; + } return true; }); } // global search - { + else { const types = this.fileProvider?.getAll(); for (const type of types) { @@ -194,16 +204,19 @@ export class MacroNavigation { } if ((type.macrofile).type === nodes.NodeType.MacroFile) { - let includes = this.getIncludeUris(type.macrofile, this.fileProvider); + const includes = this.getIncludeUris(type.macrofile, this.fileProvider); if (includes.filter(uri => uri === document.uri).length <= 0) { continue; } } - (type.macrofile).accept(candidate => { if (candidate.type === nodes.NodeType.Variable || candidate.type === nodes.NodeType.label) { - declarations.push(candidate); + const node = (candidate).getSymbol(); + if (node) { + declarations.add(node.getText(), node.getText()); + } + return false; } return true; }); @@ -211,39 +224,34 @@ export class MacroNavigation { } (macroFile).accept(candidate => { - if (candidate.type === nodes.NodeType.VariableDef || candidate.type === nodes.NodeType.labelDef /*|| candidate.type === nodes.NodeType.Function*/) { - - let node:nodes.Node | undefined; - /*if (candidate.type === nodes.NodeType.Function){ - node = (candidate).getIdentifier(); - }*/ - { - node = (candidate).getSymbol(); - } - let count = declarations.filter(a => (>a).getSymbol()?.getText() === node?.getText()); + if (candidate.type === nodes.NodeType.VariableDef || candidate.type === nodes.NodeType.labelDef) { + const node = (candidate).getSymbol(); if (node) { - let pos = document.positionAt(node.offset); + const value = declarations.get(node.getText()); //declarations.filter(a => a === node?.getText()); + const count = value?.length; + const pos = document.positionAt(node.offset); + const c = count === undefined ? 0 : count; codeLenses.push( { range: getRange(node), data: { - title: count.length + (count.length > 1 ? ' references found' : ' reference found'), + title: c + (c !== 1 ? ' references' : ' reference'), uri:document.uri, line:pos.line, character:pos.character, type: MacroCodeLensCommand.References } }); - return false; } + return false; } return true; }); return codeLenses; } - + private uriLiteralNodeToDocumentLink(document: TextDocument, uriLiteralNode: nodes.Node, documentContext: DocumentContext): DocumentLink | null { if (uriLiteralNode.getChildren().length === 0) { return null; @@ -313,22 +321,18 @@ export class MacroNavigation { private findGlobalReferences(includeUri:string, position:Position, node:nodes.Node, implType:nodes.ReferenceType | undefined = undefined):Location[] { let locations:Location[] = []; - let declarations:MacroFileType[] = []; - - if (this.fileProvider){ - declarations = this.fileProvider?.getAll(); - } + let declarations = this.fileProvider?.getAll(); for (const type of declarations) { // only accept the origin def file - if (((type.macrofile).type === nodes.NodeType.DefFile && includeUri !== type.document.uri.toLocaleLowerCase())){ + if (((type.macrofile).type === nodes.NodeType.DefFile && includeUri !== type.document.uri)){ continue; } // only accept a src file which includes the origin def file - if (this.fileProvider && (type.macrofile).type === nodes.NodeType.MacroFile) { - let includes = this.getIncludeUris(type.macrofile, this.fileProvider); + if ((type.macrofile).type === nodes.NodeType.MacroFile) { + const includes = this.getIncludeUris(type.macrofile, this.fileProvider); if (includes.filter(uri => uri === includeUri).length <= 0){ continue; } @@ -342,7 +346,7 @@ export class MacroNavigation { const name = node.getText(); // only accept the symbol of the origin symbol source file - if (symbol && includeUri !== type.document.uri.toLocaleLowerCase()) { + if (symbol && includeUri !== type.document.uri) { continue; // local declaration found } @@ -379,13 +383,19 @@ export class MacroNavigation { return locations; } + /** + * Finds the uri of the declaration origin of a certain node + * @param document + * @param node + * @param macroFile + */ private findIncludeUri(document: TextDocument, node: nodes.Node, macroFile: nodes.Node): string | null { - let includes:string[] = []; - - if (this.fileProvider){ - includes = this.getIncludeUris(macroFile, this.fileProvider); + let includes:string[] = [] + const uris = this.getIncludeUris(macroFile, this.fileProvider); + if (uris) { + includes = uris; } - + includes.push(document.uri); if (!node) { return null; @@ -408,22 +418,13 @@ export class MacroNavigation { return null; } + /** + * Gets the include uris of a macrofile + * @param macroFile + * @param fileProvider + */ private getIncludeUris(macroFile: nodes.MacroFile, fileProvider:MacroFileProvider) : string[] { - let includeUris:string[] = []; - macroFile.accept(candidate => { - if (candidate.type === nodes.NodeType.Include) { - const uriStringNode = candidate.getChild(0); - if (uriStringNode) { - const f = fileProvider.getLink(uriStringNode?.getText()); - if (f){ - includeUris.push(f); - } - } - return false; - } - return true; - }); - return includeUris; + return macroFile.getData('includes'); } private getHighlightKind(node: nodes.Node): DocumentHighlightKind { diff --git a/server/src/macroLanguageService/services/macroValidation.ts b/server/src/macroLanguageService/services/macroValidation.ts index efbcda0..4fedb74 100644 --- a/server/src/macroLanguageService/services/macroValidation.ts +++ b/server/src/macroLanguageService/services/macroValidation.ts @@ -12,7 +12,7 @@ export class MacroValidation { private settings?: LanguageSettings; - constructor( private fileProvider?: MacroFileProvider) {} + constructor( private fileProvider: MacroFileProvider) {} public configure(settings?: LanguageSettings) { this.settings = settings; diff --git a/server/src/server.ts b/server/src/server.ts index 117e79a..44f4c37 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -5,7 +5,7 @@ 'use strict'; import * as path from 'path'; -import { readFileSync} from 'fs'; +import { readFileSync, existsSync } from 'fs'; import { LanguageSettings, MacroFileProvider, FindDocumentLinks, Range } from './macroLanguageService/macroLanguageTypes'; import { Parser } from './macroLanguageService/parser/macroParser'; @@ -31,6 +31,8 @@ import { FileChangeType, DidChangeWatchedFilesParams, CodeLensParams, + CodeLens, + TextDocumentChangeEvent, } from 'vscode-languageserver'; import { @@ -68,7 +70,7 @@ let globalSettings: LanguageSettings = defaultSettings; class FileProvider implements MacroFileProvider { - private links = new Links(); + private resolver = new Links(); get(file: string): MacroFileType | undefined { @@ -76,16 +78,20 @@ class FileProvider implements MacroFileProvider { return undefined; } - let uri = this.links.resolveReference(file); + let uri = this.resolver.resolveReference(file); if (uri) { - let doc = getParsedDocument(uri, new Parser(this).parseMacroFile); + + let doc = getParsedDocument(uri, (document => { + let parser = new Parser(this); + return parser.parseMacroFile(document); + })); if (!doc) { try { const file = readFileSync(Files.uriToFilePath(uri)!, 'utf-8'); let document = TextDocument.create(uri!, 'macro', 1, file.toString()); try { + let macrofile = new Parser(this).parseMacroFile(document); - doc = { macrofile: macrofile, document: document, @@ -110,12 +116,11 @@ class FileProvider implements MacroFileProvider { } getAll(): MacroFileType[] { - let types:MacroFileType[] = []; try { - if (workspaceFolder){ + if (workspaceFolder) { let dir = Files.uriToFilePath(workspaceFolder); - let files = glob.sync(dir+'/**/*.{[sS][rR][cC],[dD][eE][fF]}'); + let files = glob.sync(dir+'/**/*.{[sS][rR][cC],[dD][eE][fF],[lL][nN][kK]}'); for (const file of files) { let type = this.get(file); if (type){ @@ -126,16 +131,21 @@ class FileProvider implements MacroFileProvider { }catch (err){ connection.console.log(err); } - return types; } getLink(ref:string) : string |undefined { - return this.links.resolveReference(ref); + return this.resolver.resolveReference(ref); } } class Links implements FindDocumentLinks { + + /** + * Returns a given path as uri + * @param ref + * @param base + */ public resolveReference(ref: string, base?: string): string | undefined { if (!workspaceFolder){ return ''; @@ -159,26 +169,20 @@ class Links implements FindDocumentLinks { return undefined; } - file = path.normalize(file.toLocaleLowerCase()); - - // Workaround to get the case-sensitive path of a non case-sensitive path - // TODO find correct solution - let files = glob.sync(Files.uriToFilePath(workspaceFolder)+'/**/*.{[sS][rR][cC],[dD][eE][fF]}'); - let filter = files.filter(a => { - let b = path.normalize(a.toLocaleLowerCase()); - if (b === file){ - return true; - } - else { - return false; - } - }); + file = this.resolvePathCaseSensitive(file); - if (filter && filter.length > 0) { - return URI.file(filter[0]).toString(); + if (file) { + return URI.file(file).toString(); } else {return '';} } + + private resolvePathCaseSensitive(file:string) { + let norm = path.normalize(file) + let root = path.parse(norm).root + let p = norm.slice(Math.max(root.length - 1, 0)) + return glob.sync(p, { nocase: true, cwd: root })[0] + } } const macroLanguageService = getMacroLanguageService({ @@ -232,27 +236,11 @@ connection.onInitialized(async () => { connection.client.register(DidChangeConfigurationNotification.type, undefined); } settings = await getSettings(); - validateWorkspace(); + Promise.resolve(revalidate(true)); }); connection.onCodeLens(codelens); -connection.onCodeLensResolve(handler => { - let data:MacroCodeLensType = handler.data; - let command:string = ''; - if (data.type === MacroCodeLensCommand.References){ - command = 'macro.codelens.references'; - } - - return { - range: handler.range, - command: { - command: command, - title:data.title, - arguments: [data.line, data.character] - } - }; -}); - +connection.onCodeLensResolve(codeLensResolve); connection.onDidChangeWatchedFiles(watchedFiles); connection.onDidChangeConfiguration(configuration); connection.onDefinition(definition); @@ -262,14 +250,7 @@ connection.onDocumentSymbol(documentSymbol); connection.onDocumentLinks(documentLinks); connection.onHover(hower); -documents.onDidChangeContent(change => { - validateTextDocument(getParsedDocument(change.document.uri, macroLanguageService.parseMacroFile)); - - if (change.document.uri.split('.').pop()?.toLocaleLowerCase() === 'def') { - Promise.resolve(validateOpenDocuments()); - } -}); - +documents.onDidChangeContent(content); documents.listen(connection); connection.listen(); @@ -296,11 +277,19 @@ function getSettings(): Thenable { return result; } +function content(change:TextDocumentChangeEvent) { + validateTextDocument(getParsedDocument(change.document.uri, macroLanguageService.parseMacroFile)); + + // TODO validate files only which include this def file + if (change.document.uri.split('.').pop()?.toLocaleLowerCase() === 'def') { + Promise.resolve(revalidate(false)); + } +} + function hower(params: TextDocumentPositionParams) { let repo = getParsedDocument(params.textDocument.uri, macroLanguageService.parseMacroFile); if (!repo) {return null;} return macroLanguageService.doHover(repo.document, params.position, repo.macrofile); - } function codelens(params: CodeLensParams) { @@ -313,6 +302,24 @@ function codelens(params: CodeLensParams) { } } +function codeLensResolve(handler:CodeLens) { + + let data:MacroCodeLensType = handler.data; + let command:string = ''; + if (data.type === MacroCodeLensCommand.References){ + command = 'macro.codelens.references'; + } + + return { + range: handler.range, + command: { + command: command, + title:data.title, + arguments: [data.line, data.character] + } + }; +} + function definition(params: DefinitionParams) { let repo = getParsedDocument(params.textDocument.uri, macroLanguageService.parseMacroFile); if (!repo) {return null;} @@ -329,14 +336,12 @@ function implementations(params: ImplementationParams) { let repo = getParsedDocument(params.textDocument.uri, macroLanguageService.parseMacroFile); if (!repo) {return null;} return macroLanguageService.findImplementations(repo.document, params.position, repo.macrofile); - return null; } function documentSymbol(params: DocumentSymbolParams) { let repo = getParsedDocument(params.textDocument.uri, macroLanguageService.parseMacroFile); if (!repo) {return null;} return macroLanguageService.findDocumentSymbols(repo.document, repo.macrofile); - return null; } function documentLinks(params: DocumentLinkParams) { @@ -345,29 +350,17 @@ function documentLinks(params: DocumentLinkParams) { return macroLanguageService.findDocumentLinks(repo.document, repo.macrofile, new Links()); } -async function validateTextDocument(doc: MacroFileType | undefined) { +function validateTextDocument(doc: MacroFileType | undefined) { if (!doc) {return;} try { - let settings = await getSettings(); - const diagnostics: Diagnostic[] = []; - if (doc.document.languageId === 'macro') { - - if (doc.document) { - if (macroLanguageService.doValidation && doc.macrofile) { - let entries = macroLanguageService.doValidation(doc.document, doc.macrofile, settings); - let index = 0; - for (const entry of entries) { - if (maxNumberOfProblems <= index){ - break; - } - diagnostics.push(entry); - ++index; - } - connection.sendDiagnostics({ uri: doc.document.uri, diagnostics }); - } + if (doc.document) { + if (macroLanguageService.doValidation && doc.macrofile) { + const entries = macroLanguageService.doValidation(doc.document, doc.macrofile, settings); + const diagnostics: Diagnostic[] = entries; + connection.sendDiagnostics({ uri: doc.document.uri, diagnostics }); } } } catch (e) { @@ -376,47 +369,53 @@ async function validateTextDocument(doc: MacroFileType | undefined) { } } -async function validateWorkspace() { - if (settings && settings?.validate?.workspace){ - let fp = new FileProvider(); - let types = fp.getAll(); - for (const element of types){ - validateTextDocument(element); - } - } -} - -async function validateOpenDocuments() { - for (const document of documents.all()){ - validateTextDocument(getParsedDocument(document.uri, macroLanguageService.parseMacroFile)); - } -} - function watchedFiles(handler:DidChangeWatchedFilesParams){ for (const file of handler.changes) { if (file.type === FileChangeType.Deleted){ - parsedDocuments.delete(file.uri); - validateWorkspace(); + parsedDocuments.clear(); + revalidate(true); } else if (file.type === FileChangeType.Changed) { - // if the file is not opened it was changed extern. - // n the parsedDocuments repo it is not up-to-date anymore. + // if the file is not opened it was changed external + // so the parsedDocuments repo is not up-to-date anymore. if (!documents.get(file.uri)){ parsedDocuments.delete(file.uri); } } else if (file.type === FileChangeType.Created) { - validateWorkspace(); + parsedDocuments.clear(); + revalidate(true); } } } -function getParsedDocument(uri: string, parser:((document:TextDocument) => Macrofile)) : MacroFileType | undefined { +function revalidate(workspace:boolean) { + if (workspace && settings && settings?.validate?.workspace){ + let fp = new FileProvider(); + let types = fp.getAll(); + for (const element of types){ + validateTextDocument(element); + } + } else{ + for (const document of documents.all()){ + validateTextDocument(getParsedDocument(document.uri, macroLanguageService.parseMacroFile,true)); + } + } +} + +/** + * Returns the parsed version of a TextDocument. + * An open TextDocument will be reparsd if the Version is newer than the parsed version. + * @param uri + * @param parser + * @param parse + */ +function getParsedDocument(uri: string, parser:((document:TextDocument) => Macrofile), parse:boolean=false) : MacroFileType | undefined { let document = documents.get(uri); if (document) { let parsed = parsedDocuments.get(uri); if (parsed) { - if (document.version !== parsed.version){ + if (document.version !== parsed.version || parse){ parsedDocuments.set(uri , { macrofile: parser(document), document: document,