From 972e59d79bd72780350f1d2cec3f10ac90d4c51e Mon Sep 17 00:00:00 2001 From: Dejan Date: Sat, 18 Sep 2021 11:57:15 +0200 Subject: [PATCH 01/12] Initial language server setup for pyFlies extension --- .gitignore | 2 + .vscode/launch.json | 28 +++++++++-- .vscode/settings.json | 4 +- package.json | 9 ++++ server/__init__.py | 0 server/__main__.py | 44 +++++++++++++++++ src/extension.ts | 112 +++++++++++++++++++++++++++++++++++------- 7 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 server/__init__.py create mode 100644 server/__main__.py diff --git a/.gitignore b/.gitignore index 04b5b0d..8464473 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules .vscode-test/ *.vsix code-extensions/ +env/ +*.log diff --git a/.vscode/launch.json b/.vscode/launch.json index ebc23ff..2613a57 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "configurations": [ { - "name": "Run Extension", + "name": "Launch Client", "type": "extensionHost", "request": "launch", "args": [ @@ -16,8 +16,24 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "${defaultBuildTask}", + "env": { + "VSCODE_DEBUG_MODE": "true" + } }, + { + "name": "Launch Server", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--tcp"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, { "name": "Extension Tests", "type": "extensionHost", @@ -31,5 +47,11 @@ ], "preLaunchTask": "${defaultBuildTask}" } - ] + ], + "compounds": [ + { + "name": "Server + Client", + "configurations": ["Launch Server", "Launch Client"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..da582b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,7 @@ "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + + "python.pythonPath": "./env/Scripts/python" } \ No newline at end of file diff --git a/package.json b/package.json index 1839020..0085ef6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,12 @@ ], "main": "./out/extension.js", "contributes": { + "commands": [ + { + "command": "countDownBlocking", + "title": "Count down 10 seconds [Blocking]" + } + ], "grammars": [ { "language": "pyflies", @@ -128,6 +134,9 @@ "pretest": "yarn run compile && yarn run lint", "test": "node ./out/test/runTest.js" }, + "dependencies": { + "vscode-languageclient": "^7.0.0" + }, "devDependencies": { "@types/vscode": "^1.50.0", "@types/glob": "^7.1.3", diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/__main__.py b/server/__main__.py new file mode 100644 index 0000000..2379b49 --- /dev/null +++ b/server/__main__.py @@ -0,0 +1,44 @@ +import argparse +import logging + +from .pyflies_ls import pyflies_server + +logging.basicConfig(filename="pygls.log", level=logging.DEBUG, filemode="w") + + +def add_arguments(parser): + parser.description = "PyFlies language server" + + parser.add_argument( + "--tcp", action="store_true", + help="Use TCP server" + ) + parser.add_argument( + "--ws", action="store_true", + help="Use WebSocket server" + ) + parser.add_argument( + "--host", default="127.0.0.1", + help="Bind to this address" + ) + parser.add_argument( + "--port", type=int, default=2087, + help="Bind to this port" + ) + + +def main(): + parser = argparse.ArgumentParser() + add_arguments(parser) + args = parser.parse_args() + + if args.tcp: + pyflies_server.start_tcp(args.host, args.port) + elif args.ws: + pyflies_server.start_ws(args.host, args.port) + else: + pyflies_server.start_io() + + +if __name__ == '__main__': + main() diff --git a/src/extension.ts b/src/extension.ts index 69acc98..b25a8be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,29 +1,107 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; +import * as net from "net"; +import * as path from "path"; + +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind +} from 'vscode-languageclient/node'; + +let client: LanguageClient; + +function getClientOptions(): LanguageClientOptions { + return { + // Register the server for plain text documents + documentSelector: ["*"], + outputChannelName: "pyFlies", + }; +} + +function isStartedInDebugMode(): boolean { + return process.env.VSCODE_DEBUG_MODE === "true"; +} + +function startLangServerTCP(addr: number): LanguageClient { + const serverOptions: ServerOptions = () => { + return new Promise((resolve /*, reject */) => { + const clientSocket = new net.Socket(); + clientSocket.connect(addr, "127.0.0.1", () => { + resolve({ + reader: clientSocket, + writer: clientSocket, + }); + }); + }); + }; + + // 'pyFlies LS (port ${addr})' + return new LanguageClient( + `tcp lang server (port ${addr})`, + serverOptions, + getClientOptions() + ); +} + +function startLangServer( + command: string, + args: string[], + cwd: string +): LanguageClient { + const serverOptions: ServerOptions = { + args, + command, + options: { cwd }, + }; + + return new LanguageClient(command, serverOptions, getClientOptions()); +} + // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { + if (isStartedInDebugMode()){ + // Development - Run the server manually + client = startLangServerTCP(2087); + } else { + // Production - Client is going to run the server (for use within `.vsix` package) + const cwd = path.join(__dirname, "..", ".."); + const pythonPath = vscode.workspace + .getConfiguration("python") + .get("pythonPath"); + + if (!pythonPath) { + throw new Error("`python.pythonPath` is not set"); + } + + client = startLangServer(pythonPath, ["-m", "server"], cwd); + } + + context.subscriptions.push(client.start()); + + var mdTableExtension = vscode.extensions.getExtension('TakumiI.markdowntable'); - var mdTableExtension = vscode.extensions.getExtension('TakumiI.markdowntable'); - - if (mdTableExtension !== undefined) { - if (mdTableExtension.isActive === false) { - mdTableExtension.activate().then( - function () { - vscode.window.showInformationMessage("Markdown Table extension activated"); - }, - function () { - vscode.window.showErrorMessage("Markdown Table activation failed"); - } - ); - } - } else { - vscode.window.showErrorMessage("Markdown Table not found!"); - } + if (mdTableExtension !== undefined) { + if (mdTableExtension.isActive === false) { + mdTableExtension.activate().then( + function () { + vscode.window.showInformationMessage("Markdown Table extension activated"); + }, + function () { + vscode.window.showErrorMessage("Markdown Table activation failed"); + } + ); + } + } else { + vscode.window.showErrorMessage("Markdown Table not found!"); + } } // this method is called when your extension is deactivated -export function deactivate() { +export function deactivate(): Thenable { + return client ? client.stop() : Promise.resolve(); } From 8c345714ce58098b3adc603e13e19c74a910f139 Mon Sep 17 00:00:00 2001 From: Dejan Date: Sat, 18 Sep 2021 11:59:57 +0200 Subject: [PATCH 02/12] Added file validation feature. Basic setups for completion and code actions. --- .gitignore | 1 + server/features/__init__.py | 0 server/features/code_actions.py | 23 ++++++++++++ server/features/completion.py | 16 ++++++++ server/features/validate.py | 49 ++++++++++++++++++++++++ server/pyflies_ls.py | 66 +++++++++++++++++++++++++++++++++ server/util.py | 4 ++ 7 files changed, 159 insertions(+) create mode 100644 server/features/__init__.py create mode 100644 server/features/code_actions.py create mode 100644 server/features/completion.py create mode 100644 server/features/validate.py create mode 100644 server/pyflies_ls.py create mode 100644 server/util.py diff --git a/.gitignore b/.gitignore index 8464473..30652c4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules *.vsix code-extensions/ env/ +__pycache__/ *.log diff --git a/server/features/__init__.py b/server/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/features/code_actions.py b/server/features/code_actions.py new file mode 100644 index 0000000..60946cb --- /dev/null +++ b/server/features/code_actions.py @@ -0,0 +1,23 @@ +import difflib +from pygls.lsp import CodeAction, CodeActionKind, Command, WorkspaceEdit, TextEdit, Range, Position +from ..util import load_document + +def process_quick_fix(ls, diag, text_document): + if diag.message.__contains__('Unknown object'): + obj = diag.message.split('"')[1] + + print(obj) + diag.range.end.character = diag.range.start.character + obj.__len__() + + new_text = determine_fix(obj, load_document(ls, text_document.uri), diag.message) + fix = CodeAction(title='Fix typo', + kind=CodeActionKind.QuickFix, + edit=WorkspaceEdit( + changes={text_document.uri : [TextEdit(range=diag.range, new_text=new_text)] } + )) + return [fix] + + +def determine_fix(obj, source, error_message): + # TODO: setup logic + return 'Real' \ No newline at end of file diff --git a/server/features/completion.py b/server/features/completion.py new file mode 100644 index 0000000..a2bd678 --- /dev/null +++ b/server/features/completion.py @@ -0,0 +1,16 @@ +from pygls.lsp import CompletionList, CompletionItem + +def trigger_characters(): + return ['t', 's', 'f', 'c'] + +def process_completions(): + return CompletionList( + is_incomplete=False, + items=[ + CompletionItem(label='test'), + CompletionItem(label='screen'), + CompletionItem(label='flow'), + CompletionItem(label='target'), + CompletionItem(label='common'), + ] + ) diff --git a/server/features/validate.py b/server/features/validate.py new file mode 100644 index 0000000..a5c6c44 --- /dev/null +++ b/server/features/validate.py @@ -0,0 +1,49 @@ +from typing import List, Union + +from textx.exceptions import TextXError +from textx import metamodel_for_language +from pyflies.exceptions import PyFliesException +from pygls.lsp.types import Diagnostic, Range, Position + +def validate( + model: str +) -> List[PyFliesException]: + """Validates given model. + + NOTE: For now returned list will contain maximum one error, since textX does not + have built-in error recovery mechanism. + + Args: + model: model + file_path: A path to the `model` file + project_root: A path to the root directory where to look for other models + Returns: + A list of textX errors or empty list if model is valid + Raises: + None + + """ + errors = [] + try: + mm = metamodel_for_language('pyflies') + mm.model_from_str(model) + # except PyFliesException as e: + # errors.append(e) + except TextXError as err: + # errors.append(err) + + msg = err.message + col = err.col + line = err.line + + d = Diagnostic( + range=Range( + start=Position(line=line - 1, character=col - 1), + end=Position(line=line - 1, character=col) + ), + message=msg, + source='pyFlies LS' + ) + + errors.append(d) + return errors diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py new file mode 100644 index 0000000..0d1f5e5 --- /dev/null +++ b/server/pyflies_ls.py @@ -0,0 +1,66 @@ +import time +from pygls.lsp.methods import (COMPLETION, DEFINITION, TEXT_DOCUMENT_DID_CHANGE, + CODE_ACTION, TEXT_DOCUMENT_DID_OPEN) + +# from pygls.capabilities import COMPLETION, TEXT_DOCUMENT_DID_CHANGE +from typing import List, Optional, Union +from pygls.server import LanguageServer +from pygls.lsp import (CompletionItem, CompletionList, CompletionOptions, + CompletionParams, DefinitionParams, DidChangeTextDocumentParams, + DidOpenTextDocumentParams, CodeActionOptions, CodeActionKind, + CodeActionParams, CodeAction, Command) +from .features.validate import validate +from .features.completion import process_completions, trigger_characters +from .features.code_actions import process_quick_fix +from .util import load_document + +COUNT_DOWN_START_IN_SECONDS = 12 +COUNT_DOWN_SLEEP_IN_SECONDS = 1 + +class PyfliesLanguageServer(LanguageServer): + + def __init__(self): + super().__init__() + +def _validate(ls, params): + ls.show_message_log('Validating pyflies...') + + # text_doc = ls.workspace.get_document(params.text_document.uri) + + source = load_document(ls, params.text_document.uri) + diagnostics = validate(source) if source else [] + + ls.publish_diagnostics(params.text_document.uri, diagnostics) + + +pyflies_server = PyfliesLanguageServer() + +@pyflies_server.feature(COMPLETION, CompletionOptions(trigger_characters=trigger_characters())) +def completions(params: CompletionParams): + """Returns completion items.""" + + return process_completions() + + +@pyflies_server.feature(TEXT_DOCUMENT_DID_CHANGE) +def did_change(ls, params: DidChangeTextDocumentParams): + """Text document did change notification.""" + _validate(ls, params) + +@pyflies_server.feature(TEXT_DOCUMENT_DID_OPEN) +async def did_open(ls, params: DidOpenTextDocumentParams): + """Text document did open notification.""" + ls.show_message('Text Document Did Open') + _validate(ls, params) + +@pyflies_server.feature(DEFINITION) +def definitions(params: DefinitionParams): + pass + +@pyflies_server.feature(CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor])) +def code_actions(ls, params: CodeActionParams) -> Optional[List[Union[Command, CodeAction]]]: + diag = params.context.diagnostics + if diag.__len__() == 0: + return None + else: + return process_quick_fix(ls, diag[0], params.text_document) diff --git a/server/util.py b/server/util.py new file mode 100644 index 0000000..617f52b --- /dev/null +++ b/server/util.py @@ -0,0 +1,4 @@ +def load_document(ls, uri): + text_doc = ls.workspace.get_document(uri) + + return text_doc.source From e3ed8380c06f9e65cd1e10e5e05b505b205eab2a Mon Sep 17 00:00:00 2001 From: Dejan Date: Sun, 19 Sep 2021 10:50:35 +0200 Subject: [PATCH 03/12] Setup logic for Quick fix for reference typos --- package.json | 6 ------ server/features/code_actions.py | 24 +++++++++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 0085ef6..6a96638 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,6 @@ ], "main": "./out/extension.js", "contributes": { - "commands": [ - { - "command": "countDownBlocking", - "title": "Count down 10 seconds [Blocking]" - } - ], "grammars": [ { "language": "pyflies", diff --git a/server/features/code_actions.py b/server/features/code_actions.py index 60946cb..b3fb86b 100644 --- a/server/features/code_actions.py +++ b/server/features/code_actions.py @@ -5,11 +5,13 @@ def process_quick_fix(ls, diag, text_document): if diag.message.__contains__('Unknown object'): obj = diag.message.split('"')[1] + obj_type = diag.message.split('"')[3] - print(obj) diag.range.end.character = diag.range.start.character + obj.__len__() - new_text = determine_fix(obj, load_document(ls, text_document.uri), diag.message) + new_text = determine_fix(obj, obj_type, load_document(ls, text_document.uri)) + if new_text == None: return None + fix = CodeAction(title='Fix typo', kind=CodeActionKind.QuickFix, edit=WorkspaceEdit( @@ -18,6 +20,18 @@ def process_quick_fix(ls, diag, text_document): return [fix] -def determine_fix(obj, source, error_message): - # TODO: setup logic - return 'Real' \ No newline at end of file +def find(lst, str): + return [i for i, x in enumerate(lst) if x.lower() == str.lower()] + +def determine_fix(obj, obj_type, source): + obj_type = obj_type.replace('Type', '') + source_list = source.split() + indexes = find(source_list, obj_type) + + possibilities = [] + for ind in indexes: + possibilities.append(source_list[ind+1]) + + print(possibilities) + matches = difflib.get_close_matches(obj, possibilities) + return matches[0] if matches.__len__() > 0 else None From 40d8477ea50d450ce4f2b2c51a4ec13d59b470e5 Mon Sep 17 00:00:00 2001 From: Dejan Date: Sat, 25 Sep 2021 15:44:32 +0200 Subject: [PATCH 04/12] Go to definition feature implementation --- server/features/definitions.py | 16 ++++++++++++++++ server/pyflies_ls.py | 17 ++++++++++------- server/util.py | 11 +++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 server/features/definitions.py diff --git a/server/features/definitions.py b/server/features/definitions.py new file mode 100644 index 0000000..1608df4 --- /dev/null +++ b/server/features/definitions.py @@ -0,0 +1,16 @@ +from pygls.lsp.types.basic_structures import Location, Position +from textx import metamodel_for_language +from pygls.lsp import Location, Range, Position + +def pos_to_range(position): + return Range( + start=Position(line=position[0]-1, character=position[1]), + end=Position(line=position[0]-1, character=position[1]+1) + ) + +def resolve_definition(model, param_name, uri): + mm = metamodel_for_language('pyflies') + m = mm.model_from_str(model) + + defs = [x for x in m.routine_types if x.name == param_name] + return Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(defs[0]._tx_position))) diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py index 0d1f5e5..4a57918 100644 --- a/server/pyflies_ls.py +++ b/server/pyflies_ls.py @@ -8,11 +8,13 @@ from pygls.lsp import (CompletionItem, CompletionList, CompletionOptions, CompletionParams, DefinitionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, CodeActionOptions, CodeActionKind, - CodeActionParams, CodeAction, Command) + CodeActionParams, CodeAction, Command, Location) +from pygls.workspace import Document from .features.validate import validate from .features.completion import process_completions, trigger_characters from .features.code_actions import process_quick_fix -from .util import load_document +from .features.definitions import resolve_definition +from .util import load_document, get_entire_string_from_index COUNT_DOWN_START_IN_SECONDS = 12 COUNT_DOWN_SLEEP_IN_SECONDS = 1 @@ -23,9 +25,6 @@ def __init__(self): super().__init__() def _validate(ls, params): - ls.show_message_log('Validating pyflies...') - - # text_doc = ls.workspace.get_document(params.text_document.uri) source = load_document(ls, params.text_document.uri) diagnostics = validate(source) if source else [] @@ -54,8 +53,12 @@ async def did_open(ls, params: DidOpenTextDocumentParams): _validate(ls, params) @pyflies_server.feature(DEFINITION) -def definitions(params: DefinitionParams): - pass +def definitions(ls, params: DefinitionParams): + # source = load_document(ls, params.text_document.uri) + text_doc = ls.workspace.get_document(params.text_document.uri) + name = get_entire_string_from_index(text_doc.offset_at_position(params.position), text_doc.source) + defs = resolve_definition(text_doc.source, name, params.text_document.uri) + return [defs] @pyflies_server.feature(CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor])) def code_actions(ls, params: CodeActionParams) -> Optional[List[Union[Command, CodeAction]]]: diff --git a/server/util.py b/server/util.py index 617f52b..599eabf 100644 --- a/server/util.py +++ b/server/util.py @@ -2,3 +2,14 @@ def load_document(ls, uri): text_doc = ls.workspace.get_document(uri) return text_doc.source + +def get_entire_string_from_index(ind, source): + start_ind = ind + while not source[start_ind].isspace(): + start_ind -= 1 + + end_ind = ind + while not source[end_ind].isspace() and not source[end_ind] == '(': + end_ind += 1 + + return source[start_ind+1:end_ind] From 801da51ee28a10b896d7a68a4c13edb4d1ef2a4e Mon Sep 17 00:00:00 2001 From: Dejan Date: Sat, 2 Oct 2021 02:39:42 +0200 Subject: [PATCH 05/12] Completion impelmentation - propose completion snippet based on context. Minor improvements for quick fix and definitions. --- package.json | 6 --- server/features/code_actions.py | 5 +-- server/features/completion.py | 73 ++++++++++++++++++++++++++++----- server/features/definitions.py | 4 +- server/features/validate.py | 30 +++++++------- server/pyflies_ls.py | 9 ++-- server/util.py | 18 +++++++- 7 files changed, 103 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 6a96638..2f63297 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,6 @@ "configuration": "./languages/pyflies.language-configuration.json" } ], - "snippets": [ - { - "language": "pyflies", - "path": "./snippets/pyflies-snippets.json" - } - ], "keybindings": [ { "command": "markdowntable.format", diff --git a/server/features/code_actions.py b/server/features/code_actions.py index b3fb86b..5ab0060 100644 --- a/server/features/code_actions.py +++ b/server/features/code_actions.py @@ -1,6 +1,6 @@ import difflib from pygls.lsp import CodeAction, CodeActionKind, Command, WorkspaceEdit, TextEdit, Range, Position -from ..util import load_document +from ..util import load_document, load_document_source def process_quick_fix(ls, diag, text_document): if diag.message.__contains__('Unknown object'): @@ -9,7 +9,7 @@ def process_quick_fix(ls, diag, text_document): diag.range.end.character = diag.range.start.character + obj.__len__() - new_text = determine_fix(obj, obj_type, load_document(ls, text_document.uri)) + new_text = determine_fix(obj, obj_type, load_document_source(ls, text_document.uri)) if new_text == None: return None fix = CodeAction(title='Fix typo', @@ -32,6 +32,5 @@ def determine_fix(obj, obj_type, source): for ind in indexes: possibilities.append(source_list[ind+1]) - print(possibilities) matches = difflib.get_close_matches(obj, possibilities) return matches[0] if matches.__len__() > 0 else None diff --git a/server/features/completion.py b/server/features/completion.py index a2bd678..567b4f9 100644 --- a/server/features/completion.py +++ b/server/features/completion.py @@ -1,16 +1,69 @@ -from pygls.lsp import CompletionList, CompletionItem +import json +import re + +from pygls.lsp.types.language_features import completion +from textx.exceptions import TextXError, TextXSyntaxError +from textx import metamodel_for_language +from ..util import load_snippets, load_document, get_model_from_source +from .validate import construct_diagnostic, validate +from pygls.lsp import CompletionList, CompletionItem, CompletionParams, CompletionItemKind, InsertTextFormat def trigger_characters(): - return ['t', 's', 'f', 'c'] + return ['t', 's', 'f', 'i', 'l', 'r', 'a', 'k', 'm'] + +def filter_snippets(trigger_character, snippets:json): + if trigger_character is None: + return snippets + else: + snippets_final = {} + for k in snippets.keys(): + if k.startswith(trigger_character): + snippets_final[k] = snippets[k] + return snippets_final + +def check_snippet(snippet, metamodel, model, offset): + + # Replaces dynamic parts of the snippet body with a hardcoded variable name to prevent false syntax errors + snippet_body = snippet['body'].replace('${0}','') + test_body = re.sub('\${([0-9]:[A-Za-z]*|[0-9])}', 'var1', snippet_body) + test_source = model[:offset] + test_body + model[offset:] + + try: + metamodel.model_from_str(test_source) + except TextXError as err: + if err.__class__ == TextXSyntaxError: + return False + + return True + +def resolve_completion_items(server, snippets, position, uri): + + doc = load_document(server, uri) + mm = metamodel_for_language('pyflies') + + offset = doc.offset_at_position(position) + completion_items = [] + + for snippet in snippets.values(): + + if check_snippet(snippet, mm, doc.source, offset) is False: + continue + + completion_items.append(CompletionItem( + label=snippet['prefix'], + kind=CompletionItemKind.Snippet, + insert_text=snippet['body'], + insert_text_format=InsertTextFormat.Snippet, + )) + + return completion_items + +def process_completions(server, params: CompletionParams): + + snippets = filter_snippets(params.context.trigger_character, load_snippets()) + completion_items = resolve_completion_items(server, snippets, params.position, params.text_document.uri) -def process_completions(): return CompletionList( is_incomplete=False, - items=[ - CompletionItem(label='test'), - CompletionItem(label='screen'), - CompletionItem(label='flow'), - CompletionItem(label='target'), - CompletionItem(label='common'), - ] + items = completion_items ) diff --git a/server/features/definitions.py b/server/features/definitions.py index 1608df4..80e1b58 100644 --- a/server/features/definitions.py +++ b/server/features/definitions.py @@ -1,6 +1,7 @@ from pygls.lsp.types.basic_structures import Location, Position from textx import metamodel_for_language from pygls.lsp import Location, Range, Position +from ..util import get_model_from_source def pos_to_range(position): return Range( @@ -9,8 +10,7 @@ def pos_to_range(position): ) def resolve_definition(model, param_name, uri): - mm = metamodel_for_language('pyflies') - m = mm.model_from_str(model) + m = get_model_from_source(model) defs = [x for x in m.routine_types if x.name == param_name] return Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(defs[0]._tx_position))) diff --git a/server/features/validate.py b/server/features/validate.py index a5c6c44..353d1a4 100644 --- a/server/features/validate.py +++ b/server/features/validate.py @@ -5,6 +5,16 @@ from pyflies.exceptions import PyFliesException from pygls.lsp.types import Diagnostic, Range, Position +def construct_diagnostic(msg, col, line): + return Diagnostic( + range=Range( + start=Position(line=line - 1, character=col - 1), + end=Position(line=line - 1, character=col) + ), + message=msg, + source='pyFlies LS' + ) + def validate( model: str ) -> List[PyFliesException]: @@ -27,23 +37,15 @@ def validate( try: mm = metamodel_for_language('pyflies') mm.model_from_str(model) - # except PyFliesException as e: - # errors.append(e) + except PyFliesException as err: + # TODO: How to determine col and line for PyFliesException + errors.append(construct_diagnostic(err.args[0], 1, 1)) except TextXError as err: - # errors.append(err) - msg = err.message col = err.col line = err.line - d = Diagnostic( - range=Range( - start=Position(line=line - 1, character=col - 1), - end=Position(line=line - 1, character=col) - ), - message=msg, - source='pyFlies LS' - ) - - errors.append(d) + errors.append(construct_diagnostic(msg, col, line)) + except Exception as e: + print(e) return errors diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py index 4a57918..2d8c599 100644 --- a/server/pyflies_ls.py +++ b/server/pyflies_ls.py @@ -14,7 +14,7 @@ from .features.completion import process_completions, trigger_characters from .features.code_actions import process_quick_fix from .features.definitions import resolve_definition -from .util import load_document, get_entire_string_from_index +from .util import load_document, get_entire_string_from_index, load_document_source COUNT_DOWN_START_IN_SECONDS = 12 COUNT_DOWN_SLEEP_IN_SECONDS = 1 @@ -26,7 +26,7 @@ def __init__(self): def _validate(ls, params): - source = load_document(ls, params.text_document.uri) + source = load_document_source(ls, params.text_document.uri) diagnostics = validate(source) if source else [] ls.publish_diagnostics(params.text_document.uri, diagnostics) @@ -35,11 +35,10 @@ def _validate(ls, params): pyflies_server = PyfliesLanguageServer() @pyflies_server.feature(COMPLETION, CompletionOptions(trigger_characters=trigger_characters())) -def completions(params: CompletionParams): +def completions(ls, params: CompletionParams): """Returns completion items.""" - return process_completions() - + return process_completions(ls, params) @pyflies_server.feature(TEXT_DOCUMENT_DID_CHANGE) def did_change(ls, params: DidChangeTextDocumentParams): diff --git a/server/util.py b/server/util.py index 599eabf..792c7a5 100644 --- a/server/util.py +++ b/server/util.py @@ -1,7 +1,21 @@ +import json +from textx import metamodel_for_language + def load_document(ls, uri): - text_doc = ls.workspace.get_document(uri) + return ls.workspace.get_document(uri) + +def load_document_source(ls, uri): + return load_document(ls, uri).source + +def get_model_from_source(model): + mm = metamodel_for_language('pyflies') + return mm.model_from_str(model) - return text_doc.source +def load_snippets(): + snippets = {} + with open('snippets/pyflies-snippets.json') as json_file: + snippets = json.load(json_file) + return snippets def get_entire_string_from_index(ind, source): start_ind = ind From b76f56cc43572e5c7af1b329cfd964a558109bce Mon Sep 17 00:00:00 2001 From: Dejan Date: Sun, 30 Jan 2022 19:08:04 +0100 Subject: [PATCH 06/12] Some improvements to Definition feature --- server/features/definitions.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/server/features/definitions.py b/server/features/definitions.py index 80e1b58..2f4d3f0 100644 --- a/server/features/definitions.py +++ b/server/features/definitions.py @@ -1,16 +1,28 @@ +from turtle import screensize +from unittest import case from pygls.lsp.types.basic_structures import Location, Position from textx import metamodel_for_language from pygls.lsp import Location, Range, Position from ..util import get_model_from_source -def pos_to_range(position): +def determine_position_from_type(type): + if type == 'pyflies.ScreenType': + return 'screen '.__len__() + elif type == 'pyflies.TestType': + return 'test '.__len__() + else: + return 1 + +def pos_to_range(position, type, name_len): + char_pos = determine_position_from_type(type) + return Range( - start=Position(line=position[0]-1, character=position[1]), - end=Position(line=position[0]-1, character=position[1]+1) + start=Position(line=position[0]-1, character=char_pos), + end=Position(line=position[0]-1, character=char_pos+name_len) ) def resolve_definition(model, param_name, uri): m = get_model_from_source(model) defs = [x for x in m.routine_types if x.name == param_name] - return Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(defs[0]._tx_position))) + return Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(defs[0]._tx_position), defs[0]._tx_fqn, param_name.__len__())) From 1f9ff7feda6bbff805fc8c5e1395c2d23d73ea4f Mon Sep 17 00:00:00 2001 From: Dejan Date: Sun, 30 Jan 2022 19:10:02 +0100 Subject: [PATCH 07/12] Initial working implementation for Reference feature --- server/features/references.py | 51 +++++++++++++++++++++++++++++++++++ server/pyflies_ls.py | 12 +++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 server/features/references.py diff --git a/server/features/references.py b/server/features/references.py new file mode 100644 index 0000000..7a843e9 --- /dev/null +++ b/server/features/references.py @@ -0,0 +1,51 @@ +import imp +import re +from textx import get_children +from pygls.lsp.types.basic_structures import Location, Range, Position +from ..util import get_model_from_source + +def pos_to_range(position, name_len): + return Range( + start=Position(line=position[0]-1, character=position[1]-1), + end=Position(line=position[0]-1, character=position[1]-1+name_len) + ) + +def is_routine(name, m): + return ([x for x in m.routine_types if x.name == name]) != 0 + +def is_var(name, m): + return ([x for x in m.vars if x.name == name]) != 0 + +def routine_or_var(param_name, m): + routines = [x for x in m.routine_types if x.name == param_name] + vars = [x for x in m.vars if x.name == param_name] + if len(routines) == 0 and len(vars) == 0: + return False + else: + return True + +def fetch_vars(m, name): + return [x._tx_position for x in get_children(lambda x: hasattr(x, 'name') and x.name == name, m)] + +def fetch_routines(m, name): + from_root = [x._tx_position for x in get_children(lambda x: hasattr(x, 'name') and x.name == name, m)] + from_flow = [x._tx_position for x in get_children(lambda x: x.__class__.__name__ in ['Test', 'Screen'] and x.type.name == name, m)] + from_root.extend(from_flow) + return from_root + +def resolve_references(model, param_name, uri): + m = get_model_from_source(model) + + occurences = [] + if is_routine(param_name, m): + occurences.extend(fetch_routines(m, param_name)) + elif is_var(param_name, m): + occurences.extend(fetch_vars(m, param_name)) + else: + return None + + refs = [] + for o in occurences: + refs.append(Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(o), param_name.__len__()))) + + return refs diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py index 2d8c599..95e7f25 100644 --- a/server/pyflies_ls.py +++ b/server/pyflies_ls.py @@ -1,6 +1,6 @@ import time from pygls.lsp.methods import (COMPLETION, DEFINITION, TEXT_DOCUMENT_DID_CHANGE, - CODE_ACTION, TEXT_DOCUMENT_DID_OPEN) + CODE_ACTION, TEXT_DOCUMENT_DID_OPEN, REFERENCES) # from pygls.capabilities import COMPLETION, TEXT_DOCUMENT_DID_CHANGE from typing import List, Optional, Union @@ -8,12 +8,13 @@ from pygls.lsp import (CompletionItem, CompletionList, CompletionOptions, CompletionParams, DefinitionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, CodeActionOptions, CodeActionKind, - CodeActionParams, CodeAction, Command, Location) + CodeActionParams, CodeAction, Command, ReferenceParams) from pygls.workspace import Document from .features.validate import validate from .features.completion import process_completions, trigger_characters from .features.code_actions import process_quick_fix from .features.definitions import resolve_definition +from .features.references import resolve_references from .util import load_document, get_entire_string_from_index, load_document_source COUNT_DOWN_START_IN_SECONDS = 12 @@ -59,6 +60,13 @@ def definitions(ls, params: DefinitionParams): defs = resolve_definition(text_doc.source, name, params.text_document.uri) return [defs] +@pyflies_server.feature(REFERENCES) +def references(ls, params: ReferenceParams): + text_doc = ls.workspace.get_document(params.text_document.uri) + name = get_entire_string_from_index(text_doc.offset_at_position(params.position), text_doc.source) + refs = resolve_references(text_doc.source, name, params.text_document.uri) + return refs + @pyflies_server.feature(CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor])) def code_actions(ls, params: CodeActionParams) -> Optional[List[Union[Command, CodeAction]]]: diag = params.context.diagnostics From c3b7c936f8328a5ddfc3d61a9ce18adf2e9a9a2e Mon Sep 17 00:00:00 2001 From: Dejan Date: Fri, 4 Feb 2022 14:56:30 +0100 Subject: [PATCH 08/12] Context completion optimized. Additional fixes. Readme and setup added. --- .vscode/launch.json | 17 +++++++ server/README.md | 12 +++++ server/__main__.py | 7 +++ server/features/code_actions.py | 38 ++++++++++----- server/features/completion.py | 68 ++++++++++++++------------- server/features/definitions.py | 28 +++++++---- server/features/references.py | 41 ++++++++++++---- server/features/validate.py | 24 +++++----- server/pyflies_ls.py | 83 +++++++++++++++++++++++++-------- server/setup.py | 47 +++++++++++++++++++ server/util.py | 19 ++++++-- 11 files changed, 287 insertions(+), 97 deletions(-) create mode 100644 server/README.md create mode 100644 server/setup.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 2613a57..334e537 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,6 +34,19 @@ "PYTHONPATH": "${workspaceFolder}" } }, + { + "name": "Launch Server - Context Completion", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--tcp", "--context-completion"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, { "name": "Extension Tests", "type": "extensionHost", @@ -52,6 +65,10 @@ { "name": "Server + Client", "configurations": ["Launch Server", "Launch Client"] + }, + { + "name": "Server (Context Completion) + Client", + "configurations": ["Launch Server - Context Completion", "Launch Client"] } ] } diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..370abb7 --- /dev/null +++ b/server/README.md @@ -0,0 +1,12 @@ +# vscode-pyflies Language Server + +vscode-pyflies Language Server contains all logic for providing features like: + +- _Code completion_ +- _Error reporting_ +- _Quick fix for typos_ +- _Find all refernces_ +- _Go to definition_ + +The server uses _[pygls](https://github.com/openlawlibrary/pygls)_ to expose all functionalities over +_[Language Server Protocol](https://microsoft.github.io/language-server-protocol/specification)_. \ No newline at end of file diff --git a/server/__main__.py b/server/__main__.py index 2379b49..6903afe 100644 --- a/server/__main__.py +++ b/server/__main__.py @@ -25,6 +25,10 @@ def add_arguments(parser): "--port", type=int, default=2087, help="Bind to this port" ) + parser.add_argument( + "--context-completion", action="store_true", + help="Use contextualized completion (might affect performance)" + ) def main(): @@ -32,6 +36,9 @@ def main(): add_arguments(parser) args = parser.parse_args() + if args.context_completion: + pyflies_server.set_context_completion() + if args.tcp: pyflies_server.start_tcp(args.host, args.port) elif args.ws: diff --git a/server/features/code_actions.py b/server/features/code_actions.py index 5ab0060..8731815 100644 --- a/server/features/code_actions.py +++ b/server/features/code_actions.py @@ -1,36 +1,50 @@ import difflib -from pygls.lsp import CodeAction, CodeActionKind, Command, WorkspaceEdit, TextEdit, Range, Position +from pygls.lsp import ( + CodeAction, + CodeActionKind, + WorkspaceEdit, + TextEdit +) from ..util import load_document, load_document_source + def process_quick_fix(ls, diag, text_document): - if diag.message.__contains__('Unknown object'): + if diag.message.__contains__("Unknown object"): obj = diag.message.split('"')[1] obj_type = diag.message.split('"')[3] diag.range.end.character = diag.range.start.character + obj.__len__() - new_text = determine_fix(obj, obj_type, load_document_source(ls, text_document.uri)) - if new_text == None: return None - - fix = CodeAction(title='Fix typo', - kind=CodeActionKind.QuickFix, - edit=WorkspaceEdit( - changes={text_document.uri : [TextEdit(range=diag.range, new_text=new_text)] } - )) + new_text = determine_fix( + obj, obj_type, load_document_source(ls, text_document.uri) + ) + if new_text == None: + return None + + fix = CodeAction( + title="Fix typo", + kind=CodeActionKind.QuickFix, + edit=WorkspaceEdit( + changes={ + text_document.uri: [TextEdit(range=diag.range, new_text=new_text)] + } + ), + ) return [fix] def find(lst, str): return [i for i, x in enumerate(lst) if x.lower() == str.lower()] + def determine_fix(obj, obj_type, source): - obj_type = obj_type.replace('Type', '') + obj_type = obj_type.replace("Type", "") source_list = source.split() indexes = find(source_list, obj_type) possibilities = [] for ind in indexes: - possibilities.append(source_list[ind+1]) + possibilities.append(source_list[ind + 1]) matches = difflib.get_close_matches(obj, possibilities) return matches[0] if matches.__len__() > 0 else None diff --git a/server/features/completion.py b/server/features/completion.py index 567b4f9..3b24bc9 100644 --- a/server/features/completion.py +++ b/server/features/completion.py @@ -6,27 +6,30 @@ from textx import metamodel_for_language from ..util import load_snippets, load_document, get_model_from_source from .validate import construct_diagnostic, validate -from pygls.lsp import CompletionList, CompletionItem, CompletionParams, CompletionItemKind, InsertTextFormat +from pygls.lsp import ( + CompletionList, + CompletionItem, + CompletionParams, + CompletionItemKind, + InsertTextFormat, +) -def trigger_characters(): - return ['t', 's', 'f', 'i', 'l', 'r', 'a', 'k', 'm'] -def filter_snippets(trigger_character, snippets:json): - if trigger_character is None: - return snippets - else: - snippets_final = {} - for k in snippets.keys(): - if k.startswith(trigger_character): - snippets_final[k] = snippets[k] - return snippets_final +def filter_snippets(doc, position, snippets: json): + trigger_character = doc.lines[position.line][position.character - 1] + snippets_final = {} + for k in snippets.keys(): + if k.startswith(trigger_character): + snippets_final[k] = snippets[k] + return snippets_final + def check_snippet(snippet, metamodel, model, offset): # Replaces dynamic parts of the snippet body with a hardcoded variable name to prevent false syntax errors - snippet_body = snippet['body'].replace('${0}','') - test_body = re.sub('\${([0-9]:[A-Za-z]*|[0-9])}', 'var1', snippet_body) - test_source = model[:offset] + test_body + model[offset:] + snippet_body = snippet["body"].replace("${0}", "") + test_body = re.sub("\${([0-9]:[A-Za-z]*|[0-9])}", "var1", snippet_body) + test_source = model[: offset - 1] + test_body + model[offset:] try: metamodel.model_from_str(test_source) @@ -36,34 +39,35 @@ def check_snippet(snippet, metamodel, model, offset): return True -def resolve_completion_items(server, snippets, position, uri): - doc = load_document(server, uri) - mm = metamodel_for_language('pyflies') +def resolve_completion_items(server, snippets, position, doc): + mm = metamodel_for_language("pyflies") offset = doc.offset_at_position(position) completion_items = [] for snippet in snippets.values(): - if check_snippet(snippet, mm, doc.source, offset) is False: - continue + if server.context_completion: + if check_snippet(snippet, mm, doc.source, offset) is False: + continue - completion_items.append(CompletionItem( - label=snippet['prefix'], - kind=CompletionItemKind.Snippet, - insert_text=snippet['body'], - insert_text_format=InsertTextFormat.Snippet, - )) + completion_items.append( + CompletionItem( + label=snippet["prefix"], + kind=CompletionItemKind.Snippet, + insert_text=snippet["body"], + insert_text_format=InsertTextFormat.Snippet, + ) + ) return completion_items + def process_completions(server, params: CompletionParams): - snippets = filter_snippets(params.context.trigger_character, load_snippets()) - completion_items = resolve_completion_items(server, snippets, params.position, params.text_document.uri) + doc = load_document(server, params.text_document.uri) + snippets = filter_snippets(doc, params.position, load_snippets()) + completion_items = resolve_completion_items(server, snippets, params.position, doc) - return CompletionList( - is_incomplete=False, - items = completion_items - ) + return CompletionList(is_incomplete=False, items=completion_items) diff --git a/server/features/definitions.py b/server/features/definitions.py index 2f4d3f0..59eace7 100644 --- a/server/features/definitions.py +++ b/server/features/definitions.py @@ -5,24 +5,36 @@ from pygls.lsp import Location, Range, Position from ..util import get_model_from_source + def determine_position_from_type(type): - if type == 'pyflies.ScreenType': - return 'screen '.__len__() - elif type == 'pyflies.TestType': - return 'test '.__len__() + if type == "pyflies.ScreenType": + return "screen ".__len__() + elif type == "pyflies.TestType": + return "test ".__len__() else: - return 1 + return 0 + def pos_to_range(position, type, name_len): char_pos = determine_position_from_type(type) return Range( - start=Position(line=position[0]-1, character=char_pos), - end=Position(line=position[0]-1, character=char_pos+name_len) + start=Position(line=position[0] - 1, character=char_pos), + end=Position(line=position[0] - 1, character=char_pos + name_len), ) + def resolve_definition(model, param_name, uri): m = get_model_from_source(model) defs = [x for x in m.routine_types if x.name == param_name] - return Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(defs[0]._tx_position), defs[0]._tx_fqn, param_name.__len__())) + var_defs = [x for x in m.vars if x.name == param_name] + defs.extend(var_defs) + return Location( + uri=uri, + range=pos_to_range( + m._tx_parser.pos_to_linecol(defs[0]._tx_position), + defs[0]._tx_fqn, + param_name.__len__(), + ), + ) diff --git a/server/features/references.py b/server/features/references.py index 7a843e9..6787604 100644 --- a/server/features/references.py +++ b/server/features/references.py @@ -6,16 +6,18 @@ def pos_to_range(position, name_len): return Range( - start=Position(line=position[0]-1, character=position[1]-1), - end=Position(line=position[0]-1, character=position[1]-1+name_len) + start=Position(line=position[0] - 1, character=position[1] - 1), + end=Position(line=position[0] - 1, character=position[1] - 1 + name_len), ) def is_routine(name, m): return ([x for x in m.routine_types if x.name == name]) != 0 + def is_var(name, m): return ([x for x in m.vars if x.name == name]) != 0 + def routine_or_var(param_name, m): routines = [x for x in m.routine_types if x.name == param_name] vars = [x for x in m.vars if x.name == param_name] @@ -24,18 +26,34 @@ def routine_or_var(param_name, m): else: return True + def fetch_vars(m, name): - return [x._tx_position for x in get_children(lambda x: hasattr(x, 'name') and x.name == name, m)] + return [ + x._tx_position + for x in get_children(lambda x: hasattr(x, "name") and x.name == name, m) + ] + def fetch_routines(m, name): - from_root = [x._tx_position for x in get_children(lambda x: hasattr(x, 'name') and x.name == name, m)] - from_flow = [x._tx_position for x in get_children(lambda x: x.__class__.__name__ in ['Test', 'Screen'] and x.type.name == name, m)] + from_root = [ + x._tx_position + for x in get_children(lambda x: hasattr(x, "name") and x.name == name, m) + ] + from_flow = [ + x._tx_position + for x in get_children( + lambda x: x.__class__.__name__ in ["Test", "Screen"] + and x.type.name == name, + m, + ) + ] from_root.extend(from_flow) return from_root + def resolve_references(model, param_name, uri): m = get_model_from_source(model) - + occurences = [] if is_routine(param_name, m): occurences.extend(fetch_routines(m, param_name)) @@ -46,6 +64,13 @@ def resolve_references(model, param_name, uri): refs = [] for o in occurences: - refs.append(Location(uri=uri, range=pos_to_range(m._tx_parser.pos_to_linecol(o), param_name.__len__()))) - + refs.append( + Location( + uri=uri, + range=pos_to_range( + m._tx_parser.pos_to_linecol(o), param_name.__len__() + ), + ) + ) + return refs diff --git a/server/features/validate.py b/server/features/validate.py index 353d1a4..17fe89c 100644 --- a/server/features/validate.py +++ b/server/features/validate.py @@ -5,19 +5,19 @@ from pyflies.exceptions import PyFliesException from pygls.lsp.types import Diagnostic, Range, Position + def construct_diagnostic(msg, col, line): return Diagnostic( - range=Range( - start=Position(line=line - 1, character=col - 1), - end=Position(line=line - 1, character=col) - ), - message=msg, - source='pyFlies LS' - ) - -def validate( - model: str -) -> List[PyFliesException]: + range=Range( + start=Position(line=line - 1, character=col - 1), + end=Position(line=line - 1, character=col), + ), + message=msg, + source="pyFlies LS", + ) + + +def validate(model: str) -> List[PyFliesException]: """Validates given model. NOTE: For now returned list will contain maximum one error, since textX does not @@ -35,7 +35,7 @@ def validate( """ errors = [] try: - mm = metamodel_for_language('pyflies') + mm = metamodel_for_language("pyflies") mm.model_from_str(model) except PyFliesException as err: # TODO: How to determine col and line for PyFliesException diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py index 95e7f25..9dc2809 100644 --- a/server/pyflies_ls.py +++ b/server/pyflies_ls.py @@ -1,17 +1,28 @@ -import time -from pygls.lsp.methods import (COMPLETION, DEFINITION, TEXT_DOCUMENT_DID_CHANGE, - CODE_ACTION, TEXT_DOCUMENT_DID_OPEN, REFERENCES) - -# from pygls.capabilities import COMPLETION, TEXT_DOCUMENT_DID_CHANGE +from pickle import TRUE from typing import List, Optional, Union from pygls.server import LanguageServer -from pygls.lsp import (CompletionItem, CompletionList, CompletionOptions, - CompletionParams, DefinitionParams, DidChangeTextDocumentParams, - DidOpenTextDocumentParams, CodeActionOptions, CodeActionKind, - CodeActionParams, CodeAction, Command, ReferenceParams) -from pygls.workspace import Document +from pygls.lsp.methods import ( + COMPLETION, + DEFINITION, + TEXT_DOCUMENT_DID_CHANGE, + CODE_ACTION, + TEXT_DOCUMENT_DID_OPEN, + REFERENCES, +) +from pygls.lsp import ( + CompletionParams, + DefinitionParams, + DidChangeTextDocumentParams, + DidOpenTextDocumentParams, + CodeActionOptions, + CodeActionKind, + CodeActionParams, + CodeAction, + Command, + ReferenceParams, +) from .features.validate import validate -from .features.completion import process_completions, trigger_characters +from .features.completion import process_completions from .features.code_actions import process_quick_fix from .features.definitions import resolve_definition from .features.references import resolve_references @@ -20,10 +31,22 @@ COUNT_DOWN_START_IN_SECONDS = 12 COUNT_DOWN_SLEEP_IN_SECONDS = 1 + class PyfliesLanguageServer(LanguageServer): + """ + Represents a language server for pyFlies language. + + This is the entry point for all communications with the client(s). + It uses custom pygls LanguageServer. + """ def __init__(self): super().__init__() + self.context_completion = False + + def set_context_completion(self): + self.context_completion = True + def _validate(ls, params): @@ -35,40 +58,60 @@ def _validate(ls, params): pyflies_server = PyfliesLanguageServer() -@pyflies_server.feature(COMPLETION, CompletionOptions(trigger_characters=trigger_characters())) + +@pyflies_server.feature( + COMPLETION +) def completions(ls, params: CompletionParams): """Returns completion items.""" return process_completions(ls, params) + @pyflies_server.feature(TEXT_DOCUMENT_DID_CHANGE) def did_change(ls, params: DidChangeTextDocumentParams): - """Text document did change notification.""" + """ + Text document did change notification. + The method calls validation on the document in which the change occured. + """ _validate(ls, params) + @pyflies_server.feature(TEXT_DOCUMENT_DID_OPEN) async def did_open(ls, params: DidOpenTextDocumentParams): - """Text document did open notification.""" - ls.show_message('Text Document Did Open') + """ + Text document did open notification. + The method calls validation on the opened document. + """ _validate(ls, params) + @pyflies_server.feature(DEFINITION) def definitions(ls, params: DefinitionParams): - # source = load_document(ls, params.text_document.uri) text_doc = ls.workspace.get_document(params.text_document.uri) - name = get_entire_string_from_index(text_doc.offset_at_position(params.position), text_doc.source) + name = get_entire_string_from_index( + text_doc.offset_at_position(params.position), text_doc.source + ) defs = resolve_definition(text_doc.source, name, params.text_document.uri) return [defs] + @pyflies_server.feature(REFERENCES) def references(ls, params: ReferenceParams): text_doc = ls.workspace.get_document(params.text_document.uri) - name = get_entire_string_from_index(text_doc.offset_at_position(params.position), text_doc.source) + name = get_entire_string_from_index( + text_doc.offset_at_position(params.position), text_doc.source + ) refs = resolve_references(text_doc.source, name, params.text_document.uri) return refs -@pyflies_server.feature(CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor])) -def code_actions(ls, params: CodeActionParams) -> Optional[List[Union[Command, CodeAction]]]: + +@pyflies_server.feature( + CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor]) +) +def code_actions( + ls, params: CodeActionParams +) -> Optional[List[Union[Command, CodeAction]]]: diag = params.context.diagnostics if diag.__len__() == 0: return None diff --git a/server/setup.py b/server/setup.py new file mode 100644 index 0000000..5ae07db --- /dev/null +++ b/server/setup.py @@ -0,0 +1,47 @@ +# flake8: noqa +import codecs +import os + +from setuptools import find_packages, setup + +PACKAGE_NAME = "pyflies-ls-server" +VERSION = "0.0.1" +AUTHOR = "Dejan Šorgić" +AUTHOR_EMAIL = "dejans1224@gmail.com" +DESCRIPTION = "a language server for pyFlies language" +KEYWORDS = "pyFlies DSL python domain specific languages language server protocol pygls" +URL = "https://github.com/pyflies/vscode-pyflies/tree/main/server" + +packages = find_packages() + +print("packages:", packages) + +README = codecs.open( + os.path.join(os.path.dirname(__file__), "README.md"), "r", encoding="utf-8" +).read() + +setup( + name=PACKAGE_NAME, + version=VERSION, + description=DESCRIPTION, + long_description=README, + long_description_content_type="text/markdown", + url=URL, + author=AUTHOR, + author_email=AUTHOR_EMAIL, + keywords=KEYWORDS, + packages=packages, + include_package_data=True, + install_requires=["pygls==0.11.2", "textx", "pyflies"], + classifiers=[ + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Topic :: Software Development :: Libraries :: Python Modules", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ] +) diff --git a/server/util.py b/server/util.py index 792c7a5..51f8e5e 100644 --- a/server/util.py +++ b/server/util.py @@ -1,29 +1,38 @@ import json from textx import metamodel_for_language +from pygls.lsp.types.basic_structures import Range, Position + def load_document(ls, uri): return ls.workspace.get_document(uri) + def load_document_source(ls, uri): return load_document(ls, uri).source + def get_model_from_source(model): - mm = metamodel_for_language('pyflies') + mm = metamodel_for_language("pyflies") return mm.model_from_str(model) + def load_snippets(): snippets = {} - with open('snippets/pyflies-snippets.json') as json_file: + with open("snippets/pyflies-snippets.json") as json_file: snippets = json.load(json_file) return snippets + def get_entire_string_from_index(ind, source): start_ind = ind - while not source[start_ind].isspace(): + if source[start_ind] == ' ': + start_ind -= 1 + + while source[start_ind].isalnum() or source[start_ind] == '_': start_ind -= 1 end_ind = ind - while not source[end_ind].isspace() and not source[end_ind] == '(': + while source[end_ind].isalnum() or source[end_ind] == '_': end_ind += 1 - return source[start_ind+1:end_ind] + return source[start_ind + 1 : end_ind] From 9f2ae864d5d91fb8d6601a9f344c1b0f80d7bf85 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 8 Sep 2022 12:38:25 +0200 Subject: [PATCH 09/12] Moved LS code to different repo. Added setup for LS --- .gitignore | 1 + .vscode/launch.json | 28 ++++---- server/README.md | 12 ---- server/__init__.py | 0 server/__main__.py | 51 -------------- server/features/__init__.py | 0 server/features/code_actions.py | 50 -------------- server/features/completion.py | 73 -------------------- server/features/definitions.py | 40 ----------- server/features/references.py | 76 -------------------- server/features/validate.py | 51 -------------- server/pyflies_ls.py | 119 -------------------------------- server/setup.py | 47 ------------- server/util.py | 38 ---------- src/extension.ts | 16 ++++- src/setup.ts | 105 ++++++++++++++++++++++++++++ 16 files changed, 136 insertions(+), 571 deletions(-) delete mode 100644 server/README.md delete mode 100644 server/__init__.py delete mode 100644 server/__main__.py delete mode 100644 server/features/__init__.py delete mode 100644 server/features/code_actions.py delete mode 100644 server/features/completion.py delete mode 100644 server/features/definitions.py delete mode 100644 server/features/references.py delete mode 100644 server/features/validate.py delete mode 100644 server/pyflies_ls.py delete mode 100644 server/setup.py delete mode 100644 server/util.py create mode 100644 src/setup.ts diff --git a/.gitignore b/.gitignore index 30652c4..a64e335 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ node_modules *.vsix code-extensions/ env/ +pyfliesls/ __pycache__/ *.log diff --git a/.vscode/launch.json b/.vscode/launch.json index 334e537..cf8f51e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,19 +34,6 @@ "PYTHONPATH": "${workspaceFolder}" } }, - { - "name": "Launch Server - Context Completion", - "type": "python", - "request": "launch", - "module": "server", - "args": ["--tcp", "--context-completion"], - "justMyCode": false, - "python": "${command:python.interpreterPath}", - "cwd": "${workspaceFolder}", - "env": { - "PYTHONPATH": "${workspaceFolder}" - } - }, { "name": "Extension Tests", "type": "extensionHost", @@ -59,6 +46,21 @@ "${workspaceFolder}/out/test/**/*.js" ], "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Launch Client prod", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "VSCODE_DEBUG_MODE": "false" + } } ], "compounds": [ diff --git a/server/README.md b/server/README.md deleted file mode 100644 index 370abb7..0000000 --- a/server/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# vscode-pyflies Language Server - -vscode-pyflies Language Server contains all logic for providing features like: - -- _Code completion_ -- _Error reporting_ -- _Quick fix for typos_ -- _Find all refernces_ -- _Go to definition_ - -The server uses _[pygls](https://github.com/openlawlibrary/pygls)_ to expose all functionalities over -_[Language Server Protocol](https://microsoft.github.io/language-server-protocol/specification)_. \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/__main__.py b/server/__main__.py deleted file mode 100644 index 6903afe..0000000 --- a/server/__main__.py +++ /dev/null @@ -1,51 +0,0 @@ -import argparse -import logging - -from .pyflies_ls import pyflies_server - -logging.basicConfig(filename="pygls.log", level=logging.DEBUG, filemode="w") - - -def add_arguments(parser): - parser.description = "PyFlies language server" - - parser.add_argument( - "--tcp", action="store_true", - help="Use TCP server" - ) - parser.add_argument( - "--ws", action="store_true", - help="Use WebSocket server" - ) - parser.add_argument( - "--host", default="127.0.0.1", - help="Bind to this address" - ) - parser.add_argument( - "--port", type=int, default=2087, - help="Bind to this port" - ) - parser.add_argument( - "--context-completion", action="store_true", - help="Use contextualized completion (might affect performance)" - ) - - -def main(): - parser = argparse.ArgumentParser() - add_arguments(parser) - args = parser.parse_args() - - if args.context_completion: - pyflies_server.set_context_completion() - - if args.tcp: - pyflies_server.start_tcp(args.host, args.port) - elif args.ws: - pyflies_server.start_ws(args.host, args.port) - else: - pyflies_server.start_io() - - -if __name__ == '__main__': - main() diff --git a/server/features/__init__.py b/server/features/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/features/code_actions.py b/server/features/code_actions.py deleted file mode 100644 index 8731815..0000000 --- a/server/features/code_actions.py +++ /dev/null @@ -1,50 +0,0 @@ -import difflib -from pygls.lsp import ( - CodeAction, - CodeActionKind, - WorkspaceEdit, - TextEdit -) -from ..util import load_document, load_document_source - - -def process_quick_fix(ls, diag, text_document): - if diag.message.__contains__("Unknown object"): - obj = diag.message.split('"')[1] - obj_type = diag.message.split('"')[3] - - diag.range.end.character = diag.range.start.character + obj.__len__() - - new_text = determine_fix( - obj, obj_type, load_document_source(ls, text_document.uri) - ) - if new_text == None: - return None - - fix = CodeAction( - title="Fix typo", - kind=CodeActionKind.QuickFix, - edit=WorkspaceEdit( - changes={ - text_document.uri: [TextEdit(range=diag.range, new_text=new_text)] - } - ), - ) - return [fix] - - -def find(lst, str): - return [i for i, x in enumerate(lst) if x.lower() == str.lower()] - - -def determine_fix(obj, obj_type, source): - obj_type = obj_type.replace("Type", "") - source_list = source.split() - indexes = find(source_list, obj_type) - - possibilities = [] - for ind in indexes: - possibilities.append(source_list[ind + 1]) - - matches = difflib.get_close_matches(obj, possibilities) - return matches[0] if matches.__len__() > 0 else None diff --git a/server/features/completion.py b/server/features/completion.py deleted file mode 100644 index 3b24bc9..0000000 --- a/server/features/completion.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -import re - -from pygls.lsp.types.language_features import completion -from textx.exceptions import TextXError, TextXSyntaxError -from textx import metamodel_for_language -from ..util import load_snippets, load_document, get_model_from_source -from .validate import construct_diagnostic, validate -from pygls.lsp import ( - CompletionList, - CompletionItem, - CompletionParams, - CompletionItemKind, - InsertTextFormat, -) - - -def filter_snippets(doc, position, snippets: json): - trigger_character = doc.lines[position.line][position.character - 1] - snippets_final = {} - for k in snippets.keys(): - if k.startswith(trigger_character): - snippets_final[k] = snippets[k] - return snippets_final - - -def check_snippet(snippet, metamodel, model, offset): - - # Replaces dynamic parts of the snippet body with a hardcoded variable name to prevent false syntax errors - snippet_body = snippet["body"].replace("${0}", "") - test_body = re.sub("\${([0-9]:[A-Za-z]*|[0-9])}", "var1", snippet_body) - test_source = model[: offset - 1] + test_body + model[offset:] - - try: - metamodel.model_from_str(test_source) - except TextXError as err: - if err.__class__ == TextXSyntaxError: - return False - - return True - - -def resolve_completion_items(server, snippets, position, doc): - - mm = metamodel_for_language("pyflies") - offset = doc.offset_at_position(position) - completion_items = [] - - for snippet in snippets.values(): - - if server.context_completion: - if check_snippet(snippet, mm, doc.source, offset) is False: - continue - - completion_items.append( - CompletionItem( - label=snippet["prefix"], - kind=CompletionItemKind.Snippet, - insert_text=snippet["body"], - insert_text_format=InsertTextFormat.Snippet, - ) - ) - - return completion_items - - -def process_completions(server, params: CompletionParams): - - doc = load_document(server, params.text_document.uri) - snippets = filter_snippets(doc, params.position, load_snippets()) - completion_items = resolve_completion_items(server, snippets, params.position, doc) - - return CompletionList(is_incomplete=False, items=completion_items) diff --git a/server/features/definitions.py b/server/features/definitions.py deleted file mode 100644 index 59eace7..0000000 --- a/server/features/definitions.py +++ /dev/null @@ -1,40 +0,0 @@ -from turtle import screensize -from unittest import case -from pygls.lsp.types.basic_structures import Location, Position -from textx import metamodel_for_language -from pygls.lsp import Location, Range, Position -from ..util import get_model_from_source - - -def determine_position_from_type(type): - if type == "pyflies.ScreenType": - return "screen ".__len__() - elif type == "pyflies.TestType": - return "test ".__len__() - else: - return 0 - - -def pos_to_range(position, type, name_len): - char_pos = determine_position_from_type(type) - - return Range( - start=Position(line=position[0] - 1, character=char_pos), - end=Position(line=position[0] - 1, character=char_pos + name_len), - ) - - -def resolve_definition(model, param_name, uri): - m = get_model_from_source(model) - - defs = [x for x in m.routine_types if x.name == param_name] - var_defs = [x for x in m.vars if x.name == param_name] - defs.extend(var_defs) - return Location( - uri=uri, - range=pos_to_range( - m._tx_parser.pos_to_linecol(defs[0]._tx_position), - defs[0]._tx_fqn, - param_name.__len__(), - ), - ) diff --git a/server/features/references.py b/server/features/references.py deleted file mode 100644 index 6787604..0000000 --- a/server/features/references.py +++ /dev/null @@ -1,76 +0,0 @@ -import imp -import re -from textx import get_children -from pygls.lsp.types.basic_structures import Location, Range, Position -from ..util import get_model_from_source - -def pos_to_range(position, name_len): - return Range( - start=Position(line=position[0] - 1, character=position[1] - 1), - end=Position(line=position[0] - 1, character=position[1] - 1 + name_len), - ) - -def is_routine(name, m): - return ([x for x in m.routine_types if x.name == name]) != 0 - - -def is_var(name, m): - return ([x for x in m.vars if x.name == name]) != 0 - - -def routine_or_var(param_name, m): - routines = [x for x in m.routine_types if x.name == param_name] - vars = [x for x in m.vars if x.name == param_name] - if len(routines) == 0 and len(vars) == 0: - return False - else: - return True - - -def fetch_vars(m, name): - return [ - x._tx_position - for x in get_children(lambda x: hasattr(x, "name") and x.name == name, m) - ] - - -def fetch_routines(m, name): - from_root = [ - x._tx_position - for x in get_children(lambda x: hasattr(x, "name") and x.name == name, m) - ] - from_flow = [ - x._tx_position - for x in get_children( - lambda x: x.__class__.__name__ in ["Test", "Screen"] - and x.type.name == name, - m, - ) - ] - from_root.extend(from_flow) - return from_root - - -def resolve_references(model, param_name, uri): - m = get_model_from_source(model) - - occurences = [] - if is_routine(param_name, m): - occurences.extend(fetch_routines(m, param_name)) - elif is_var(param_name, m): - occurences.extend(fetch_vars(m, param_name)) - else: - return None - - refs = [] - for o in occurences: - refs.append( - Location( - uri=uri, - range=pos_to_range( - m._tx_parser.pos_to_linecol(o), param_name.__len__() - ), - ) - ) - - return refs diff --git a/server/features/validate.py b/server/features/validate.py deleted file mode 100644 index 17fe89c..0000000 --- a/server/features/validate.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import List, Union - -from textx.exceptions import TextXError -from textx import metamodel_for_language -from pyflies.exceptions import PyFliesException -from pygls.lsp.types import Diagnostic, Range, Position - - -def construct_diagnostic(msg, col, line): - return Diagnostic( - range=Range( - start=Position(line=line - 1, character=col - 1), - end=Position(line=line - 1, character=col), - ), - message=msg, - source="pyFlies LS", - ) - - -def validate(model: str) -> List[PyFliesException]: - """Validates given model. - - NOTE: For now returned list will contain maximum one error, since textX does not - have built-in error recovery mechanism. - - Args: - model: model - file_path: A path to the `model` file - project_root: A path to the root directory where to look for other models - Returns: - A list of textX errors or empty list if model is valid - Raises: - None - - """ - errors = [] - try: - mm = metamodel_for_language("pyflies") - mm.model_from_str(model) - except PyFliesException as err: - # TODO: How to determine col and line for PyFliesException - errors.append(construct_diagnostic(err.args[0], 1, 1)) - except TextXError as err: - msg = err.message - col = err.col - line = err.line - - errors.append(construct_diagnostic(msg, col, line)) - except Exception as e: - print(e) - return errors diff --git a/server/pyflies_ls.py b/server/pyflies_ls.py deleted file mode 100644 index 9dc2809..0000000 --- a/server/pyflies_ls.py +++ /dev/null @@ -1,119 +0,0 @@ -from pickle import TRUE -from typing import List, Optional, Union -from pygls.server import LanguageServer -from pygls.lsp.methods import ( - COMPLETION, - DEFINITION, - TEXT_DOCUMENT_DID_CHANGE, - CODE_ACTION, - TEXT_DOCUMENT_DID_OPEN, - REFERENCES, -) -from pygls.lsp import ( - CompletionParams, - DefinitionParams, - DidChangeTextDocumentParams, - DidOpenTextDocumentParams, - CodeActionOptions, - CodeActionKind, - CodeActionParams, - CodeAction, - Command, - ReferenceParams, -) -from .features.validate import validate -from .features.completion import process_completions -from .features.code_actions import process_quick_fix -from .features.definitions import resolve_definition -from .features.references import resolve_references -from .util import load_document, get_entire_string_from_index, load_document_source - -COUNT_DOWN_START_IN_SECONDS = 12 -COUNT_DOWN_SLEEP_IN_SECONDS = 1 - - -class PyfliesLanguageServer(LanguageServer): - """ - Represents a language server for pyFlies language. - - This is the entry point for all communications with the client(s). - It uses custom pygls LanguageServer. - """ - - def __init__(self): - super().__init__() - self.context_completion = False - - def set_context_completion(self): - self.context_completion = True - - -def _validate(ls, params): - - source = load_document_source(ls, params.text_document.uri) - diagnostics = validate(source) if source else [] - - ls.publish_diagnostics(params.text_document.uri, diagnostics) - - -pyflies_server = PyfliesLanguageServer() - - -@pyflies_server.feature( - COMPLETION -) -def completions(ls, params: CompletionParams): - """Returns completion items.""" - - return process_completions(ls, params) - - -@pyflies_server.feature(TEXT_DOCUMENT_DID_CHANGE) -def did_change(ls, params: DidChangeTextDocumentParams): - """ - Text document did change notification. - The method calls validation on the document in which the change occured. - """ - _validate(ls, params) - - -@pyflies_server.feature(TEXT_DOCUMENT_DID_OPEN) -async def did_open(ls, params: DidOpenTextDocumentParams): - """ - Text document did open notification. - The method calls validation on the opened document. - """ - _validate(ls, params) - - -@pyflies_server.feature(DEFINITION) -def definitions(ls, params: DefinitionParams): - text_doc = ls.workspace.get_document(params.text_document.uri) - name = get_entire_string_from_index( - text_doc.offset_at_position(params.position), text_doc.source - ) - defs = resolve_definition(text_doc.source, name, params.text_document.uri) - return [defs] - - -@pyflies_server.feature(REFERENCES) -def references(ls, params: ReferenceParams): - text_doc = ls.workspace.get_document(params.text_document.uri) - name = get_entire_string_from_index( - text_doc.offset_at_position(params.position), text_doc.source - ) - refs = resolve_references(text_doc.source, name, params.text_document.uri) - return refs - - -@pyflies_server.feature( - CODE_ACTION, CodeActionOptions(code_action_kinds=[CodeActionKind.Refactor]) -) -def code_actions( - ls, params: CodeActionParams -) -> Optional[List[Union[Command, CodeAction]]]: - diag = params.context.diagnostics - if diag.__len__() == 0: - return None - else: - return process_quick_fix(ls, diag[0], params.text_document) diff --git a/server/setup.py b/server/setup.py deleted file mode 100644 index 5ae07db..0000000 --- a/server/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -# flake8: noqa -import codecs -import os - -from setuptools import find_packages, setup - -PACKAGE_NAME = "pyflies-ls-server" -VERSION = "0.0.1" -AUTHOR = "Dejan Šorgić" -AUTHOR_EMAIL = "dejans1224@gmail.com" -DESCRIPTION = "a language server for pyFlies language" -KEYWORDS = "pyFlies DSL python domain specific languages language server protocol pygls" -URL = "https://github.com/pyflies/vscode-pyflies/tree/main/server" - -packages = find_packages() - -print("packages:", packages) - -README = codecs.open( - os.path.join(os.path.dirname(__file__), "README.md"), "r", encoding="utf-8" -).read() - -setup( - name=PACKAGE_NAME, - version=VERSION, - description=DESCRIPTION, - long_description=README, - long_description_content_type="text/markdown", - url=URL, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - keywords=KEYWORDS, - packages=packages, - include_package_data=True, - install_requires=["pygls==0.11.2", "textx", "pyflies"], - classifiers=[ - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "Topic :: Software Development :: Libraries :: Python Modules", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - ] -) diff --git a/server/util.py b/server/util.py deleted file mode 100644 index 51f8e5e..0000000 --- a/server/util.py +++ /dev/null @@ -1,38 +0,0 @@ -import json -from textx import metamodel_for_language -from pygls.lsp.types.basic_structures import Range, Position - - -def load_document(ls, uri): - return ls.workspace.get_document(uri) - - -def load_document_source(ls, uri): - return load_document(ls, uri).source - - -def get_model_from_source(model): - mm = metamodel_for_language("pyflies") - return mm.model_from_str(model) - - -def load_snippets(): - snippets = {} - with open("snippets/pyflies-snippets.json") as json_file: - snippets = json.load(json_file) - return snippets - - -def get_entire_string_from_index(ind, source): - start_ind = ind - if source[start_ind] == ' ': - start_ind -= 1 - - while source[start_ind].isalnum() or source[start_ind] == '_': - start_ind -= 1 - - end_ind = ind - while source[end_ind].isalnum() or source[end_ind] == '_': - end_ind += 1 - - return source[start_ind + 1 : end_ind] diff --git a/src/extension.ts b/src/extension.ts index b25a8be..18dd395 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import * as vscode from 'vscode'; import * as net from "net"; import * as path from "path"; +import { installLSWithProgress } from './setup'; import { LanguageClient, @@ -63,7 +64,20 @@ function startLangServer( // this method is called when your extension is activated // your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { + if (isStartedInDebugMode()) { + // Development - Run the server manually + client = startLangServerTCP(parseInt(process.env.SERVER_PORT || "2087")); + } else { + // Production - Client is going to run the server (for use within `.vsix` package) + try { + const python = await installLSWithProgress(context); + client = startLangServer(python, ["-m", "pyflies-ls"], context.extensionPath); + } catch (err:any) { + vscode.window.showErrorMessage(err.toString()); + } + } + if (isStartedInDebugMode()){ // Development - Run the server manually client = startLangServerTCP(2087); diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..d458780 --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,105 @@ +import { execSync } from "child_process"; +import { existsSync, readdirSync } from "fs"; +import { join } from "path"; +import { ExtensionContext, ProgressLocation, window, workspace} from "vscode"; + +export const IS_WIN = process.platform === "win32"; + +function createVirtualEnvironment(python: string, name: string, cwd: string): string { + const path = join(cwd, name); + if (!existsSync(path)) { + const createVenvCmd = `${python} -m venv ${name}`; + execSync(createVenvCmd, { cwd }); + } + return path; +} + +function getPython(): string { + return workspace.getConfiguration("python").get("pythonPath", getPythonCrossPlatform()); +} + +function getPythonCrossPlatform(): string { + return IS_WIN ? "python" : "python3"; +} + +function getPythonFromVenvPath(venvPath: string): string { + return IS_WIN ? join(venvPath, "Scripts", "python") : join(venvPath, "bin", "python"); +} + +function getPythonVersion(python: string): number[] | undefined { + const getPythonVersionCmd = `${python} --version`; + const version = execSync(getPythonVersionCmd).toString(); + return version.match(RegExp(/\d/g))?.map((v) => Number.parseInt(v)); +} + +function getVenvPackageVersion(python: string, name: string): boolean { + const listPipPackagesCmd = `${python} -m pip show ${name}`; + + try { + const packageInfo = execSync(listPipPackagesCmd).toString(); + if (packageInfo === undefined){ + return false; + } + return true; + } catch (err) { + return false; + } +} + +function installServer(python: string){ + execSync(`${python} -m pip install pyflies-ls`); +} + +function* installLS(context: ExtensionContext): IterableIterator { + yield "Installing textX language server"; + + // Get python interpreter + const python = getPython(); + // Check python version (3.5+ is required) + const [major, minor] = getPythonVersion(python) || [3, 6]; + if (major !== 3 || minor < 5) { + throw new Error("Python 3.5+ is required!"); + } + + // Create virtual environment + const venv = createVirtualEnvironment(python, "pyfliesls", context.extensionPath); + yield `Virtual Environment created in: ${venv}`; + + // Install source from wheels + const venvPython = getPythonFromVenvPath(venv); + const wheelsPath = join(context.extensionPath, "wheels"); + installServer(venvPython); + // installAllWheelsFromDirectory(venvPython, wheelsPath); + yield `Successfully installed pyflies-LS.`; +} + +export async function installLSWithProgress(context: ExtensionContext): Promise { + // Check if LS is already installed + const venvPython = getPythonFromVenvPath(join(context.extensionPath, "pyfliesls")); + + if (getVenvPackageVersion(venvPython, "pyflies-ls")) { + return Promise.resolve(venvPython); + } + + // Install with progress bar + return await window.withProgress({ + location: ProgressLocation.Window, + }, (progress): Promise => { + return new Promise((resolve, reject) => { + + // Catch generator errors + try { + // Go through installation steps + for (const step of installLS(context)) { + progress.report({ message: step }); + } + + } catch (err) { + reject(err); + } + + resolve(venvPython); + }); + }); + +} From 33716a1276abbad994e950781bfdce8c2c9f65da Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 21 Sep 2022 07:47:00 +0200 Subject: [PATCH 10/12] Minor updates in settings files --- .vscode/launch.json | 90 +++++++++++++++++++++---------------------- .vscode/settings.json | 2 +- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index cf8f51e..19c0169 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,25 +3,25 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ + "version": "0.2.0", + "configurations": [ - { - "name": "Launch Client", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}", - "env": { + { + "name": "Launch Client", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { "VSCODE_DEBUG_MODE": "true" } - }, - { + }, + { "name": "Launch Server", "type": "python", "request": "launch", @@ -34,41 +34,39 @@ "PYTHONPATH": "${workspaceFolder}" } }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Launch Client prod", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}", - "env": { - "VSCODE_DEBUG_MODE": "false" + { + "name": "Launch Server - Context Completion", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--tcp", "--context-completion"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" } - } - ], - "compounds": [ + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ], + "compounds": [ { "name": "Server + Client", "configurations": ["Launch Server", "Launch Client"] }, - { + { "name": "Server (Context Completion) + Client", "configurations": ["Launch Server - Context Completion", "Launch Client"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index da582b3..605e8d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,5 @@ // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "python.pythonPath": "./env/Scripts/python" + "python.pythonPath": "./pyfliesls/Scripts/python" } \ No newline at end of file From 0e955d91b84c3f03c998f87a207d8653082eec78 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 21 Sep 2022 18:04:59 +0200 Subject: [PATCH 11/12] Remove duplicated code --- src/extension.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 18dd395..98cc847 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,14 +2,12 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; import * as net from "net"; -import * as path from "path"; import { installLSWithProgress } from './setup'; import { LanguageClient, LanguageClientOptions, ServerOptions, - TransportKind } from 'vscode-languageclient/node'; let client: LanguageClient; @@ -78,23 +76,6 @@ export async function activate(context: vscode.ExtensionContext) { } } - if (isStartedInDebugMode()){ - // Development - Run the server manually - client = startLangServerTCP(2087); - } else { - // Production - Client is going to run the server (for use within `.vsix` package) - const cwd = path.join(__dirname, "..", ".."); - const pythonPath = vscode.workspace - .getConfiguration("python") - .get("pythonPath"); - - if (!pythonPath) { - throw new Error("`python.pythonPath` is not set"); - } - - client = startLangServer(pythonPath, ["-m", "server"], cwd); - } - context.subscriptions.push(client.start()); var mdTableExtension = vscode.extensions.getExtension('TakumiI.markdowntable'); From 9b1a3d77a36d4c68a7f9b8ab7f29fb3ed7e5e77b Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 19 Sep 2023 09:39:02 +0200 Subject: [PATCH 12/12] Setup fixes --- src/extension.ts | 2 +- src/setup.ts | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 98cc847..ecc10be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -70,7 +70,7 @@ export async function activate(context: vscode.ExtensionContext) { // Production - Client is going to run the server (for use within `.vsix` package) try { const python = await installLSWithProgress(context); - client = startLangServer(python, ["-m", "pyflies-ls"], context.extensionPath); + client = startLangServer(python, ["-m", "pyflies_ls"], context.extensionPath); } catch (err:any) { vscode.window.showErrorMessage(err.toString()); } diff --git a/src/setup.ts b/src/setup.ts index d458780..d2943e3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -55,10 +55,10 @@ function* installLS(context: ExtensionContext): IterableIterator { // Get python interpreter const python = getPython(); - // Check python version (3.5+ is required) - const [major, minor] = getPythonVersion(python) || [3, 6]; - if (major !== 3 || minor < 5) { - throw new Error("Python 3.5+ is required!"); + // Check python version (3.7+ is required) + const [major, minor] = getPythonVersion(python) || [3, 7]; + if (major !== 3 || minor < 7) { + throw new Error("Python 3.7+ is required!"); } // Create virtual environment @@ -67,9 +67,7 @@ function* installLS(context: ExtensionContext): IterableIterator { // Install source from wheels const venvPython = getPythonFromVenvPath(venv); - const wheelsPath = join(context.extensionPath, "wheels"); installServer(venvPython); - // installAllWheelsFromDirectory(venvPython, wheelsPath); yield `Successfully installed pyflies-LS.`; }