diff --git a/webgui-new/package-lock.json b/webgui-new/package-lock.json index e9de6792e..53fced3fd 100644 --- a/webgui-new/package-lock.json +++ b/webgui-new/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@codemirror/lang-cpp": "^6.0.2", + "@codemirror/lang-python": "^6.1.6", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.11.0", @@ -307,6 +308,18 @@ "@lezer/cpp": "^1.0.0" } }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", + "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, "node_modules/@codemirror/language": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", @@ -740,6 +753,16 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/python": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", + "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-alpha.114", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.114.tgz", diff --git a/webgui-new/package.json b/webgui-new/package.json index eb58ce287..d6ae483d7 100644 --- a/webgui-new/package.json +++ b/webgui-new/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@codemirror/lang-cpp": "^6.0.2", + "@codemirror/lang-python": "^6.1.6", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.11.0", diff --git a/webgui-new/src/components/codemirror-editor/codemirror-editor.tsx b/webgui-new/src/components/codemirror-editor/codemirror-editor.tsx index 4bcc0e928..987affe9c 100644 --- a/webgui-new/src/components/codemirror-editor/codemirror-editor.tsx +++ b/webgui-new/src/components/codemirror-editor/codemirror-editor.tsx @@ -1,10 +1,11 @@ -import ReactCodeMirror, { Decoration, EditorView, ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import ReactCodeMirror, { Decoration, EditorView, Extension, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { AccordionLabel } from 'enums/accordion-enum'; import { ThemeContext } from 'global-context/theme-context'; import React, { useContext, useRef, useState, useEffect, MouseEvent } from 'react'; -import { getCppAstNodeInfoByPosition, getCppReferenceTypes, getCppReferences } from 'service/cpp-service'; +import { createClient, getAstNodeInfoByPosition, getReferenceTypes, getReferences } from 'service/language-service'; import { AstNodeInfo, FileInfo, Position, Range } from '@thrift-generated'; import { cpp } from '@codemirror/lang-cpp'; +import { python } from '@codemirror/lang-python'; import { githubDark, githubLight } from '@uiw/codemirror-theme-github'; import { EditorContextMenu } from 'components/editor-context-menu/editor-context-menu'; import { FileName } from 'components/file-name/file-name'; @@ -75,6 +76,8 @@ export const CodeMirrorEditor = (): JSX.Element => { useEffect(() => { if(!editorRef.current || !editorRef.current.view) return; setHighlightRanges([]); + + createClient(appCtx.workspaceId, fileInfo?.type); }, [appCtx.workspaceId, fileInfo, fileContent]) const createHighlightDecoration = (view: EditorView, highlightPosition: HighlightPosition, highlightColor: string) => { @@ -100,10 +103,23 @@ export const CodeMirrorEditor = (): JSX.Element => { return Decoration.set(decorations, true); })} + const languageExtension = (fileType?: string) => + { + switch(fileType) + { + case "CPP": + return cpp(); + case "PY": + return python(); + default: + return null; + } + } + const updateHighlights = async (astNode : AstNodeInfo) => { - const refTypes = await getCppReferenceTypes(astNode.id as string) + const refTypes = await getReferenceTypes(astNode.id as string) if(visitedLastAstNode?.id !== astNode.id){ - const allReferences = await getCppReferences(astNode.id as string, refTypes.get('Usage') as number, []); + const allReferences = await getReferences(astNode.id as string, refTypes.get('Usage') as number, []); const referencesInFile = allReferences.filter(ref => ref.range?.file === fileInfo?.id); setHighlightRanges(referencesInFile.map(nodeInfo => { const startpos = nodeInfo?.range?.range?.startpos as { line: number, column: number }; @@ -174,7 +190,8 @@ export const CodeMirrorEditor = (): JSX.Element => { const astNodeInfo = fileInfo?.type === 'Unknown' ? null - : await getCppAstNodeInfoByPosition(fileInfo?.id as string, line.number, column); + : await getAstNodeInfoByPosition(fileInfo?.id as string, line.number, column); + if (astNodeInfo) { sendGAEvent({ event_action: 'click_on_word', @@ -318,7 +335,7 @@ export const CodeMirrorEditor = (): JSX.Element => { e) as Extension[]} theme={theme === 'dark' ? githubDark : githubLight} basicSetup={{ syntaxHighlighting: false, diff --git a/webgui-new/src/components/editor-context-menu/editor-context-menu.tsx b/webgui-new/src/components/editor-context-menu/editor-context-menu.tsx index 91ff29c6e..b97217d6a 100644 --- a/webgui-new/src/components/editor-context-menu/editor-context-menu.tsx +++ b/webgui-new/src/components/editor-context-menu/editor-context-menu.tsx @@ -3,12 +3,13 @@ import { IconButton, Menu, MenuItem, Modal, Tooltip } from '@mui/material'; import { ChevronRight, Close } from '@mui/icons-material'; import { TabName } from 'enums/tab-enum'; import { - getCppAstNodeInfo, - getCppDiagramTypes, - getCppDocumentation, - getCppReferenceTypes, - getCppReferences, -} from 'service/cpp-service'; + createClient, + getAstNodeInfo, + getDiagramTypes, + getDocumentation, + getReferenceTypes, + getReferences, +} from 'service/language-service'; import { getAsHTMLForNode } from 'service/cpp-reparse-service'; import { AstNodeInfo, Range } from '@thrift-generated'; import { convertSelectionRangeToString } from 'utils/utils'; @@ -52,10 +53,13 @@ export const EditorContextMenu = ({ useEffect(() => { if (!appCtx.languageNodeId) return; const init = async () => { - const initAstNodeInfo = await getCppAstNodeInfo(appCtx.languageNodeId); + const fileInfo = await getFileInfo(appCtx.projectFileId); + createClient(appCtx.workspaceId, fileInfo?.type); + + const initAstNodeInfo = await getAstNodeInfo(appCtx.languageNodeId); setAstNodeInfo(initAstNodeInfo); - const initDiagramTypes = await getCppDiagramTypes(appCtx.languageNodeId); + const initDiagramTypes = await getDiagramTypes(appCtx.languageNodeId); setDiagramTypes(initDiagramTypes); }; init(); @@ -69,7 +73,7 @@ export const EditorContextMenu = ({ }; const getDocs = async () => { - const initDocs = await getCppDocumentation(astNodeInfo?.id as string); + const initDocs = await getDocumentation(astNodeInfo?.id as string); const fileInfo = await getFileInfo(appCtx.projectFileId as string); const parser = new DOMParser(); const parsedHTML = parser.parseFromString(initDocs, 'text/html'); @@ -115,8 +119,8 @@ export const EditorContextMenu = ({ const jumpToDef = async () => { if (!astNodeInfo) return; - const refTypes = await getCppReferenceTypes(astNodeInfo.id as string); - const defRefs = await getCppReferences( + const refTypes = await getReferenceTypes(astNodeInfo.id as string); + const defRefs = await getReferences( astNodeInfo.id as string, refTypes.get('Definition') as number, astNodeInfo.tags ?? [] diff --git a/webgui-new/src/components/info-tree/info-tree.tsx b/webgui-new/src/components/info-tree/info-tree.tsx index 72b6387db..3c5ad3260 100644 --- a/webgui-new/src/components/info-tree/info-tree.tsx +++ b/webgui-new/src/components/info-tree/info-tree.tsx @@ -3,12 +3,13 @@ import { Code } from '@mui/icons-material'; import { ExpandMore, ChevronRight } from '@mui/icons-material'; import React, { SyntheticEvent, useContext, useEffect, useState } from 'react'; import { - getCppReferenceTypes, - getCppReferences, - getCppProperties, - getCppReferenceCount, - getCppAstNodeInfo, -} from 'service/cpp-service'; + createClient, + getReferenceTypes, + getReferences, + getProperties, + getReferenceCount, + getAstNodeInfo, +} from 'service/language-service'; import { AstNodeInfo, FileInfo, Range } from '@thrift-generated'; import { FileIcon, RefIcon } from 'components/custom-icon/custom-icon'; import { TabName } from 'enums/tab-enum'; @@ -39,20 +40,23 @@ export const InfoTree = (): JSX.Element => { if (!appCtx.languageNodeId) return; setLoadComplete(false); const init = async () => { - const initAstNodeInfo = await getCppAstNodeInfo(appCtx.languageNodeId as string); + const fileInfo = await getFileInfo(appCtx.projectFileId); + + createClient(appCtx.workspaceId, fileInfo?.type); + const initAstNodeInfo = await getAstNodeInfo(appCtx.languageNodeId as string); if (!initAstNodeInfo) return; - const initProps = await getCppProperties(initAstNodeInfo.id as string); - const initRefTypes = await getCppReferenceTypes(initAstNodeInfo.id as string); + const initProps = await getProperties(initAstNodeInfo.id as string); + const initRefTypes = await getReferenceTypes(initAstNodeInfo.id as string); const initRefCounts: typeof refCounts = new Map(); const initRefs: typeof refs = new Map(); const initFileUsages: typeof fileUsages = new Map(); for (const [rType, rId] of initRefTypes) { - const refCount = await getCppReferenceCount(initAstNodeInfo.id as string, rId); + const refCount = await getReferenceCount(initAstNodeInfo.id as string, rId); initRefCounts.set(rType, refCount); - const refsForType = await getCppReferences(initAstNodeInfo.id as string, rId, initAstNodeInfo.tags ?? []); + const refsForType = await getReferences(initAstNodeInfo.id as string, rId, initAstNodeInfo.tags ?? []); initRefs.set(rType, refsForType); if (rType === 'Caller' || rType === 'Usage') { diff --git a/webgui-new/src/service/language-service.ts b/webgui-new/src/service/language-service.ts new file mode 100644 index 000000000..4aa4d8eba --- /dev/null +++ b/webgui-new/src/service/language-service.ts @@ -0,0 +1,296 @@ +import thrift from 'thrift'; +import { LanguageService, FilePosition, Position } from '@thrift-generated'; +import { config } from './config'; +import { toast } from 'react-toastify'; + +let client: LanguageService.Client | undefined; +export const createClient = (workspace: string, fileType: string | undefined) => { + if (!config || !fileType) return; + + const service = () => + { + switch(fileType) + { + case "CPP": + return "CppService"; + case "PY": + return "PythonService"; + } + }; + + const connection = thrift.createXHRConnection(config.webserver_host, config.webserver_port, { + transport: thrift.TBufferedTransport, + protocol: thrift.TJSONProtocol, + https: config.webserver_https, + path: `${config.webserver_path}/${workspace}/${service()}`, + }); + client = thrift.createXHRClient(LanguageService, connection); + return client; +}; + +export const getFileTypes = async () => { + if (!client) { + return []; + } + return await client.getFileTypes(); +}; + +export const getFileDiagramTypes = async (fileId: string) => { + let resultMap = new Map(); + if (!client) { + return resultMap; + } + try { + resultMap = await client.getFileDiagramTypes(fileId); + } catch (e) { + console.error(e); + resultMap = new Map(); + } + return resultMap; +}; + +export const getFileDiagram = async (fileId: string, diagramId: number) => { + if (!client) { + return ''; + } + try { + return await client.getFileDiagram(fileId, diagramId); + } catch (e) { + toast.error('Could not display diagram.'); + console.error(e); + return ''; + } +}; + +export const getFileDiagramLegend = async (diagramId: number) => { + if (!client) { + return ''; + } + try { + return await client.getFileDiagramLegend(diagramId); + } catch (e) { + toast.error('Could not display diagram legend.'); + console.error(e); + return ''; + } +}; + +export const getDiagramTypes = async (astNodeId: string) => { + let resultMap = new Map(); + if (!client) { + return resultMap; + } + try { + resultMap = await client.getDiagramTypes(astNodeId); + } catch (e) { + console.error(e); + resultMap = new Map(); + } + return resultMap; +}; + +export const getDiagram = async (astNodeId: string, diagramId: number) => { + if (!client) { + return ''; + } + try { + return await client.getDiagram(astNodeId, diagramId); + } catch (e) { + toast.error('Could not display diagram.'); + console.error(e); + return ''; + } +}; + +export const getDiagramLegend = async (diagramId: number) => { + if (!client) { + return ''; + } + try { + return await client.getDiagramLegend(diagramId); + } catch (e) { + toast.error('Could not display diagram legend.'); + console.error(e); + return ''; + } +}; + +export const getFileReferenceTypes = async (fileId: string) => { + let resultMap = new Map(); + if (!client) { + return resultMap; + } + try { + resultMap = await client.getFileReferenceTypes(fileId); + } catch (e) { + console.error(e); + resultMap = new Map(); + } + return resultMap; +}; + +export const getFileReferences = async (fileId: string, referenceId: number) => { + if (!client) { + return []; + } + try { + return await client.getFileReferences(fileId, referenceId); + } catch (e) { + console.error(e); + return []; + } +}; + +export const getFileReferenceCount = async (fileId: string, referenceId: number) => { + if (!client) { + return 0; + } + try { + return await client.getFileReferenceCount(fileId, referenceId); + } catch (e) { + console.error(e); + return 0; + } +}; + +export const getReferenceTypes = async (astNodeId: string) => { + let resultMap = new Map(); + if (!client) { + return resultMap; + } + try { + resultMap = await client.getReferenceTypes(astNodeId); + } catch (e) { + console.error(e); + resultMap = new Map(); + } + return resultMap; +}; + +export const getReferences = async (astNodeId: string, referenceId: number, tags: string[]) => { + if (!client) { + return []; + } + try { + return await client.getReferences(astNodeId, referenceId, tags); + } catch (e) { + console.error(e); + return []; + } +}; + +export const getReferenceCount = async (astNodeId: string, referenceId: number) => { + if (!client) { + return 0; + } + try { + return await client.getReferenceCount(astNodeId, referenceId); + } catch (e) { + console.error(e); + return 0; + } +}; + +export const getReferencesInFile = async ( + astNodeId: string, + referenceId: number, + fileId: string, + tags: string[] +) => { + if (!client) { + return []; + } + try { + return await client.getReferencesInFile(astNodeId, referenceId, fileId, tags); + } catch (e) { + console.error(e); + return []; + } +}; + +export const getReferencesPage = async ( + astNodeId: string, + referenceId: number, + pageSize: number, + pageNo: number +) => { + if (!client) { + return []; + } + try { + return await client.getReferencesPage(astNodeId, referenceId, pageSize, pageNo); + } catch (e) { + console.error(e); + return []; + } +}; + +export const getSourceText = async (astNodeId: string) => { + if (!client) { + return ''; + } + try { + return await client.getSourceText(astNodeId); + } catch (e) { + console.error(e); + return ''; + } +}; + +export const getProperties = async (astNodeId: string) => { + let resultMap = new Map(); + if (!client) { + return resultMap; + } + try { + resultMap = await client.getProperties(astNodeId); + } catch (e) { + console.error(e); + resultMap = new Map(); + } + return resultMap; +}; + +export const getDocumentation = async (astNodeId: string) => { + if (!client) { + return ''; + } + try { + return await client.getDocumentation(astNodeId); + } catch (e) { + toast.error('Could not get documentation about this AST node.'); + console.error(e); + return ''; + } +}; + +export const getAstNodeInfo = async (astNodeId: string) => { + if (!client) { + return; + } + try { + return await client.getAstNodeInfo(astNodeId); + } catch (e) { + console.error(e); + return; + } +}; + +export const getAstNodeInfoByPosition = async (fileId: string, line: number, column: number) => { + if (!client) { + return; + } + try { + return await client.getAstNodeInfoByPosition( + new FilePosition({ + file: fileId, + pos: new Position({ + line, + column, + }), + }) + ); + } catch (e) { + return; + } +};