diff --git a/service/azservice/__main__.py b/service/azservice/__main__.py index b4a34e1..58ad32e 100644 --- a/service/azservice/__main__.py +++ b/service/azservice/__main__.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- from __future__ import print_function +from concurrent.futures import ThreadPoolExecutor from sys import stdin, stdout, stderr import json import time @@ -16,6 +17,8 @@ from azservice.tooling import GLOBAL_ARGUMENTS, initialize, load_command_table, get_help, get_current_subscription, get_configured_defaults, get_defaults, is_required, run_argument_value_completer, get_arguments, load_arguments, arguments_loaded +from azservice.recommend_tooling import request_recommend_service, init as recommend_init + NO_AZ_PREFIX_COMPLETION_ENABLED = True # Adds proposals without 'az' as prefix to trigger, 'az' is then inserted as part of the completion. AUTOMATIC_SNIPPETS_ENABLED = True # Adds snippet proposals derived from the command table TWO_SEGMENTS_COMPLETION_ENABLED = False # Adds 'webapp create', 'appservice plan', etc. as proposals. @@ -29,6 +32,7 @@ def get_group_index(command_table): index = { '': [], '-': [] } + # build subgroup tree for command in command_table.values(): parts = command.name.split() len_parts = len(parts) @@ -116,14 +120,18 @@ def add_command_documentation(completion, command): def get_completions(group_index, command_table, snippets, query, verbose=False): if 'argument' in query: + # input commands finished and begin to input argument return get_argument_value_completions(command_table, query, verbose) if 'subcommand' not in query: + # provide subgroup and required arguments completions return get_snippet_completions(command_table, snippets) + get_prefix_command_completions(group_index, command_table) + [AZ_COMPLETION] command_name = query['subcommand'] if command_name in command_table: + # input commands finished return get_argument_name_completions(command_table, query) + \ get_global_argument_name_completions(query) if command_name in group_index: + # input commands not finished return get_command_completions(group_index, command_table, command_name) if verbose: print('Subcommand not found ({})'.format(command_name), file=stderr) return [] @@ -304,12 +312,16 @@ def get_options(options): option if isinstance(option, str) else option.target if hasattr(option, 'target') else None - for option in options ] if option ] + for option in options ] if option ] + +def log(message): + print(message, file=stderr) def main(): - timings = False + timings = True start = time.time() initialize() + recommend_init() if timings: print('initialize {} s'.format(time.time() - start), file=stderr) start = time.time() @@ -327,22 +339,29 @@ def main(): def enqueue_output(input, queue): for line in iter(input.readline, b''): queue.put(line) + log('put to queue: size - {}'.format(queue.qsize())) + # create a thread to put requests in a queue queue = Queue() thread = Thread(target=enqueue_output, args=(stdin, queue)) thread.daemon = True thread.start() + recommend_executor = ThreadPoolExecutor(max_workers=1) + bkg_start = time.time() keep_loading = True - while True: - + while True: + # loading all arguments is time consuming + # load 10 arguments per loop until all are loaded finished if keep_loading: keep_loading = load_arguments(command_table, 10) if not keep_loading and timings: print('load_arguments {} s'.format(time.time() - bkg_start), file=stderr) try: + # non-blocking way to get request from the queue if keep loading line = queue.get_nowait() if keep_loading else queue.get() + log('line: {}'.format(line)) except Empty: continue @@ -353,11 +372,17 @@ def enqueue_output(input, queue): response_data = get_status() if timings: print('get_status {} s'.format(time.time() - start), file=stderr) elif request['data'].get('request') == 'hover': + # display highlight response_data = get_hover_text(group_index, command_table, request['data']['command']) if timings: print('get_hover_text {} s'.format(time.time() - start), file=stderr) + elif request['data'].get('request') == 'recommendation': + recommend_executor.submit(request_recommend_service, (request)) + if timings: print('submit {} s'.format(time.time() - start), file=stderr) + continue else: response_data = get_completions(group_index, command_table, snippets, request['data'], True) if timings: print('get_completions {} s'.format(time.time() - start), file=stderr) + response = { 'sequence': request['sequence'], 'data': response_data diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py new file mode 100644 index 0000000..9fe5d24 --- /dev/null +++ b/service/azservice/recommend_tooling.py @@ -0,0 +1,133 @@ +import hashlib +import json +import time +from enum import Enum +from sys import stdout, stderr + +from azure.cli.core import __version__ as version +from azure.cli.core import telemetry +from azure.cli.core.azclierror import RecommendationError + +from azservice.tooling2 import cli_ctx + +class RecommendType(int, Enum): + All = 1 + Solution = 2 + Command = 3 + Scenario = 4 + +cli_ctx = None +def init(): + global cli_ctx + from azservice.tooling2 import cli_ctx + +def request_recommend_service(request): + start = time.time() + + command_list = request['data']['commandList'] + recommends = [] + from azure.cli.core.azclierror import RecommendationError + try: + recommends = get_recommends(command_list) + except RecommendationError as e: + print(e.error_msg, file=stderr) + + response = { + 'sequence': request['sequence'], + 'data': recommends + } + output = json.dumps(response) + stdout.write(output + '\n') + stdout.flush() + stderr.flush() + print('request_recommend_service {} s'.format(time.time() - start), file=stderr) + + +def get_recommends(command_list): + print('cli_ctx - {}'.format(cli_ctx), file=stderr) + api_recommends = get_recommends_from_api(command_list, cli_ctx.config.getint('next', 'num_limit', fallback=5)) + recommends = get_scenarios_info(api_recommends) + return recommends + + +def get_recommends_from_api(command_list, top_num=5): + """query next command from web api""" + import requests + url = "https://cli-recommendation.azurewebsites.net/api/RecommendationService" + debug_url = "http://localhost:7071/api/RecommendationService" + + user_id = telemetry._get_user_azure_id() # pylint: disable=protected-access + hashed_user_id = hashlib.sha256(user_id.encode('utf-8')).hexdigest() + + type = RecommendType.All + + payload = { + "command_list": command_list, + "type": type, + "top_num": top_num, + 'cli_version': version, + 'user_id': hashed_user_id + } + + correlation_id = telemetry._session.correlation_id + subscription_id = telemetry._get_azure_subscription_id() + if telemetry.is_telemetry_enabled(): + if correlation_id: + payload['correlation_id'] = correlation_id + if subscription_id: + payload['subscription_id'] = subscription_id + + print('request body - {}'.format(payload), file=stderr) + + try: + request_body = json.dumps(payload) + start = time.time() + response = requests.post(url, request_body, timeout=2) + print('request recommendation service {} s'.format(time.time() - start), file=stderr) + response.raise_for_status() + except requests.ConnectionError as e: + raise RecommendationError(f'Network Error: {e}') from e + except requests.exceptions.HTTPError as e: + raise RecommendationError(f'{e}') from e + except requests.RequestException as e: + raise RecommendationError(f'Request Error: {e}') from e + + recommends = [] + if 'data' in response.json(): + recommends = response.json()['data'] + + return recommends + + +def get_scenarios_info(recommends): + scenarios = get_scenarios(recommends) or [] + scenarios_info = [] + print('scenarios size - {}'.format(len(scenarios)), file=stderr) + for idx, s in enumerate(scenarios): + scenarios_info.append(get_info_of_one_scenario(s, idx)) + return scenarios_info + + +def get_info_of_one_scenario(s, index): + idx_display = f'[{index + 1}]' + scenario_desc = f'{s["scenario"]}' + command_size = f'{len(s["nextCommandSet"])} Commands' + description = f'{idx_display} {scenario_desc} ({command_size})' + + next_command_set = [] + for next_command in s['nextCommandSet']: + command_info = { + 'reason': next_command['reason'], + 'example': next_command['example'] + } + next_command_set.append(command_info) + + return { + 'description': description, + 'executeIndex': s['executeIndex'], + 'nextCommandSet': next_command_set + } + + +def get_scenarios(recommends): + return [rec for rec in recommends if rec['type'] == RecommendType.Scenario] \ No newline at end of file diff --git a/service/azservice/tooling.py b/service/azservice/tooling.py index f240b55..a993905 100644 --- a/service/azservice/tooling.py +++ b/service/azservice/tooling.py @@ -9,4 +9,4 @@ if LooseVersion(__version__) < LooseVersion('2.0.24'): from azservice.tooling1 import GLOBAL_ARGUMENTS, initialize, load_command_table, get_help, get_current_subscription, get_configured_defaults, get_defaults, is_required, run_argument_value_completer, get_arguments, load_arguments, arguments_loaded else: - from azservice.tooling2 import GLOBAL_ARGUMENTS, initialize, load_command_table, get_help, get_current_subscription, get_configured_defaults, get_defaults, is_required, run_argument_value_completer, get_arguments, load_arguments, arguments_loaded + from azservice.tooling2 import GLOBAL_ARGUMENTS, initialize, load_command_table, get_help, get_current_subscription, get_configured_defaults, get_defaults, is_required, run_argument_value_completer, get_arguments, load_arguments, arguments_loaded \ No newline at end of file diff --git a/service/azservice/tooling2.py b/service/azservice/tooling2.py index 8a985bf..d66f8b2 100644 --- a/service/azservice/tooling2.py +++ b/service/azservice/tooling2.py @@ -156,13 +156,13 @@ def run_argument_value_completer(command, argument, cli_arguments): args = _to_argument_object(command, cli_arguments) _add_defaults(command, args) return argument.completer(prefix='', action=None, parsed_args=args) - except TypeError: + except Exception: try: return argument.completer(prefix='') - except TypeError: + except Exception: try: return argument.completer() - except TypeError: + except Exception: return None diff --git a/service/start.py b/service/start.py index f3db864..55fa33f 100644 --- a/service/start.py +++ b/service/start.py @@ -2,5 +2,6 @@ import sys sys.path.insert(0, os.path.dirname(__file__)) +print(sys.executable, file=sys.stderr) -import azservice.__main__ \ No newline at end of file +import azservice.__main__ \ No newline at end of file diff --git a/src/azService.ts b/src/azService.ts index eaf0a64..41568a3 100644 --- a/src/azService.ts +++ b/src/azService.ts @@ -91,7 +91,7 @@ export class AzService { }, onCancel); } - private async send(data: T, onCancel?: (handle: () => void) => void): Promise { + async send(data: T, onCancel?: (handle: () => void) => void): Promise { const process = await this.getProcess(); return new Promise((resolve, reject) => { if (onCancel) { @@ -117,9 +117,12 @@ export class AzService { private async getProcess(): Promise { if (this.process) { + console.log("process exists already"); return this.process; } return this.process = (async () => { + console.log("begin to create process"); + const { stdout } = await exec('az --version'); let version = ( /azure-cli\s+\(([^)]+)\)/m.exec(stdout) @@ -127,6 +130,8 @@ export class AzService { || [] )[1]; if (version) { + console.log("version: " + version); + const r = /[^-][a-z]/ig; if (r.exec(version)) { version = version.substr(0, r.lastIndex - 1) + '-' + version.substr(r.lastIndex - 1); @@ -136,6 +141,7 @@ export class AzService { throw 'wrongVersion'; } const pythonLocation = (/^Python location '([^']*)'/m.exec(stdout) || [])[1]; + console.log('pythonLocation: ' + pythonLocation) const processOptions = await this.getSpawnProcessOptions(); return this.spawn(pythonLocation, processOptions); })().catch(err => { diff --git a/src/extension.ts b/src/extension.ts index 94ebe40..558dbeb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as jmespath from 'jmespath'; -import { HoverProvider, Hover, SnippetString, StatusBarAlignment, StatusBarItem, ExtensionContext, TextDocument, TextDocumentChangeEvent, Disposable, TextEditor, Selection, languages, commands, Range, ViewColumn, Position, CancellationToken, ProviderResult, CompletionItem, CompletionList, CompletionItemKind, CompletionItemProvider, window, workspace, env, Uri, WorkspaceEdit, } from 'vscode'; +import { HoverProvider, Hover, SnippetString, StatusBarAlignment, StatusBarItem, ExtensionContext, TextDocument, TextDocumentChangeEvent, Disposable, TextEditor, Selection, languages, commands, Range, ViewColumn, Position, CancellationToken, ProviderResult, CompletionItem, CompletionList, CompletionItemKind, CompletionItemProvider, CompletionContext, CompletionTriggerKind, window, workspace, env, Uri, WorkspaceEdit } from 'vscode'; import * as process from "process"; import { AzService, CompletionKind, Arguments, Status } from './azService'; @@ -11,15 +11,23 @@ import { parse, findNode } from './parser'; import { exec } from './utils'; import * as spinner from 'elegant-spinner'; +import { RecommendParser } from './recommend/parser'; +import { RecommendService, Recommendation } from './recommend/RecommendService'; + export function activate(context: ExtensionContext) { const azService = new AzService(azNotFound); - context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(azService), ' ')); + const recommendService = new RecommendService(azService); + context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(azService, recommendService), ' ')); + context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzRecommendationProvider(recommendService), '\n')); context.subscriptions.push(languages.registerHoverProvider('azcli', new AzHoverProvider(azService))); const status = new StatusBarInfo(azService); context.subscriptions.push(status); context.subscriptions.push(new RunLineInTerminal()); context.subscriptions.push(new RunLineInEditor(status)); context.subscriptions.push(commands.registerCommand('ms-azurecli.installAzureCLI', installAzureCLI)); + // context.subscriptions.push(commands.registerCommand('ms-azurecli.setCurrentRecommends', RecommendService.setCurrentRecommends)); + // context.subscriptions.push(commands.registerCommand('ms-azurecli.initCurrentRecommends', RecommendService.initCurrentRecommends)); + // context.subscriptions.push(commands.registerCommand('ms-azurecli.postProcessOfRecommend', RecommendService.postProcessOfRecommend)); } const completionKinds: Record = { @@ -32,18 +40,22 @@ const completionKinds: Record = { class AzCompletionItemProvider implements CompletionItemProvider { - constructor(private azService: AzService) { + constructor(private azService: AzService, private recommendService: RecommendService) { } provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + console.log('trigger-----------------'); + const line = document.lineAt(position).text; const parsed = parse(line); const start = parsed.subcommand[0]; if (start && start.offset + start.length < position.character && start.text !== 'az') { return; } + // find keyword not input completely const node = findNode(parsed, position.character - 1); if (node && node.kind === 'comment') { + // would be comment later input, ignore return; } // TODO: Use the above instead of parsing again. @@ -58,6 +70,16 @@ class AzCompletionItemProvider implements CompletionItemProvider { const argument = (/\s(--?[^\s]+)\s+[^-\s]*$/.exec(upToCursor) || [])[1]; const prefix = (/(^|\s)([^\s]*)$/.exec(upToCursor) || [])[2]; const lead = /^-*/.exec(prefix)![0]; + if (argument != null && RecommendService.isReadyToRequestService(position.line)) { + // ready to request recommendation service + const { commandListJson: commandListJson } = RecommendParser.parseLines(document, position); + RecommendService.setLine(position.line); + this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + .then(recommendations => { + console.log('setNextScenarios recommendations'); + RecommendService.setScenarios(recommendations); + }); + } return this.azService.getCompletions(subcommand[0] === 'az' ? { subcommand: subcommand.slice(1).join(' '), argument, arguments: args } : {}, token.onCancellationRequested) .then(completions => completions.map(({ name, kind, detail, documentation, snippet, sortText }) => { const item = new CompletionItem(name, completionKinds[kind]); @@ -99,6 +121,121 @@ class AzCompletionItemProvider implements CompletionItemProvider { } } +class AzRecommendationProvider implements CompletionItemProvider { + + constructor(private recommendService: RecommendService) { + } + + provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { + if (context.triggerKind != CompletionTriggerKind.TriggerCharacter && document.lineAt(position.line).text.trim().length == 0) { + return; + } + console.log('trigger recommendation: line'); + + // const { executedCommand: executedCommand, commandListJson: commandListJson } = RecommendParser.parseLines(document, position); + // const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends() + // if (currentRecommends == null) { + // console.log('provideCompletionItems triggered ...'); + // return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + // .then(nextScenarios => nextScenarios.map(({ description, executeIndex, nextCommandSet }) => { + // const item = new CompletionItem(description, CompletionItemKind.Unit); + // item.insertText = ''; + // item.command = { + // title: 'set current recommends', + // command: 'ms-azurecli.setCurrentRecommends', + // arguments: [{ description, executeIndex, nextCommandSet }] + // }; + // return item; + // })); + // } + + const { commandListJson: commandListJson } = RecommendParser.parseLines(document, position); + const scenarios: Recommendation[] | null = RecommendService.popScenarios(); + if (scenarios == null) { + console.log('provideCompletionItems triggered ...'); + return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + .then(nextScenarios => nextScenarios.map((scenario) => { + return this.processItem(scenario); + })); + } + + const items: CompletionItem[] = []; + for (let scenarioIndex = 0; scenarioIndex < scenarios.length; scenarioIndex++) { + items.push(this.processItem(scenarios[scenarioIndex])); + } + return items; + + + // const items: CompletionItem[] = [] + // RecommendService.preprocessRecommend(executedCommand); + // for (let index = 0; index < currentRecommends.nextCommandSet.length; index++) { + // const nextCommand = currentRecommends.nextCommandSet[index] + // const label = (!nextCommand.isExecuted ? `[${index + 1}] ` : '[executed] ') + nextCommand.reason; + // const item = new CompletionItem(label, CompletionItemKind.Function); + // let command = "\n# " + nextCommand.reason + '\n# example: ' + nextCommand.example + '\n' + nextCommand.command; + // let arg_index = 1; + // for (const arg of nextCommand.arguments) { + // command += ' ' + arg + '$' + arg_index + // arg_index += 1 + // } + // item.insertText = new SnippetString(command); + // item.detail = nextCommand.example; + // items.push(item); + // } + // const cleanItem = new CompletionItem('no more commands in this scenario are needed', CompletionItemKind.Event); + // cleanItem.command = { + // title: 'clean current recommends', + // command: 'ms-azurecli.initCurrentRecommends' + // }; + // cleanItem.insertText = '\n' + // items.push(cleanItem) + // return items; + } + + processItem(scenario: Recommendation): CompletionItem { + const item = new CompletionItem(scenario.description, CompletionItemKind.Unit); + + let insertText = ""; + const commentTag = " (comment it since it already exists in script)"; + // let argIndex = 1; + for (let commandIndex = 0; commandIndex < scenario.nextCommandSet.length; commandIndex++) { + // const command = scenario.nextCommandSet[commandIndex]; + // insertText += "\n# " + command.reason + '\n# example: ' + command.example + '\n'; + // if (scenario.executeIndex.indexOf(commandIndex) < 0) { + // insertText += '# '; + // } + // insertText += command.command; + // for (const arg of command.arguments) { + // insertText += ' ' + arg + '$' + argIndex; + // argIndex++; + // } + // insertText += '\n' + + + + const command = scenario.nextCommandSet[commandIndex]; + insertText += "\n# " + command.reason; + if (scenario.executeIndex.indexOf(commandIndex) < 0) { + insertText += commentTag + '\n# '; + } else{ + insertText += '\n'; + } + + insertText += RecommendParser.formatRecommendSample(command.example) + '\n'; + + // insertText += command.command; + // for (const arg of command.arguments) { + // insertText += ' ' + arg + '$' + argIndex; + // argIndex++; + // } + // insertText += '\n' + } + + item.insertText = new SnippetString(insertText); + return item; + } +} + class AzHoverProvider implements HoverProvider { constructor(private azService: AzService) { @@ -157,10 +294,10 @@ class RunLineInEditor { private commandRunningStatusBarItem: StatusBarItem; private statusBarUpdateInterval!: NodeJS.Timer; private statusBarSpinner = spinner(); - private hideStatusBarItemTimeout! : NodeJS.Timeout; - private statusBarItemText : string = ''; + private hideStatusBarItemTimeout!: NodeJS.Timeout; + private statusBarItemText: string = ''; // using backtick (`) as continuation character on Windows, backslash (\) on other systems - private continuationCharacter : string = process.platform === "win32" ? "`" : "\\"; + private continuationCharacter: string = process.platform === "win32" ? "`" : "\\"; constructor(private status: StatusBarInfo) { this.disposables.push(commands.registerTextEditorCommand('ms-azurecli.toggleLiveQuery', editor => this.toggleQuery(editor))); @@ -169,10 +306,10 @@ class RunLineInEditor { this.disposables.push(workspace.onDidChangeTextDocument(event => this.change(event))); this.commandRunningStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); - this.disposables.push(this.commandRunningStatusBarItem); + this.disposables.push(this.commandRunningStatusBarItem); } - private runningCommandCount : number = 0; + private runningCommandCount: number = 0; private run(source: TextEditor) { this.refreshContinuationCharacter(); const command = this.getSelectedCommand(source); @@ -201,8 +338,8 @@ class RunLineInEditor { .then(() => exec(command)) .then(({ stdout }) => stdout, ({ stdout, stderr }) => JSON.stringify({ stderr, stdout }, null, ' ')) .then(content => replaceContent(target, content) - .then(() => this.parsedResult = JSON.parse(content)) - .then(undefined, err => {}) + .then(() => this.parsedResult = JSON.parse(content)) + .then(undefined, err => { }) ) .then(() => this.commandFinished(t0)) ) @@ -230,9 +367,9 @@ class RunLineInEditor { window.showInformationMessage("Please put the cursor on a line that contains a command (or part of a command)."); return ""; } - + // look upwards find the start of the command (if necessary) - while(!source.document.lineAt(lineNumber).text.trim().toLowerCase().startsWith(commandPrefix)) { + while (!source.document.lineAt(lineNumber).text.trim().toLowerCase().startsWith(commandPrefix)) { lineNumber--; } @@ -241,11 +378,11 @@ class RunLineInEditor { while (command.trim().endsWith(this.continuationCharacter)) { // concatenate all lines into a single command - lineNumber ++; + lineNumber++; command = command.trim().slice(0, -1) + this.stripComments(source.document.lineAt(lineNumber).text); } return command; - } + } else { // execute only the selected text const selectionStart = source.selection.start; @@ -257,7 +394,7 @@ class RunLineInEditor { else { // multiline command command = this.stripComments(source.document.lineAt(selectionStart.line).text.substring(selectionStart.character)); - for (let index = selectionStart.line+1; index <= selectionEnd.line; index++) { + for (let index = selectionStart.line + 1; index <= selectionEnd.line; index++) { if (command.trim().endsWith(this.continuationCharacter)) { command = command.trim().slice(0, -1); // remove continuation character from command } @@ -301,7 +438,7 @@ class RunLineInEditor { } // true if the specified position is in a string literal (surrounded by single quotes) - private isEmbeddedInString(text: string, position: number) : boolean { + private isEmbeddedInString(text: string, position: number): boolean { var stringStart = text.indexOf("'"); // start of string literal if (stringStart !== -1) { while (stringStart !== -1) { @@ -376,9 +513,9 @@ class RunLineInEditor { replaceContent(resultEditor, JSON.stringify(result, null, ' ')) .then(undefined, console.error); } catch (err) { - if (!(err && err.name === 'ParserError')) { - // console.error(err); Ignore because jmespath sometimes fails on partial queries. - } + // if (!(err && err.name === 'ParserError')) { + // // console.error(err); Ignore because jmespath sometimes fails on partial queries. + // } } } } diff --git a/src/recommend/RecommendService.ts b/src/recommend/RecommendService.ts new file mode 100644 index 0000000..b4a1436 --- /dev/null +++ b/src/recommend/RecommendService.ts @@ -0,0 +1,117 @@ +import { AzService } from "../azService"; + +export interface Recommendation { + description: string; + executeIndex: number[] + nextCommandSet: CommandInfo[] +} + +export interface CommandInfo { + reason: string; + example: string; +} + +export interface RecommendationQuery { + request: 'recommendation'; + commandList: string; +} + +export class RecommendService { + + // private static currentRecommends: Recommendation | null = null; + + // the line on which the recommended scenarios are based + private static line: number = -1; + private static scenarios: Recommendation[] | null = null; + + constructor(private azService: AzService) { + } + + static isReadyToRequestService(currentLine: number) { + return RecommendService.scenarios == null || currentLine != RecommendService.line; + } + + // static getCurrentRecommends(): Recommendation | null { + // return RecommendService.currentRecommends; + // } + + static setScenarios(nextScenarios: Recommendation[]) { + RecommendService.scenarios = nextScenarios; + } + + static popScenarios() { + const scenarios = RecommendService.scenarios; + RecommendService.scenarios = null; + return scenarios; + } + + static setLine(line: number) { + return RecommendService.line = line; + } + + // static setCurrentRecommends(recommends: Recommendation): void { + // let executeIndex = recommends.executeIndex; + // let nextCommandSet = []; + // for (let index of executeIndex) { + // recommends.nextCommandSet[index].isExecuted = false; + // nextCommandSet.push(recommends.nextCommandSet[index]); + // } + // for (let command of recommends.nextCommandSet) { + // if (command.isExecuted == null || command.isExecuted) { + // command.isExecuted = true + // nextCommandSet.push(command); + // } + // } + // recommends.nextCommandSet = nextCommandSet; + // RecommendService.currentRecommends = recommends; + // } + + // static initCurrentRecommends() { + // RecommendService.currentRecommends = null; + // } + + // static postProcessOfRecommend(index: number) { + // if (RecommendService.currentRecommends == null) { + // return; + // } + // let nextCommandSet = RecommendService.currentRecommends.nextCommandSet + // const executedCommand = nextCommandSet[index] + // delete nextCommandSet[index] + // executedCommand.isExecuted = true + // nextCommandSet.push(executedCommand) + // } + + // static preprocessRecommend(executedCommands: Set){ + // if (RecommendService.currentRecommends == null) { + // return; + // } + // let nextCommandSet = RecommendService.currentRecommends.nextCommandSet; + // const unusedCommands = []; + // const usedCommands = [] + // for (let command of nextCommandSet) { + // if (executedCommands.has(command.command)) { + // command.isExecuted = true; + // usedCommands.push(command) + // } else { + // command.isExecuted = false; + // unusedCommands.push(command) + // } + // } + + // RecommendService.currentRecommends.nextCommandSet = unusedCommands.concat(usedCommands); + // } + + async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { + try { + console.log('request recommendation service'); + return this.azService.send({ + request: 'recommendation', + commandList: commandList + }, onCancel); + } catch (err) { + console.error(err); + return []; + } + } +} + diff --git a/src/recommend/parser.ts b/src/recommend/parser.ts new file mode 100644 index 0000000..1d5a659 --- /dev/null +++ b/src/recommend/parser.ts @@ -0,0 +1,104 @@ +import { TextDocument, Position } from 'vscode'; + + +export class RecommendParser { + + private static readonly MAX_COMMAND_LIST_SIZE = 30; + + static parseLines(document: TextDocument, position: Position): { commandListJson: string } { + const commandListArr: string[] = []; + let line; + for (let i = 0; i <= position.line && commandListArr.length < RecommendParser.MAX_COMMAND_LIST_SIZE; i++) { + line = document.lineAt(i).text; + const command = RecommendParser.parseLine(line) + if (command != null && command.command.length > 0) { + commandListArr.push(JSON.stringify(command)); + } + } + if (commandListArr.length == 0) { + return { commandListJson: "" }; + } + const commandListJson = JSON.stringify(commandListArr) + return { commandListJson: commandListJson } + } + + static formatRecommendSample(commandSample: string): string { + const regex = /"[^"]*"|'[^']*'|\#.*|[^\s"'#]+/g; + let m; + let isSubCommand = true; + let formattedSample: string = ''; + const args: string[] = []; + const argsValues = new Map(); // : Map + + while (m = regex.exec(commandSample)) { + const text = m[0]; + if (text.startsWith('-')) { + isSubCommand = false; + args.push(text); + } else if (isSubCommand) { + formattedSample += text + ' '; + } else { + let arg = args[args.length - 1]; + if (!argsValues.has(arg)) { + argsValues.set(arg, []); + } + let values = argsValues.get(arg); + values.push(text); + } + } + + for (let arg of args) { + formattedSample += arg + ' '; + if (!argsValues.has(arg)) { + continue; + } + let curArgs = argsValues.get(arg); + let curArg = curArgs.join(' '); + if (curArg.startsWith('$')) { + formattedSample += '<' + curArg.substring(1) + '> '; + } else { + formattedSample += curArg + ' '; + } + } + + return formattedSample; + } + + private static parseLine(line: string) { + const regex = /"[^"]*"|'[^']*'|\#.*|[^\s"'#]+/g; + let m; + let isSubCommand = true; + let subcommand: string = ''; + const args: string[] = []; + let isFirstText = true; + while (m = regex.exec(line)) { + const text = m[0]; + if (text.startsWith('#')) { + break; + } + if (isFirstText) { + if (text != 'az') { + break; + } + isFirstText = false; + continue; + } + if (text.startsWith('-')) { + isSubCommand = false; + args.push(text); + } else if (isSubCommand) { + subcommand = subcommand + ' ' + text; + } + } + subcommand = subcommand.trim(); + if (subcommand.length == 0) { + return null; + } + + const command = { + command: subcommand, + arguments: args + } + return command + } +} \ No newline at end of file