Skip to content

Commit

Permalink
Merge pull request #8 from Avaiga/feature/#5-diagnostic-link-and-comp…
Browse files Browse the repository at this point in the history
…letion

#5 completion in quick input
  • Loading branch information
FredLL-Avaiga committed Jan 23, 2023
2 parents 8510fa3 + be544f6 commit b69c053
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 98 deletions.
8 changes: 6 additions & 2 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
"Enter a name for a new {0} entity.": "Enter a name for a new {0} entity.",
"new {0} name": "new {0} name",
"New module name": "New module name",
"Create a new function": "Create a new function",
"Create a new class": "Create a new class",
"function name": "function name",
"class name": "class name",
"No selected element.": "No selected element.",
"Select property for {0}.": "Select property for {0}.",
"No {0} entity in toml.": "No {0} entity in toml.",
"Select {0} entities for {1}.{2}": "Select {0} entities for {1}.{2}",
"Select Python module for {0}.{1}": "Select Python module for {0}.{1}",
"Enter Python module for {0}.{1}": "Enter Python module for {0}.{1}",
"Select Python {0} for {1}.{2}": "Select Python {0} for {1}.{2}",
"Enter Python {0} name for {1}.{2}": "Enter Python {0} name for {1}.{2}",
"Select value for {0}.{1}": "Select value for {0}.{1}",
"Enter value for {0}.{1}": "Enter value for {0}.{1}",
"Select data node": "Select data node",
Expand All @@ -27,6 +29,8 @@
"Cannot find file for Python {0}: '{1}'.": "Cannot find file for Python {0}: '{1}'.",
"Cannot find Python {0}: '{1}'.": "Cannot find Python {0}: '{1}'.",
"Main module file has been set up as {0} in Workspace settings": "Main module file has been set up as {0} in Workspace settings",
"Create a new function": "Create a new function",
"Create a new class": "Create a new class",
"Select file": "Select file",
"edit": "edit",
"New Property": "New Property",
Expand Down
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Taipy Studio Configuration Builder",
"description": "Visual Studio Code extension for Taipy: Configuration Builder",
"publisher": "Taipy",
"version": "0.2.2",
"version": "0.3.0",
"homepage": "https://github.com/Avaiga/taipy-studio-config.git",
"repository": {
"type": "git",
Expand Down Expand Up @@ -87,6 +87,11 @@
"command": "taipy.config.revealInExplorer",
"title": "%taipy.config.commands.taipy.config.revealInExplorer%",
"icon": "$(go-to-file)"
},
{
"command": "taipy.details.showLink",
"title": "%taipy.config.commands.taipy.details.showLink%",
"icon": "$(type-hierarchy)"
}
],
"customEditors": [
Expand Down Expand Up @@ -221,6 +226,10 @@
{
"command": "taipy.perspective.showFromDiagram",
"when": "webviewId == 'taipy.config.editor.diagram' && webviewSection == 'taipy.node'"
},
{
"command": "taipy.details.showLink",
"when": "webviewId == 'taipy-config-details' && webviewSection == 'taipy.property'"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"taipy.config.commands.taipy.config.task.create": "Taipy: Create New Task",
"taipy.config.commands.taipy.config.pipeline.create": "Taipy: Create New Pipeline",
"taipy.config.commands.taipy.config.scenario.create": "Taipy: Create New Scenario",
"taipy.config.commands.taipy.details.showLink": "Taipy: Show Source",
"taipy.config.commands.taipy.perspective.show": "Taipy: Show View",
"taipy.config.commands.taipy.perspective.showFromDiagram": "Taipy: Show View",
"taipy.config.commands.taipy.diagram.addNode": "Taipy: Add/Show node in active View",
Expand Down
4 changes: 4 additions & 0 deletions shared/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* specific language governing permissions and limitations under the License.
*/

import { Diagnostic } from "vscode";
import { DisplayModel } from "./diagram";

export const NoDetailsId = "NoDetails";
Expand All @@ -23,8 +24,11 @@ export interface DataNodeDetailsProps {
nodeType: string;
nodeName: string;
node: Record<string, string | string[]>;
diagnostics?: Record<string, WebDiag>;
}

export type WebDiag = {message?: string; severity?: number; link?: boolean; uri: string};

export const ConfigEditorId = "ConfigEditor";

export interface ConfigEditorProps {
Expand Down
5 changes: 5 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class Context {
commands.registerCommand(revealConfigNodeCmd, this.revealConfigNodeInEditors, this);
commands.registerCommand("taipy.perspective.show", this.showPerspective, this);
commands.registerCommand("taipy.perspective.showFromDiagram", this.showPerspectiveFromDiagram, this);
commands.registerCommand("taipy.details.showLink", this.showPropertyLink, this);
// Perspective Provider
vsContext.subscriptions.push(workspace.registerTextDocumentContentProvider(PERSPECTIVE_SCHEME, new PerspectiveContentProvider()));
// Create Tree Views
Expand Down Expand Up @@ -299,6 +300,10 @@ export class Context {
commands.executeCommand("vscode.openWith", getPerspectiveUri(Uri.parse(item.baseUri, true), item.perspective), ConfigEditorProvider.viewType);
}

private showPropertyLink(item: { baseUri: string; }) {
commands.executeCommand("vscode.open", Uri.parse(item.baseUri, true));
}

getSymbols(uri: string) {
return (uri && this.symbolsByUri[uri]) || [];
}
Expand Down
43 changes: 4 additions & 39 deletions src/providers/CompletionItemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,16 @@

import {
CancellationToken,
commands,
CompletionContext,
CompletionItem,
CompletionItemProvider,
CompletionTriggerKind,
DocumentSymbol,
l10n,
Position,
Range,
SnippetString,
SymbolKind,
TextDocument,
TextEdit,
Uri,
workspace,
} from "vscode";

Expand All @@ -37,7 +33,7 @@ import { calculatePythonSymbols, getEnum, getEnumProps, getProperties, isClass,
import { TAIPY_STUDIO_SETTINGS_NAME } from "../utils/constants";
import { getDescendantProperties, getPythonSuffix, getSectionName, getSymbol, getSymbolArrayValue, getUnsuffixedName } from "../utils/symbols";
import { getOriginalUri } from "./PerpectiveContentProvider";
import { getMainPythonUri } from "../utils/utils";
import { getCreateFunctionOrClassLabel, getModulesAndSymbols } from "../utils/pythonSymbols";

const nodeTypes = [DataNode, Task, Pipeline, Scenario];
const validLinks = nodeTypes.reduce((vl, nt) => {
Expand Down Expand Up @@ -144,44 +140,13 @@ export class ConfigCompletionItemProvider implements CompletionItemProvider<Comp

const getPythonSymbols = async (isFunction: boolean, lineText: string, position: Position) => {
// get python symbols in repository
const pythonUris = await workspace.findFiles("**/*.py");
const mainUri = await getMainPythonUri();
const symbolsByUri = await Promise.all(
pythonUris.map(
(uri) =>
new Promise<{ uri: Uri; symbols: DocumentSymbol[] }>((resolve, reject) => {
commands.executeCommand("vscode.executeDocumentSymbolProvider", uri).then((symbols: DocumentSymbol[]) => resolve({ uri, symbols }), reject);
})
)
);
const symbolsWithModule = [] as string[];
const modulesByUri = pythonUris.reduce((pv, uri) => {
const uriStr = uri.path;
if (uriStr === mainUri?.path) {
pv[uriStr] = "__main__";
} else {
const paths = workspace.asRelativePath(uri).split("/");
const file = paths.at(-1);
paths.pop();
const fileMod = `${file.split(".", 2)[0]}`;
const module = paths.length ? `${paths.join(".")}.${fileMod}` : fileMod;
pv[uriStr] = module;
}
return pv;
}, {} as Record<string, string>);
symbolsByUri.forEach((su) => {
Array.isArray(su.symbols) && su.symbols.forEach((symbol) => {
if ((isFunction && symbol.kind === SymbolKind.Function) || (!isFunction && symbol.kind === SymbolKind.Class)) {
symbolsWithModule.push(`${modulesByUri[su.uri.path]}.${symbol.name}`);
}
});
});
const cis = symbolsWithModule.map((v) => getCompletionItemInString(v, lineText, position, undefined, getPythonSuffix(isFunction)));
const [symbolsWithModule, modulesByUri] = await getModulesAndSymbols(isFunction);
const modules = Object.values(modulesByUri);
const cis = symbolsWithModule.map((v) => getCompletionItemInString(v, lineText, position, undefined, getPythonSuffix(isFunction)));
modules.push(l10n.t("New module name"));
cis.push(
getCompletionItemInString(
isFunction ? l10n.t("Create a new function") : l10n.t("Create a new class"),
getCreateFunctionOrClassLabel(isFunction),
lineText,
position,
[modules.length === 1 ? modules[0] : modules, isFunction ? l10n.t("function name") : l10n.t("class name")],
Expand Down
154 changes: 130 additions & 24 deletions src/providers/ConfigDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,32 @@ import {
WorkspaceEdit,
TextDocument,
l10n,
SymbolKind,
Position,
languages,
commands,
DocumentLink,
QuickPickItemKind,
} from "vscode";

import { getCspScriptSrc, getDefaultConfig, getNonce, joinPaths } from "../utils/utils";
import { DataNodeDetailsId, NoDetailsId, webviewsLibraryDir, webviewsLibraryName, containerId, DataNodeDetailsProps, NoDetailsProps } from "../../shared/views";
import { getCspScriptSrc, getDefaultConfig, getNonce, getPositionFragment, joinPaths } from "../utils/utils";
import {
DataNodeDetailsId,
NoDetailsId,
webviewsLibraryDir,
webviewsLibraryName,
containerId,
DataNodeDetailsProps,
NoDetailsProps,
WebDiag,
} from "../../shared/views";
import { Action, EditProperty, Refresh } from "../../shared/commands";
import { ViewMessage } from "../../shared/messages";
import { Context } from "../context";
import { getOriginalUri, isUriEqual } from "./PerpectiveContentProvider";
import { getEnum, getEnumProps, getProperties } from "../schema/validation";
import { getDescendantProperties, getNodeFromSymbol, getSectionName, getSymbol, getUnsuffixedName } from "../utils/symbols";
import { getEnum, getEnumProps, getProperties, calculatePythonSymbols, isFunction, isClass } from "../schema/validation";
import { getDescendantProperties, getNodeFromSymbol, getPythonSuffix, getSectionName, getSymbol, getUnsuffixedName } from "../utils/symbols";
import { getChildType } from "../../shared/childtype";
import { stringify } from "@iarna/toml";
import { getCreateFunctionOrClassLabel, getModulesAndSymbols } from "../utils/pythonSymbols";

export class ConfigDetailsView implements WebviewViewProvider {
private _view: WebviewView;
Expand All @@ -63,10 +75,41 @@ export class ConfigDetailsView implements WebviewViewProvider {
this.configUri = getOriginalUri(uri);
this.nodeType = nodeType;
this.nodeName = name;
this._view?.webview.postMessage({
viewId: DataNodeDetailsId,
props: { nodeType, nodeName: name, node } as DataNodeDetailsProps,
} as ViewMessage);
this.getNodeDiagnosticsAndLinks(node).then((diags) => {
this._view?.webview.postMessage({
viewId: DataNodeDetailsId,
props: { nodeType, nodeName: name, node, diagnostics: Object.keys(diags).length ? diags : undefined } as DataNodeDetailsProps,
} as ViewMessage);
});
}

private async getNodeDiagnosticsAndLinks(node: any) {
const diags = languages.getDiagnostics(this.configUri);
const links = (await commands.executeCommand("vscode.executeLinkProvider", this.configUri)) as DocumentLink[];
if (diags.length || links.length) {
const symbols = this.taipyContext.getSymbols(this.configUri.toString());
return Object.keys(node).reduce((obj, key) => {
const symbol = getSymbol(symbols, this.nodeType, this.nodeName, key);
if (symbol) {
const diag = diags.find((d) => !!d.range.intersection(symbol.range));
if (diag) {
obj[key] = {
message: diag.message,
severity: diag.severity,
uri: this.configUri.with({ fragment: getPositionFragment(diag.range.start) }).toString(),
};
}
const link = links.find((l) => !!l.range.intersection(symbol.range));
if (link) {
obj[key] = obj[key] || { uri: "" };
obj[key].uri = link.target?.toString();
obj[key].link = true;
}
}
return obj;
}, {} as Record<string, WebDiag>);
}
return {};
}

//called when a view first becomes visible
Expand Down Expand Up @@ -132,7 +175,7 @@ export class ConfigDetailsView implements WebviewViewProvider {
if (insert) {
const nameSymbol = getSymbol(symbols, nodeType, nodeName);
propertyRange = nameSymbol.range;
const currentProps = nameSymbol.children.map(s => s.name.toLowerCase());
const currentProps = nameSymbol.children.map((s) => s.name.toLowerCase());
const properties = (await getProperties(nodeType)).filter((p) => !currentProps.includes(p.toLowerCase()));
propertyName = await window.showQuickPick(properties, { canPickMany: false, title: l10n.t("Select property for {0}.", nodeType) });
if (!propertyName) {
Expand All @@ -147,7 +190,7 @@ export class ConfigDetailsView implements WebviewViewProvider {
const childType = getChildType(nodeType);
const values = ((propertyValue || []) as string[]).map((v) => getUnsuffixedName(v.toLowerCase()));
const childNames = getSymbol(symbols, childType).children.map(
s => ({ label: getSectionName(s.name), picked: values.includes(getUnsuffixedName(s.name.toLowerCase())) } as QuickPickItem)
(s) => ({ label: getSectionName(s.name), picked: values.includes(getUnsuffixedName(s.name.toLowerCase())) } as QuickPickItem)
);
if (!childNames.length) {
window.showInformationMessage(l10n.t("No {0} entity in toml.", childType));
Expand All @@ -162,18 +205,81 @@ export class ConfigDetailsView implements WebviewViewProvider {
}
newVal = res.map((q) => q.label);
} else {
const enumProps = await getEnumProps();
const enumProp = enumProps.find((p) => p.toLowerCase() === propertyName?.toLowerCase());
const res = enumProp
? await window.showQuickPick(
getEnum(enumProp).map((v) => ({ label: v, picked: v === propertyValue })),
{ canPickMany: false, title: l10n.t("Select value for {0}.{1}", nodeType, propertyName) }
)
: await window.showInputBox({ title: l10n.t("Enter value for {0}.{1}", nodeType, propertyName), value: propertyValue as string });
if (res === undefined) {
return;
await calculatePythonSymbols();
const isFn = isFunction(propertyName);
if (isFn || isClass(propertyName)) {
const [symbolsWithModule, modulesByUri] = await getModulesAndSymbols(isFn);
const currentModule = propertyValue && (propertyValue as string).split(".", 2)[0];
let resMod: string;
let resUri: string;
if (Object.keys(modulesByUri).length) {
const items = Object.entries(modulesByUri).map(
([uri, module]) => ({ label: module, picked: module === currentModule, uri: uri } as QuickPickItem & { uri?: string; create?: boolean })
);
items.push({ label: "", kind: QuickPickItemKind.Separator });
items.push({ label: l10n.t("New module name"), create: true });
const item = await window.showQuickPick(items, { canPickMany: false, title: l10n.t("Select Python module for {0}.{1}", nodeType, propertyName) });
if (!item) {
return;
}
if (!item.create) {
resMod = item.label;
resUri = item.uri;
}
}
if (!resMod) {
resMod = await window.showInputBox({ title: l10n.t("Enter Python module for {0}.{1}", nodeType, propertyName), value: currentModule });
if (resMod) {
resMod = resMod.trim();
resUri = Object.keys(modulesByUri).find((u) => modulesByUri[u] === resMod);
}
}
if (!resMod) {
return;
}
const symbols = symbolsWithModule.filter((s) => s.split(".", 2)[0] === resMod);
let resFunc: string;
if (symbols.length) {
const currentfunc = propertyValue && propertyValue.includes(".") && `${resMod}.${(propertyValue as string).split(".", 2)[1]}`;
const items = symbols.map((fn) => ({ label: fn, picked: fn === currentfunc } as QuickPickItem & { create?: boolean }));
items.push({ label: "", kind: QuickPickItemKind.Separator });
items.push({ label: getCreateFunctionOrClassLabel(isFn), create: true });
const item = await window.showQuickPick(items, {
canPickMany: false,
title: l10n.t("Select Python {0} for {1}.{2}", getPythonSuffix(isFn), nodeType, propertyName),
});
if (!item) {
return;
}
if (!item.create) {
resFunc = item.label;
}
}
if (!resFunc) {
resFunc = await window.showInputBox({
title: l10n.t("Enter Python {0} name for {1}.{2}", getPythonSuffix(isFn), nodeType, propertyName),
value: `${resMod}.${getPythonSuffix(isFn)}`,
valueSelection: [resMod.length + 1, resMod.length + 1 + getPythonSuffix(isFn).length],
});
}
if (!resFunc) {
return;
}
newVal = `${resFunc}:${getPythonSuffix(isFn)}`;
} else {
const enumProps = await getEnumProps();
const enumProp = enumProps.find((p) => p.toLowerCase() === propertyName?.toLowerCase());
const res = enumProp
? await window.showQuickPick(
getEnum(enumProp).map((v) => ({ label: v, picked: v === propertyValue })),
{ canPickMany: false, title: l10n.t("Select value for {0}.{1}", nodeType, propertyName) }
)
: await window.showInputBox({ title: l10n.t("Enter value for {0}.{1}", nodeType, propertyName), value: propertyValue as string });
if (res === undefined) {
return;
}
newVal = typeof res === "string" ? res : res.label;
}
newVal = typeof res === "string" ? res : res.label;
}
if (insert) {
propertyRange = propertyRange.with({ end: propertyRange.end.with({ character: 0 }) });
Expand All @@ -185,7 +291,7 @@ export class ConfigDetailsView implements WebviewViewProvider {
: TextEdit.replace(propertyRange, stringify.value(newVal).trim()),
]);
return workspace.applyEdit(we);
}
}

private getHtmlForWebview(webview: Webview) {
// Get the local path to main script run in the webview, then convert it to a uri we can use in the webview.
Expand Down
Loading

0 comments on commit b69c053

Please sign in to comment.