From 24822190972817fb5792add72d1133b694f6b995 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Mon, 6 Mar 2023 14:52:18 +0800 Subject: [PATCH 01/12] init --- service/azservice/__main__.py | 41 ++++++++- service/azservice/recommend_tooling.py | 112 +++++++++++++++++++++++++ service/start.py | 3 +- src/azService.ts | 34 ++++++++ src/extension.ts | 80 ++++++++++++++---- src/recommendation/parser.ts | 62 ++++++++++++++ 6 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 service/azservice/recommend_tooling.py create mode 100644 src/recommendation/parser.ts diff --git a/service/azservice/__main__.py b/service/azservice/__main__.py index b4a34e1..67d0bee 100644 --- a/service/azservice/__main__.py +++ b/service/azservice/__main__.py @@ -16,6 +16,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 initialize as recommend_init, get_recommends + 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 +31,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 +119,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 [] @@ -305,13 +312,27 @@ def get_options(options): option.target if hasattr(option, 'target') else None for option in options ] if option ] + +def get_recommendations(command_list): + recommends = [] + from azure.cli.core.azclierror import RecommendationError + try: + recommends = get_recommends(command_list) + except RecommendationError as e: + log(e.error_msg) + return recommends + +def log(message): + print(message, file=stderr) def main(): - timings = False + timings = True start = time.time() initialize() if timings: print('initialize {} s'.format(time.time() - start), file=stderr) + recommend_init() + start = time.time() command_table = load_command_table() if timings: print('load_command_table {} s'.format(time.time() - start), file=stderr) @@ -324,10 +345,16 @@ def main(): snippets = get_snippets(command_table) if AUTOMATIC_SNIPPETS_ENABLED else [] if timings: print('get_snippets {} s'.format(time.time() - start), file=stderr) + # start = time.time() + # recommend_set_cli_ctx(cli_ctx) + # if timings: print('recommend_set_cli_ctx {} s'.format(time.time() - start), file=stderr) + 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 @@ -335,14 +362,17 @@ def enqueue_output(input, queue): 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 +383,16 @@ 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': + response_data = get_recommendations(request['data']['commandList']) + if timings: print('get_recommendations {} s'.format(time.time() - start), file=stderr) 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..08d6d63 --- /dev/null +++ b/service/azservice/recommend_tooling.py @@ -0,0 +1,112 @@ +import hashlib +import json +import time +from enum import Enum +from sys import stderr + +from azure.cli.core import get_default_cli, __version__ as version +from azure.cli.core import telemetry +from azure.cli.core.azclierror import RecommendationError + + +class RecommendType(int, Enum): + All = 1 + Solution = 2 + Command = 3 + Scenario = 4 + + +cli_ctx = None +def initialize(): + global cli_ctx + cli_ctx = get_default_cli() + print(f"version = {version}", file=stderr) + +def get_recommends(command_list): + 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 = [] + arg_index = 1 + for next_command in s['nextCommandSet']: + command = 'az ' + next_command['command'] + for arg in next_command['arguments']: + command += ' ' + arg + '$' + str(arg_index) + arg_index += 1 + command_info = { + 'command': command, + 'reason': next_command['reason'], + 'example': next_command['example'] + } + next_command_set.append(command_info) + + return { + 'description': description, + 'nextCommandSet': next_command_set + } + +def get_scenarios(recommends): + return [rec for rec in recommends if rec['type'] == RecommendType.Scenario] 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..0b35534 100644 --- a/src/azService.ts +++ b/src/azService.ts @@ -56,6 +56,22 @@ interface Message { data: T; } +export interface Recommendation { + description: string; + nextCommandSet: CommandInfo[] +} + +export interface CommandInfo { + command: string, + reason: string; + example: string +} + +export interface RecommendationQuery { + request: 'recommendation'; + commandList: string; +} + export class AzService { private process: Promise | undefined; @@ -91,6 +107,18 @@ export class AzService { }, onCancel); } + async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { + try { + return this.send({ + request: 'recommendation', + commandList: commandList + }, onCancel); + } catch (err) { + console.error(err); + return []; + } + } + private async send(data: T, onCancel?: (handle: () => void) => void): Promise { const process = await this.getProcess(); return new Promise((resolve, reject) => { @@ -117,9 +145,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 +158,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 +169,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..540047d 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,9 +11,12 @@ import { parse, findNode } from './parser'; import { exec } from './utils'; import * as spinner from 'elegant-spinner'; +import * as AzRecommendationParser from './recommendation/parser'; + export function activate(context: ExtensionContext) { const azService = new AzService(azNotFound); context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(azService), ' ')); + context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzRecommendationProvider(azService), '\n')); context.subscriptions.push(languages.registerHoverProvider('azcli', new AzHoverProvider(azService))); const status = new StatusBarInfo(azService); context.subscriptions.push(status); @@ -36,14 +39,18 @@ class AzCompletionItemProvider implements CompletionItemProvider { } 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. @@ -99,6 +106,49 @@ class AzCompletionItemProvider implements CompletionItemProvider { } } +class AzRecommendationProvider implements CompletionItemProvider { + + private readonly MAX_COMMAND_LIST_SIZE = 30; + + constructor(private azService: AzService) { + } + + provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { + if (context.triggerKind != CompletionTriggerKind.TriggerCharacter) { + // request recommendation service only when a line has been input in full + return; + } + console.log('trigger recommendation: line'); + const commandListArr: string[] = []; + let line; + for (let i = 0; i < position.line && commandListArr.length < this.MAX_COMMAND_LIST_SIZE; i++) { + line = document.lineAt(i).text; + const command = AzRecommendationParser.parseLine(line) + if (command != null && command.length > 0) { + commandListArr.push(command); + } + } + if (commandListArr.length == 0) { + return; + } + const commandListJson = JSON.stringify(commandListArr) + + // return new CompletionList; + return this.azService.getRecommendation(commandListJson, token.onCancellationRequested) + .then(completions => completions.map(({ description, nextCommandSet }) => { + const item = new CompletionItem(description, CompletionItemKind.Snippet); + let nextCommands = '' + for (const nextCommand of nextCommandSet) { + nextCommands += '\n# ' + nextCommand.reason + '\n# example: ' + nextCommand.example + '\n' + nextCommands += nextCommand.command + '\n' + } + item.insertText = new SnippetString(nextCommands); + return item; + })); + } + +} + class AzHoverProvider implements HoverProvider { constructor(private azService: AzService) { @@ -157,10 +207,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))); @@ -201,8 +251,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 +280,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 +291,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 +307,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 +351,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 +426,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/recommendation/parser.ts b/src/recommendation/parser.ts new file mode 100644 index 0000000..c04b2a9 --- /dev/null +++ b/src/recommendation/parser.ts @@ -0,0 +1,62 @@ +export function parse(line: string) { + +} + + +export function 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 json = JSON.stringify({ + command: subcommand, + // arguments: args + }) + + return json +} + + +export function validateLine(line: string) { + const regex = /"[^"]*"|'[^']*'|\#.*|[^\s"'#]+/g; + let m; + let validLine: string = ''; + while (m = regex.exec(line)) { + const text = m[0]; + const isComment = text.startsWith('#'); + if (isComment) { + break; + } + validLine = validLine + ' ' + text; + } + validLine = validLine.trim() + if (!validLine.startsWith('az ')) { + validLine = '' + } + return validLine; +} \ No newline at end of file From d48c0f9288fb16c76171689c8925541c8239f09e Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Fri, 10 Mar 2023 15:34:19 +0800 Subject: [PATCH 02/12] tmp --- service/azservice/recommend_tooling.py | 8 ++- src/azService.ts | 30 +--------- src/extension.ts | 78 +++++++++++++++++++------- src/recommendation/RecommendService.ts | 75 +++++++++++++++++++++++++ src/recommendation/parser.ts | 10 ++-- 5 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 src/recommendation/RecommendService.ts diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index 08d6d63..6b34e8b 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -93,11 +93,12 @@ def get_info_of_one_scenario(s, index): arg_index = 1 for next_command in s['nextCommandSet']: command = 'az ' + next_command['command'] - for arg in next_command['arguments']: - command += ' ' + arg + '$' + str(arg_index) - arg_index += 1 + # for arg in next_command['arguments']: + # command += ' ' + arg + '$' + str(arg_index) + # arg_index += 1 command_info = { 'command': command, + 'arguments': next_command['arguments'], 'reason': next_command['reason'], 'example': next_command['example'] } @@ -105,6 +106,7 @@ def get_info_of_one_scenario(s, index): return { 'description': description, + 'executeIndex': s['executeIndex'], 'nextCommandSet': next_command_set } diff --git a/src/azService.ts b/src/azService.ts index 0b35534..41568a3 100644 --- a/src/azService.ts +++ b/src/azService.ts @@ -56,22 +56,6 @@ interface Message { data: T; } -export interface Recommendation { - description: string; - nextCommandSet: CommandInfo[] -} - -export interface CommandInfo { - command: string, - reason: string; - example: string -} - -export interface RecommendationQuery { - request: 'recommendation'; - commandList: string; -} - export class AzService { private process: Promise | undefined; @@ -107,19 +91,7 @@ export class AzService { }, onCancel); } - async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { - try { - return this.send({ - request: 'recommendation', - commandList: commandList - }, onCancel); - } catch (err) { - console.error(err); - return []; - } - } - - 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) { diff --git a/src/extension.ts b/src/extension.ts index 540047d..fe854e3 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, CompletionContext, CompletionTriggerKind, 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'; @@ -12,17 +12,22 @@ import { exec } from './utils'; import * as spinner from 'elegant-spinner'; import * as AzRecommendationParser from './recommendation/parser'; +import { RecommendService, Recommendation } from './recommendation/RecommendService'; export function activate(context: ExtensionContext) { const azService = new AzService(azNotFound); + const recommendService = new RecommendService(azService); context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(azService), ' ')); - context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzRecommendationProvider(azService), '\n')); + 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 = { @@ -110,7 +115,7 @@ class AzRecommendationProvider implements CompletionItemProvider { private readonly MAX_COMMAND_LIST_SIZE = 30; - constructor(private azService: AzService) { + constructor(private recommendService: RecommendService) { } provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { @@ -121,30 +126,63 @@ class AzRecommendationProvider implements CompletionItemProvider { console.log('trigger recommendation: line'); const commandListArr: string[] = []; let line; + const executedCommand = [] for (let i = 0; i < position.line && commandListArr.length < this.MAX_COMMAND_LIST_SIZE; i++) { line = document.lineAt(i).text; const command = AzRecommendationParser.parseLine(line) - if (command != null && command.length > 0) { - commandListArr.push(command); + if (command != null && command.command.length > 0) { + executedCommand.push('az ' + command.command) + commandListArr.push(JSON.stringify(command)); } } if (commandListArr.length == 0) { return; } const commandListJson = JSON.stringify(commandListArr) - - // return new CompletionList; - return this.azService.getRecommendation(commandListJson, token.onCancellationRequested) - .then(completions => completions.map(({ description, nextCommandSet }) => { - const item = new CompletionItem(description, CompletionItemKind.Snippet); - let nextCommands = '' - for (const nextCommand of nextCommandSet) { - nextCommands += '\n# ' + nextCommand.reason + '\n# example: ' + nextCommand.example + '\n' - nextCommands += nextCommand.command + '\n' - } - item.insertText = new SnippetString(nextCommands); - return item; - })); + + const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends(commandListArr) + if (currentRecommends == null) { + return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + .then(completions => completions.map(({ description, executeIndex, nextCommandSet }) => { + const item = new CompletionItem(description, CompletionItemKind.Operator); + item.command = { + title: 'set current recommends', + command: 'ms-azurecli.setCurrentRecommends', + arguments: [{ description, executeIndex, nextCommandSet }] + }; + return item; + })); + } + + 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; + // item.command = { + // title: 'post process of choosing scenario one command', + // command: 'ms-azurecli.postProcessOfRecommend', + // arguments: [index] + // }; + 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; } } @@ -219,10 +257,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); diff --git a/src/recommendation/RecommendService.ts b/src/recommendation/RecommendService.ts new file mode 100644 index 0000000..cf4cafb --- /dev/null +++ b/src/recommendation/RecommendService.ts @@ -0,0 +1,75 @@ +import { AzService } from "../azService"; + +export interface Recommendation { + description: string; + executeIndex: number[] + nextCommandSet: CommandInfo[] +} + +export interface CommandInfo { + command: string, + arguments: string[], + reason: string; + example: string; + isExecuted: boolean | false +} + +export interface RecommendationQuery { + request: 'recommendation'; + commandList: string; +} + + +export class RecommendService { + + private static currentRecommends: Recommendation | null = null; + + constructor(private azService: AzService) { + } + + static getCurrentRecommends(commandList: string[]): Recommendation | null { + return RecommendService.currentRecommends; + } + + static setCurrentRecommends(recommends: Recommendation): void { + let executeIndex = recommends.executeIndex; + let nextCommandSet = []; + for (let index of executeIndex) { + nextCommandSet.push(recommends.nextCommandSet[index]); + } + 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: string[]){ + + } + + async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { + try { + return this.azService.send({ + request: 'recommendation', + commandList: commandList + }, onCancel); + } catch (err) { + console.error(err); + return []; + } + } +} + diff --git a/src/recommendation/parser.ts b/src/recommendation/parser.ts index c04b2a9..c88497c 100644 --- a/src/recommendation/parser.ts +++ b/src/recommendation/parser.ts @@ -33,12 +33,14 @@ export function parseLine(line: string) { if (subcommand.length == 0) { return null; } - const json = JSON.stringify({ + + const command = { command: subcommand, - // arguments: args - }) + arguments: args + } + const json = JSON.stringify(command) - return json + return command } From 93c8e03b39fae3a19d863e99d4474f23a4a9c803 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Fri, 10 Mar 2023 17:38:29 +0800 Subject: [PATCH 03/12] need action after selecting --- src/extension.ts | 15 +++++-------- src/recommendation/RecommendService.ts | 29 +++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index fe854e3..711adc8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -120,18 +120,17 @@ class AzRecommendationProvider implements CompletionItemProvider { provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { if (context.triggerKind != CompletionTriggerKind.TriggerCharacter) { - // request recommendation service only when a line has been input in full return; } console.log('trigger recommendation: line'); const commandListArr: string[] = []; let line; - const executedCommand = [] + const executedCommand = new Set() for (let i = 0; i < position.line && commandListArr.length < this.MAX_COMMAND_LIST_SIZE; i++) { line = document.lineAt(i).text; const command = AzRecommendationParser.parseLine(line) if (command != null && command.command.length > 0) { - executedCommand.push('az ' + command.command) + executedCommand.add('az ' + command.command) commandListArr.push(JSON.stringify(command)); } } @@ -142,9 +141,9 @@ class AzRecommendationProvider implements CompletionItemProvider { const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends(commandListArr) if (currentRecommends == null) { - return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + const items = this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) .then(completions => completions.map(({ description, executeIndex, nextCommandSet }) => { - const item = new CompletionItem(description, CompletionItemKind.Operator); + const item = new CompletionItem(description, CompletionItemKind.Module); item.command = { title: 'set current recommends', command: 'ms-azurecli.setCurrentRecommends', @@ -152,6 +151,7 @@ class AzRecommendationProvider implements CompletionItemProvider { }; return item; })); + return items; } const items: CompletionItem[] = [] @@ -168,11 +168,6 @@ class AzRecommendationProvider implements CompletionItemProvider { } item.insertText = new SnippetString(command); item.detail = nextCommand.example; - // item.command = { - // title: 'post process of choosing scenario one command', - // command: 'ms-azurecli.postProcessOfRecommend', - // arguments: [index] - // }; items.push(item); } const cleanItem = new CompletionItem('no more commands in this scenario are needed', CompletionItemKind.Event); diff --git a/src/recommendation/RecommendService.ts b/src/recommendation/RecommendService.ts index cf4cafb..8d35c44 100644 --- a/src/recommendation/RecommendService.ts +++ b/src/recommendation/RecommendService.ts @@ -11,7 +11,7 @@ export interface CommandInfo { arguments: string[], reason: string; example: string; - isExecuted: boolean | false + isExecuted: boolean | null } export interface RecommendationQuery { @@ -35,8 +35,15 @@ export class RecommendService { 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; } @@ -56,8 +63,24 @@ export class RecommendService { nextCommandSet.push(executedCommand) } - static preprocessRecommend(executedCommands: string[]){ - + static preprocessRecommend(executedCommands: any){ + 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 { From da6d86aca5a5d4c1bb08e4c090414047b51bc214 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Fri, 10 Mar 2023 18:18:54 +0800 Subject: [PATCH 04/12] tmp --- src/recommendation/RecommendService.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/recommendation/RecommendService.ts b/src/recommendation/RecommendService.ts index 8d35c44..cc921bb 100644 --- a/src/recommendation/RecommendService.ts +++ b/src/recommendation/RecommendService.ts @@ -24,13 +24,25 @@ export class RecommendService { private static currentRecommends: Recommendation | null = null; + // the line on which the recommended scenarios are based + private static requestServiceLine: number = -1; + private static nextScenarios: Recommendation[] | null = null; + constructor(private azService: AzService) { } - static getCurrentRecommends(commandList: string[]): Recommendation | null { + static isReadyToRequestService(currentLine: number) { + + } + + static getCurrentRecommends(): Recommendation | null { return RecommendService.currentRecommends; } + static getNextScenarios(): Recommendation[] | null { + return RecommendService.nextScenarios; + } + static setCurrentRecommends(recommends: Recommendation): void { let executeIndex = recommends.executeIndex; let nextCommandSet = []; From 2240b301f640bdb2aaddaa722d12ae501c25c36d Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Mon, 13 Mar 2023 14:57:40 +0800 Subject: [PATCH 05/12] tmp --- service/azservice/__main__.py | 27 ++----- service/azservice/recommend_tooling.py | 32 +++++++- service/azservice/tooling2.py | 6 +- src/extension.ts | 42 +++++----- src/recommendation/RecommendService.ts | 30 ++++--- src/recommendation/parser.ts | 104 ++++++++++++------------- 6 files changed, 130 insertions(+), 111 deletions(-) diff --git a/service/azservice/__main__.py b/service/azservice/__main__.py index 67d0bee..34e0bb7 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,7 +17,7 @@ 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 initialize as recommend_init, get_recommends +from azservice.recommend_tooling import request_recommend_service 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 @@ -311,16 +312,7 @@ def get_options(options): option if isinstance(option, str) else option.target if hasattr(option, 'target') else None - for option in options ] if option ] - -def get_recommendations(command_list): - recommends = [] - from azure.cli.core.azclierror import RecommendationError - try: - recommends = get_recommends(command_list) - except RecommendationError as e: - log(e.error_msg) - return recommends + for option in options ] if option ] def log(message): print(message, file=stderr) @@ -331,8 +323,6 @@ def main(): initialize() if timings: print('initialize {} s'.format(time.time() - start), file=stderr) - recommend_init() - start = time.time() command_table = load_command_table() if timings: print('load_command_table {} s'.format(time.time() - start), file=stderr) @@ -345,10 +335,6 @@ def main(): snippets = get_snippets(command_table) if AUTOMATIC_SNIPPETS_ENABLED else [] if timings: print('get_snippets {} s'.format(time.time() - start), file=stderr) - # start = time.time() - # recommend_set_cli_ctx(cli_ctx) - # if timings: print('recommend_set_cli_ctx {} s'.format(time.time() - start), file=stderr) - def enqueue_output(input, queue): for line in iter(input.readline, b''): queue.put(line) @@ -360,6 +346,8 @@ def enqueue_output(input, queue): thread.daemon = True thread.start() + recommend_executor = ThreadPoolExecutor(max_workers=1) + bkg_start = time.time() keep_loading = True while True: @@ -387,8 +375,9 @@ def enqueue_output(input, queue): 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': - response_data = get_recommendations(request['data']['commandList']) - if timings: print('get_recommendations {} s'.format(time.time() - start), file=stderr) + 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) diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index 6b34e8b..b994f68 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -2,7 +2,7 @@ import json import time from enum import Enum -from sys import stderr +from sys import stdout, stderr from azure.cli.core import get_default_cli, __version__ as version from azure.cli.core import telemetry @@ -17,16 +17,41 @@ class RecommendType(int, Enum): cli_ctx = None + + def initialize(): global cli_ctx cli_ctx = get_default_cli() print(f"version = {version}", file=stderr) + +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): 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 @@ -42,7 +67,7 @@ def get_recommends_from_api(command_list, top_num=5): "command_list": command_list, "type": type, "top_num": top_num, - 'cli_version': version, + 'cli_version': version, 'user_id': hashed_user_id } @@ -75,6 +100,7 @@ def get_recommends_from_api(command_list, top_num=5): return recommends + def get_scenarios_info(recommends): scenarios = get_scenarios(recommends) or [] scenarios_info = [] @@ -83,6 +109,7 @@ def get_scenarios_info(recommends): 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"]}' @@ -110,5 +137,6 @@ def get_info_of_one_scenario(s, index): 'nextCommandSet': next_command_set } + def get_scenarios(recommends): return [rec for rec in recommends if rec['type'] == RecommendType.Scenario] 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/src/extension.ts b/src/extension.ts index 711adc8..4a8caf5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,13 +11,13 @@ import { parse, findNode } from './parser'; import { exec } from './utils'; import * as spinner from 'elegant-spinner'; -import * as AzRecommendationParser from './recommendation/parser'; +import { RecommendParser } from './recommendation/parser'; import { RecommendService, Recommendation } from './recommendation/RecommendService'; export function activate(context: ExtensionContext) { const azService = new AzService(azNotFound); const recommendService = new RecommendService(azService); - context.subscriptions.push(languages.registerCompletionItemProvider('azcli', new AzCompletionItemProvider(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); @@ -40,7 +40,7 @@ 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 { @@ -70,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.setCurrentLine(position.line); + this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested, true) + .then(recommendations => { + console.log('setNextScenarios recommendations'); + RecommendService.setNextScenarios(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]); @@ -113,8 +123,6 @@ class AzCompletionItemProvider implements CompletionItemProvider { class AzRecommendationProvider implements CompletionItemProvider { - private readonly MAX_COMMAND_LIST_SIZE = 30; - constructor(private recommendService: RecommendService) { } @@ -123,26 +131,13 @@ class AzRecommendationProvider implements CompletionItemProvider { return; } console.log('trigger recommendation: line'); - const commandListArr: string[] = []; - let line; - const executedCommand = new Set() - for (let i = 0; i < position.line && commandListArr.length < this.MAX_COMMAND_LIST_SIZE; i++) { - line = document.lineAt(i).text; - const command = AzRecommendationParser.parseLine(line) - if (command != null && command.command.length > 0) { - executedCommand.add('az ' + command.command) - commandListArr.push(JSON.stringify(command)); - } - } - if (commandListArr.length == 0) { - return; - } - const commandListJson = JSON.stringify(commandListArr) - const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends(commandListArr) + const { executedCommand: executedCommand, commandListJson: commandListJson } = RecommendParser.parseLines(document, position); + const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends() if (currentRecommends == null) { - const items = this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) - .then(completions => completions.map(({ description, executeIndex, nextCommandSet }) => { + console.log('provideCompletionItems triggered ...'); + return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) + .then(nextScenarios => nextScenarios.map(({ description, executeIndex, nextCommandSet }) => { const item = new CompletionItem(description, CompletionItemKind.Module); item.command = { title: 'set current recommends', @@ -151,7 +146,6 @@ class AzRecommendationProvider implements CompletionItemProvider { }; return item; })); - return items; } const items: CompletionItem[] = [] diff --git a/src/recommendation/RecommendService.ts b/src/recommendation/RecommendService.ts index cc921bb..cbf6dda 100644 --- a/src/recommendation/RecommendService.ts +++ b/src/recommendation/RecommendService.ts @@ -19,7 +19,6 @@ export interface RecommendationQuery { commandList: string; } - export class RecommendService { private static currentRecommends: Recommendation | null = null; @@ -32,15 +31,19 @@ export class RecommendService { } static isReadyToRequestService(currentLine: number) { - + return RecommendService.requestServiceLine != currentLine; // RecommendService.isReInit && } static getCurrentRecommends(): Recommendation | null { return RecommendService.currentRecommends; } - - static getNextScenarios(): Recommendation[] | null { - return RecommendService.nextScenarios; + + static setNextScenarios(nextScenarios: Recommendation[]) { + RecommendService.nextScenarios = nextScenarios; + } + + static setCurrentLine(line: number) { + return RecommendService.requestServiceLine = line; } static setCurrentRecommends(recommends: Recommendation): void { @@ -75,7 +78,7 @@ export class RecommendService { nextCommandSet.push(executedCommand) } - static preprocessRecommend(executedCommands: any){ + static preprocessRecommend(executedCommands: Set){ if (RecommendService.currentRecommends == null) { return; } @@ -95,12 +98,17 @@ export class RecommendService { RecommendService.currentRecommends.nextCommandSet = unusedCommands.concat(usedCommands); } - async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { + async getRecommendation(commandList: string, onCancel: (handle: () => void) => void, isRequestService?: boolean): Promise { try { - return this.azService.send({ - request: 'recommendation', - commandList: commandList - }, onCancel); + if (RecommendService.nextScenarios == null || isRequestService) { + console.log('request recommendation service'); + return this.azService.send({ + request: 'recommendation', + commandList: commandList + }, onCancel); + } + console.log('get next scenarios directly'); + return RecommendService.nextScenarios; } catch (err) { console.error(err); return []; diff --git a/src/recommendation/parser.ts b/src/recommendation/parser.ts index c88497c..6ddb59b 100644 --- a/src/recommendation/parser.ts +++ b/src/recommendation/parser.ts @@ -1,64 +1,64 @@ -export function parse(line: string) { +import { TextDocument, Position } from 'vscode'; -} +export class RecommendParser { -export function 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; + private static readonly MAX_COMMAND_LIST_SIZE = 30; + + static parseLines(document: TextDocument, position: Position): { executedCommand: Set, commandListJson: string } { + const commandListArr: string[] = []; + let line; + const executedCommand = new Set() + 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) { + executedCommand.add('az ' + command.command) + commandListArr.push(JSON.stringify(command)); + } } - if (isFirstText) { - if (text != 'az') { + if (commandListArr.length == 0) { + return { executedCommand: executedCommand, commandListJson: "" }; + } + const commandListJson = JSON.stringify(commandListArr) + return { executedCommand: executedCommand, commandListJson: commandListJson } + } + + 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; } - isFirstText = false; - continue; + if (isFirstText) { + if (text != 'az') { + break; + } + isFirstText = false; + continue; + } + if (text.startsWith('-')) { + isSubCommand = false; + args.push(text); + } else if (isSubCommand) { + subcommand = subcommand + ' ' + text; + } } - if (text.startsWith('-')) { - isSubCommand = false; - args.push(text); - } else if (isSubCommand) { - subcommand = subcommand + ' ' + text; + subcommand = subcommand.trim(); + if (subcommand.length == 0) { + return null; } - } - subcommand = subcommand.trim(); - if (subcommand.length == 0) { - return null; - } - - const command = { - command: subcommand, - arguments: args - } - const json = JSON.stringify(command) - return command -} - - -export function validateLine(line: string) { - const regex = /"[^"]*"|'[^']*'|\#.*|[^\s"'#]+/g; - let m; - let validLine: string = ''; - while (m = regex.exec(line)) { - const text = m[0]; - const isComment = text.startsWith('#'); - if (isComment) { - break; + const command = { + command: subcommand, + arguments: args } - validLine = validLine + ' ' + text; - } - validLine = validLine.trim() - if (!validLine.startsWith('az ')) { - validLine = '' + return command } - return validLine; } \ No newline at end of file From e76fac6e76a925d648123ba028b2a15961011a01 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Mon, 13 Mar 2023 16:07:05 +0800 Subject: [PATCH 06/12] need fix rejected issue --- service/azservice/__main__.py | 6 +++--- service/azservice/output_tool.py | 11 +++++++++++ service/azservice/recommend_tooling.py | 12 ++++++------ src/extension.ts | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 service/azservice/output_tool.py diff --git a/service/azservice/__main__.py b/service/azservice/__main__.py index 34e0bb7..7d6fee6 100644 --- a/service/azservice/__main__.py +++ b/service/azservice/__main__.py @@ -19,6 +19,8 @@ from azservice.recommend_tooling import request_recommend_service +from azservice.output_tool import flush_output + 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. @@ -387,9 +389,7 @@ def enqueue_output(input, queue): 'data': response_data } output = json.dumps(response) - stdout.write(output + '\n') - stdout.flush() - stderr.flush() + flush_output(output) main() diff --git a/service/azservice/output_tool.py b/service/azservice/output_tool.py new file mode 100644 index 0000000..5f26766 --- /dev/null +++ b/service/azservice/output_tool.py @@ -0,0 +1,11 @@ +from sys import stdin, stdout, stderr + +def flush_output(output): + print('flush_output ================= ', file=stderr) + stdout.write(output + '\n') + stdout.flush() + stderr.flush() + +# def write(output): +# print('flush_output ================= ', file=stderr) +# flush_output(output) \ No newline at end of file diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index b994f68..a14d3c2 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -8,6 +8,7 @@ from azure.cli.core import telemetry from azure.cli.core.azclierror import RecommendationError +from azservice.output_tool import flush_output class RecommendType(int, Enum): All = 1 @@ -27,6 +28,10 @@ def initialize(): def request_recommend_service(request): start = time.time() + + if cli_ctx is None: + initialize() + command_list = request['data']['commandList'] recommends = [] from azure.cli.core.azclierror import RecommendationError @@ -40,9 +45,7 @@ def request_recommend_service(request): 'data': recommends } output = json.dumps(response) - stdout.write(output + '\n') - stdout.flush() - stderr.flush() + flush_output(output) print('request_recommend_service {} s'.format(time.time() - start), file=stderr) @@ -120,9 +123,6 @@ def get_info_of_one_scenario(s, index): arg_index = 1 for next_command in s['nextCommandSet']: command = 'az ' + next_command['command'] - # for arg in next_command['arguments']: - # command += ' ' + arg + '$' + str(arg_index) - # arg_index += 1 command_info = { 'command': command, 'arguments': next_command['arguments'], diff --git a/src/extension.ts b/src/extension.ts index 4a8caf5..2ba07d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -127,7 +127,7 @@ class AzRecommendationProvider implements CompletionItemProvider { } provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { - if (context.triggerKind != CompletionTriggerKind.TriggerCharacter) { + if (context.triggerKind != CompletionTriggerKind.TriggerCharacter && document.lineAt(position.line).text.trim().length == 0) { return; } console.log('trigger recommendation: line'); From fb0983729b621b39651b8973f5958dadcfb0cb6c Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 9 May 2023 15:12:55 +0800 Subject: [PATCH 07/12] tmp --- service/azservice/recommend_tooling.py | 8 +++++--- src/extension.ts | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index a14d3c2..e9ce784 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -17,7 +17,7 @@ class RecommendType(int, Enum): Scenario = 4 -cli_ctx = None +cli_ctx def initialize(): @@ -29,8 +29,8 @@ def initialize(): def request_recommend_service(request): start = time.time() - if cli_ctx is None: - initialize() + # if cli_ctx is None: + # initialize() command_list = request['data']['commandList'] recommends = [] @@ -50,6 +50,8 @@ def request_recommend_service(request): def get_recommends(command_list): + # global cli_ctx + 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 diff --git a/src/extension.ts b/src/extension.ts index 2ba07d8..51a81a8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -138,7 +138,8 @@ class AzRecommendationProvider implements CompletionItemProvider { console.log('provideCompletionItems triggered ...'); return this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) .then(nextScenarios => nextScenarios.map(({ description, executeIndex, nextCommandSet }) => { - const item = new CompletionItem(description, CompletionItemKind.Module); + const item = new CompletionItem(description, CompletionItemKind.Unit); + item.insertText = ''; item.command = { title: 'set current recommends', command: 'ms-azurecli.setCurrentRecommends', From 0f8984890062fec19b064edb9374d6e4f022ec2f Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 9 May 2023 15:44:33 +0800 Subject: [PATCH 08/12] tmp --- service/azservice/__main__.py | 9 +++++---- service/azservice/output_tool.py | 11 ----------- service/azservice/recommend_tooling.py | 23 ++++++++--------------- 3 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 service/azservice/output_tool.py diff --git a/service/azservice/__main__.py b/service/azservice/__main__.py index 7d6fee6..58ad32e 100644 --- a/service/azservice/__main__.py +++ b/service/azservice/__main__.py @@ -17,9 +17,7 @@ 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 - -from azservice.output_tool import flush_output +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 @@ -323,6 +321,7 @@ def main(): timings = True start = time.time() initialize() + recommend_init() if timings: print('initialize {} s'.format(time.time() - start), file=stderr) start = time.time() @@ -389,7 +388,9 @@ def enqueue_output(input, queue): 'data': response_data } output = json.dumps(response) - flush_output(output) + stdout.write(output + '\n') + stdout.flush() + stderr.flush() main() diff --git a/service/azservice/output_tool.py b/service/azservice/output_tool.py deleted file mode 100644 index 5f26766..0000000 --- a/service/azservice/output_tool.py +++ /dev/null @@ -1,11 +0,0 @@ -from sys import stdin, stdout, stderr - -def flush_output(output): - print('flush_output ================= ', file=stderr) - stdout.write(output + '\n') - stdout.flush() - stderr.flush() - -# def write(output): -# print('flush_output ================= ', file=stderr) -# flush_output(output) \ No newline at end of file diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index e9ce784..2f13015 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -4,11 +4,11 @@ from enum import Enum from sys import stdout, stderr -from azure.cli.core import get_default_cli, __version__ as version +from azure.cli.core import __version__ as version from azure.cli.core import telemetry from azure.cli.core.azclierror import RecommendationError -from azservice.output_tool import flush_output +from azservice.tooling2 import cli_ctx class RecommendType(int, Enum): All = 1 @@ -16,22 +16,14 @@ class RecommendType(int, Enum): Command = 3 Scenario = 4 - -cli_ctx - - -def initialize(): +cli_ctx = None +def init(): global cli_ctx - cli_ctx = get_default_cli() - print(f"version = {version}", file=stderr) - + from azservice.tooling2 import cli_ctx def request_recommend_service(request): start = time.time() - # if cli_ctx is None: - # initialize() - command_list = request['data']['commandList'] recommends = [] from azure.cli.core.azclierror import RecommendationError @@ -45,12 +37,13 @@ def request_recommend_service(request): 'data': recommends } output = json.dumps(response) - flush_output(output) + stdout.write(output + '\n') + stdout.flush() + stderr.flush() print('request_recommend_service {} s'.format(time.time() - start), file=stderr) def get_recommends(command_list): - # global cli_ctx 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) From 3c7b46ac7d6bf5cea748530dc579379b3efe7b61 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 9 May 2023 15:56:51 +0800 Subject: [PATCH 09/12] rename folder --- service/azservice/recommend_tooling.py | 2 +- service/azservice/tooling.py | 2 +- src/extension.ts | 4 ++-- src/{recommendation => recommend}/RecommendService.ts | 0 src/{recommendation => recommend}/parser.ts | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{recommendation => recommend}/RecommendService.ts (100%) rename src/{recommendation => recommend}/parser.ts (100%) diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index 2f13015..900087a 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -134,4 +134,4 @@ def get_info_of_one_scenario(s, index): def get_scenarios(recommends): - return [rec for rec in recommends if rec['type'] == RecommendType.Scenario] + 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/src/extension.ts b/src/extension.ts index 51a81a8..f722a64 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,8 +11,8 @@ import { parse, findNode } from './parser'; import { exec } from './utils'; import * as spinner from 'elegant-spinner'; -import { RecommendParser } from './recommendation/parser'; -import { RecommendService, Recommendation } from './recommendation/RecommendService'; +import { RecommendParser } from './recommend/parser'; +import { RecommendService, Recommendation } from './recommend/RecommendService'; export function activate(context: ExtensionContext) { const azService = new AzService(azNotFound); diff --git a/src/recommendation/RecommendService.ts b/src/recommend/RecommendService.ts similarity index 100% rename from src/recommendation/RecommendService.ts rename to src/recommend/RecommendService.ts diff --git a/src/recommendation/parser.ts b/src/recommend/parser.ts similarity index 100% rename from src/recommendation/parser.ts rename to src/recommend/parser.ts From 6617734386903f8acccb102b2b67618051142677 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Thu, 25 May 2023 17:58:28 +0800 Subject: [PATCH 10/12] scenario recommendation: one step --- src/extension.ts | 117 +++++++++++++++++--------- src/recommend/RecommendService.ts | 133 ++++++++++++++++-------------- 2 files changed, 147 insertions(+), 103 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index f722a64..6a2a9ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,9 +25,9 @@ export function activate(context: ExtensionContext) { 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)); + // 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 = { @@ -73,11 +73,11 @@ class AzCompletionItemProvider implements CompletionItemProvider { if (argument != null && RecommendService.isReadyToRequestService(position.line)) { // ready to request recommendation service const { commandListJson: commandListJson } = RecommendParser.parseLines(document, position); - RecommendService.setCurrentLine(position.line); + RecommendService.setLine(position.line); this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested, true) .then(recommendations => { console.log('setNextScenarios recommendations'); - RecommendService.setNextScenarios(recommendations); + RecommendService.setScenarios(recommendations); }); } return this.azService.getCompletions(subcommand[0] === 'az' ? { subcommand: subcommand.slice(1).join(' '), argument, arguments: args } : {}, token.onCancellationRequested) @@ -132,49 +132,88 @@ class AzRecommendationProvider implements CompletionItemProvider { } console.log('trigger recommendation: line'); - const { executedCommand: executedCommand, commandListJson: commandListJson } = RecommendParser.parseLines(document, position); - const currentRecommends: Recommendation | null = RecommendService.getCurrentRecommends() - if (currentRecommends == null) { + // 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(({ 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; + .then(nextScenarios => nextScenarios.map((scenario) => { + return this.processItem(scenario); })); } - 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 items: CompletionItem[] = []; + for (let scenarioIndex = 0; scenarioIndex < scenarios.length; scenarioIndex++) { + items.push(this.processItem(scenarios[scenarioIndex])); } - 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; + + + // 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 = ""; + 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' + } + + item.insertText = new SnippetString(insertText); + return item; + } } class AzHoverProvider implements HoverProvider { diff --git a/src/recommend/RecommendService.ts b/src/recommend/RecommendService.ts index cbf6dda..38db7d2 100644 --- a/src/recommend/RecommendService.ts +++ b/src/recommend/RecommendService.ts @@ -11,7 +11,6 @@ export interface CommandInfo { arguments: string[], reason: string; example: string; - isExecuted: boolean | null } export interface RecommendationQuery { @@ -21,86 +20,92 @@ export interface RecommendationQuery { export class RecommendService { - private static currentRecommends: Recommendation | null = null; + // private static currentRecommends: Recommendation | null = null; // the line on which the recommended scenarios are based - private static requestServiceLine: number = -1; - private static nextScenarios: Recommendation[] | null = null; + private static line: number = -1; + private static scenarios: Recommendation[] | null = null; constructor(private azService: AzService) { } static isReadyToRequestService(currentLine: number) { - return RecommendService.requestServiceLine != currentLine; // RecommendService.isReInit && + return RecommendService.scenarios == null || currentLine != RecommendService.line; // RecommendService.isReInit && } - static getCurrentRecommends(): Recommendation | null { - return RecommendService.currentRecommends; - } - - static setNextScenarios(nextScenarios: Recommendation[]) { - RecommendService.nextScenarios = nextScenarios; - } + // static getCurrentRecommends(): Recommendation | null { + // return RecommendService.currentRecommends; + // } - static setCurrentLine(line: number) { - return RecommendService.requestServiceLine = 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 setScenarios(nextScenarios: Recommendation[]) { + RecommendService.scenarios = nextScenarios; } - static initCurrentRecommends() { - RecommendService.currentRecommends = null; + static popScenarios() { + const scenarios = RecommendService.scenarios; + RecommendService.scenarios = null; + return scenarios; } - - 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 setLine(line: number) { + return RecommendService.line = line; } - 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); - } + // 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, isRequestService?: boolean): Promise { try { - if (RecommendService.nextScenarios == null || isRequestService) { + if (RecommendService.scenarios == null || isRequestService) { console.log('request recommendation service'); return this.azService.send({ request: 'recommendation', @@ -108,7 +113,7 @@ export class RecommendService { }, onCancel); } console.log('get next scenarios directly'); - return RecommendService.nextScenarios; + return RecommendService.scenarios; } catch (err) { console.error(err); return []; From fe0d9bc213077dd1e0f936878598487f84048bd1 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 6 Jun 2023 16:49:59 +0800 Subject: [PATCH 11/12] tmp --- service/azservice/recommend_tooling.py | 4 -- src/extension.ts | 37 ++++++++++++----- src/recommend/RecommendService.ts | 24 +++++------ src/recommend/parser.ts | 56 ++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/service/azservice/recommend_tooling.py b/service/azservice/recommend_tooling.py index 900087a..9fe5d24 100644 --- a/service/azservice/recommend_tooling.py +++ b/service/azservice/recommend_tooling.py @@ -115,12 +115,8 @@ def get_info_of_one_scenario(s, index): description = f'{idx_display} {scenario_desc} ({command_size})' next_command_set = [] - arg_index = 1 for next_command in s['nextCommandSet']: - command = 'az ' + next_command['command'] command_info = { - 'command': command, - 'arguments': next_command['arguments'], 'reason': next_command['reason'], 'example': next_command['example'] } diff --git a/src/extension.ts b/src/extension.ts index 6a2a9ac..67008b7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -74,7 +74,7 @@ class AzCompletionItemProvider implements CompletionItemProvider { // ready to request recommendation service const { commandListJson: commandListJson } = RecommendParser.parseLines(document, position); RecommendService.setLine(position.line); - this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested, true) + this.recommendService.getRecommendation(commandListJson, token.onCancellationRequested) .then(recommendations => { console.log('setNextScenarios recommendations'); RecommendService.setScenarios(recommendations); @@ -198,17 +198,36 @@ class AzRecommendationProvider implements CompletionItemProvider { let insertText = ""; 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 + '\n# example: ' + command.example + '\n'; + insertText += "\n# " + command.reason; if (scenario.executeIndex.indexOf(commandIndex) < 0) { - insertText += '# '; + insertText += ' (has been input)\n' + '# '; + } else{ + insertText += '\n'; } - insertText += command.command; - for (const arg of command.arguments) { - insertText += ' ' + arg + '$' + argIndex; - argIndex++; - } - 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); diff --git a/src/recommend/RecommendService.ts b/src/recommend/RecommendService.ts index 38db7d2..b4a1436 100644 --- a/src/recommend/RecommendService.ts +++ b/src/recommend/RecommendService.ts @@ -7,8 +7,6 @@ export interface Recommendation { } export interface CommandInfo { - command: string, - arguments: string[], reason: string; example: string; } @@ -30,13 +28,13 @@ export class RecommendService { } static isReadyToRequestService(currentLine: number) { - return RecommendService.scenarios == null || currentLine != RecommendService.line; // RecommendService.isReInit && + return RecommendService.scenarios == null || currentLine != RecommendService.line; } // static getCurrentRecommends(): Recommendation | null { // return RecommendService.currentRecommends; // } - + static setScenarios(nextScenarios: Recommendation[]) { RecommendService.scenarios = nextScenarios; } @@ -46,7 +44,7 @@ export class RecommendService { RecommendService.scenarios = null; return scenarios; } - + static setLine(line: number) { return RecommendService.line = line; } @@ -103,17 +101,13 @@ export class RecommendService { // RecommendService.currentRecommends.nextCommandSet = unusedCommands.concat(usedCommands); // } - async getRecommendation(commandList: string, onCancel: (handle: () => void) => void, isRequestService?: boolean): Promise { + async getRecommendation(commandList: string, onCancel: (handle: () => void) => void): Promise { try { - if (RecommendService.scenarios == null || isRequestService) { - console.log('request recommendation service'); - return this.azService.send({ - request: 'recommendation', - commandList: commandList - }, onCancel); - } - console.log('get next scenarios directly'); - return RecommendService.scenarios; + 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 index 6ddb59b..1d5a659 100644 --- a/src/recommend/parser.ts +++ b/src/recommend/parser.ts @@ -5,25 +5,65 @@ export class RecommendParser { private static readonly MAX_COMMAND_LIST_SIZE = 30; - static parseLines(document: TextDocument, position: Position): { executedCommand: Set, commandListJson: string } { + static parseLines(document: TextDocument, position: Position): { commandListJson: string } { const commandListArr: string[] = []; let line; - const executedCommand = new Set() 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) { - executedCommand.add('az ' + command.command) commandListArr.push(JSON.stringify(command)); } } if (commandListArr.length == 0) { - return { executedCommand: executedCommand, commandListJson: "" }; + return { commandListJson: "" }; } - const commandListJson = JSON.stringify(commandListArr) - return { executedCommand: executedCommand, commandListJson: 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; @@ -54,7 +94,7 @@ export class RecommendParser { if (subcommand.length == 0) { return null; } - + const command = { command: subcommand, arguments: args From 152ab560172cc7f92dd1faa14219d45a965b27c3 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Wed, 7 Jun 2023 18:25:59 +0800 Subject: [PATCH 12/12] modify comment --- src/extension.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 67008b7..558dbeb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -196,7 +196,8 @@ class AzRecommendationProvider implements CompletionItemProvider { const item = new CompletionItem(scenario.description, CompletionItemKind.Unit); let insertText = ""; - let argIndex = 1; + 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'; @@ -215,7 +216,7 @@ class AzRecommendationProvider implements CompletionItemProvider { const command = scenario.nextCommandSet[commandIndex]; insertText += "\n# " + command.reason; if (scenario.executeIndex.indexOf(commandIndex) < 0) { - insertText += ' (has been input)\n' + '# '; + insertText += commentTag + '\n# '; } else{ insertText += '\n'; }