├── .env ├── .vscodeignore ├── react-help-app ├── .env ├── build.sh ├── src │ ├── react-app-env.d.ts │ ├── index.css │ ├── reportWebVitals.ts │ ├── index.tsx │ ├── components │ │ └── Accordion.tsx │ └── app.tsx ├── public │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── tailwind.config.js ├── tslint.json ├── scripts │ └── build-non-split.js ├── tsconfig.json └── package.json ├── assets └── icons │ └── trelent.woff ├── grammars ├── tree-sitter.wasm ├── tree-sitter-java.wasm ├── tree-sitter-python.wasm ├── tree-sitter-c_sharp.wasm ├── tree-sitter-javascript.wasm └── tree-sitter-typescript.wasm ├── images ├── trelent-icon.png └── trelent-example.gif ├── .gitignore ├── installDependencies.sh ├── publish.sh ├── src ├── test │ ├── suite │ │ ├── parser │ │ │ ├── parser-test-files │ │ │ │ ├── test.py │ │ │ │ ├── test.cs │ │ │ │ ├── test.java │ │ │ │ ├── test.ts │ │ │ │ └── test.js │ │ │ ├── csharp.test.ts │ │ │ ├── java.test.ts │ │ │ ├── python.test.ts │ │ │ ├── javascript.test.ts │ │ │ └── typescript.test.ts │ │ ├── index.ts │ │ └── autodoc │ │ │ └── ModifyRanges.test.ts │ └── runTest.ts ├── services │ ├── dev.ts │ ├── authenticate.ts │ ├── progress.ts │ ├── uri.ts │ ├── telemetry.ts │ ├── billing.ts │ ├── docs.ts │ └── codeParser.ts ├── helpers │ ├── token.ts │ ├── modules.ts │ ├── langs.ts │ ├── util.ts │ └── webview.ts ├── parser │ ├── types.ts │ ├── util.ts │ ├── parser.ts │ ├── queries.ts │ └── langs │ │ ├── java.ts │ │ ├── javascript.ts │ │ ├── typescript.ts │ │ ├── python.ts │ │ └── csharp.ts ├── extension.ts ├── autodoc │ ├── DocstringCodelens.ts │ ├── DocstringDecorator.ts │ ├── changeDetection.ts │ └── DocstringInsertService.ts └── api │ ├── conf.ts │ └── api.ts ├── tsconfig.extension.json ├── .github └── workflows │ └── workflows.yaml ├── .vscode └── launch.json ├── LICENSE ├── README.md └── package.json /.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=./ -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | build/src/test/** 4 | -------------------------------------------------------------------------------- /react-help-app/.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=./ 2 | BUILD_PATH=../build/react-help-app -------------------------------------------------------------------------------- /react-help-app/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | node ./scripts/build-non-split.js -------------------------------------------------------------------------------- /react-help-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /react-help-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /assets/icons/trelent.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/assets/icons/trelent.woff -------------------------------------------------------------------------------- /grammars/tree-sitter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter.wasm -------------------------------------------------------------------------------- /images/trelent-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/images/trelent-icon.png -------------------------------------------------------------------------------- /images/trelent-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/images/trelent-example.gif -------------------------------------------------------------------------------- /grammars/tree-sitter-java.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter-java.wasm -------------------------------------------------------------------------------- /grammars/tree-sitter-python.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter-python.wasm -------------------------------------------------------------------------------- /grammars/tree-sitter-c_sharp.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter-c_sharp.wasm -------------------------------------------------------------------------------- /react-help-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /grammars/tree-sitter-javascript.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter-javascript.wasm -------------------------------------------------------------------------------- /grammars/tree-sitter-typescript.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trelent/Trelent-VSCode-Extension/HEAD/grammars/tree-sitter-typescript.wasm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | node_modules 3 | build 4 | .vscode 5 | *.vsix 6 | src/welcome-page-out 7 | .DS_Store 8 | yarn-error.log 9 | .vscode-test 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /react-help-app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | -------------------------------------------------------------------------------- /installDependencies.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | cd "${SCRIPT_DIR}" 4 | yarn 5 | cd "./react-help-app" 6 | yarn -------------------------------------------------------------------------------- /react-help-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /react-help-app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "src/**/*.ts"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Install dependencies and build our project 4 | ./installDependencies.sh 5 | ./buildAll.sh 6 | 7 | # Publish the extension. By default, VSCE will use the $VSCE_PAT 8 | # env var as the publisher token. 9 | yarn run vsce publish --yarn -------------------------------------------------------------------------------- /src/test/suite/parser/parser-test-files/test.py: -------------------------------------------------------------------------------- 1 | 2 | def foo(): 3 | """ 4 | a 5 | """ 6 | def foobar(): 7 | """ 8 | b 9 | """ 10 | return foo() + bar() 11 | return "a" 12 | 13 | 14 | def foo(): 15 | def foobar(): 16 | return "b" 17 | return "a" -------------------------------------------------------------------------------- /react-help-app/scripts/build-non-split.js: -------------------------------------------------------------------------------- 1 | const rewire = require("rewire"); 2 | const defaults = rewire("react-scripts/scripts/build.js"); 3 | let config = defaults.__get__("config"); 4 | 5 | config.optimization.splitChunks = { 6 | cacheGroups: { 7 | default: false, 8 | }, 9 | }; 10 | 11 | config.optimization.runtimeChunk = false; -------------------------------------------------------------------------------- /tsconfig.extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "build", 6 | "lib": ["es6", "dom", "es2019"], 7 | "sourceMap": true, 8 | "rootDir": ".", 9 | "strict": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", ".vscode-test", "src/test/suite/parser/parser-test-files"] 13 | } 14 | -------------------------------------------------------------------------------- /src/test/suite/parser/parser-test-files/test.cs: -------------------------------------------------------------------------------- 1 | 2 | class Program { 3 | //Documented 4 | public Program(){ 5 | 6 | } 7 | 8 | //Documented 9 | public void thing(){ 10 | 11 | //Documented 12 | void localFunction(){ 13 | 14 | } 15 | } 16 | } 17 | 18 | class Program2{ 19 | 20 | public Program2(){ 21 | 22 | } 23 | 24 | public void thing(){ 25 | 26 | void localFunction(){ 27 | 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /react-help-app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/test/suite/parser/parser-test-files/test.java: -------------------------------------------------------------------------------- 1 | 2 | public class test { 3 | 4 | /** 5 | * The testjava function tests the java implementation of the function. 6 | 7 | * 8 | * 9 | * @return A void 10 | * 11 | * @docauthor Trelent 12 | */ 13 | public testjava(){ 14 | 15 | } 16 | 17 | /* 18 | * this is a docstring 19 | */ 20 | public void test() { 21 | 22 | } 23 | 24 | public testjava(){ 25 | 26 | } 27 | 28 | public void test() { 29 | 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /react-help-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /src/services/dev.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class DevService { 4 | constructor(context: vscode.ExtensionContext) { 5 | context.subscriptions.push( 6 | vscode.commands.registerCommand("trelent.dev.clearTrelentContext", () => { 7 | this.clearTrelentContext(context); 8 | }) 9 | ); 10 | } 11 | 12 | public clearTrelentContext(context: vscode.ExtensionContext) { 13 | context.globalState.update("Trelent.trelent", undefined); 14 | vscode.window.showInformationMessage("Trelent context cleared!"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/workflows.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: pull_request 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: [macos-latest] 8 | runs-on: ${{ matrix.os }} 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Install Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18.x 16 | - run: ./installDependencies.sh 17 | - run: ./buildAll.sh 18 | - run: xvfb-run -a yarn test 19 | if: runner.os == 'Linux' 20 | - run: yarn test 21 | if: runner.os != 'Linux' 22 | -------------------------------------------------------------------------------- /src/helpers/token.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const KEY = "access_token"; 4 | 5 | export class TokenManager { 6 | static setToken(context: vscode.ExtensionContext, token: string) { 7 | return context.secrets.store(KEY, token); 8 | } 9 | 10 | static async getToken(context: vscode.ExtensionContext): Promise { 11 | try { 12 | let token = await context.secrets.get(KEY); 13 | if(token === undefined) { 14 | token = ""; 15 | } 16 | 17 | return token; 18 | } 19 | catch(error) { 20 | console.error(error); 21 | return ""; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /react-help-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-help-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./app"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById("root") as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/parser/types.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxNode } from "web-tree-sitter"; 2 | 3 | export type Function = { 4 | body: string; 5 | definition: string; 6 | definition_line: number; 7 | docstring: string | undefined; 8 | docstring_offset: number; 9 | docstring_range: number[] | undefined; 10 | name: string; 11 | params: string[]; 12 | range: number[]; // [[start col, start line], [end col, end line]] 13 | text: string; 14 | levenshteinDistanceSum?: number; 15 | }; 16 | 17 | export type QueryGroup = { 18 | defNode: SyntaxNode; 19 | nameNode: SyntaxNode; 20 | paramsNode: SyntaxNode; 21 | bodyNode: SyntaxNode; 22 | docNodes: SyntaxNode[]; 23 | }; 24 | 25 | export enum DocTag { 26 | AUTO, 27 | IGNORE, 28 | HIGHLIGHT, 29 | NONE, 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "args": [ 6 | "--extensionDevelopmentPath=${workspaceFolder}" 7 | ], 8 | "name": "Launch Extension", 9 | "outFiles": [ 10 | "${workspaceFolder}/build/src/**/*.js" 11 | ], 12 | "request": "launch", 13 | "type": "extensionHost" 14 | }, 15 | { 16 | "name": "Extension Tests", 17 | "type": "extensionHost", 18 | "request": "launch", 19 | "runtimeExecutable": "${execPath}", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/build/src/test/suite/index" 23 | ], 24 | "outFiles": ["${workspaceFolder}/build/src/test/**/**/*.js"] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 10 | 11 | // The path to the extension test runner script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error(err); 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); -------------------------------------------------------------------------------- /src/helpers/modules.ts: -------------------------------------------------------------------------------- 1 | import { isLanguageSupported } from "../helpers/langs"; 2 | 3 | const MODULE_REGEX: { [key: string]: RegExp[] } = { 4 | csharp: [/(?:using)\s[\S]*;{1}/gm], 5 | java: [/(?:import)\s[\S]*;{1}/gm], 6 | javascript: [/(?:import).*;?/gm], 7 | python: [/(?:import).*\n/gm], 8 | }; 9 | 10 | export class ModuleGatherer { 11 | static getModules( 12 | document: string, 13 | language: string 14 | ): Promise { 15 | return new Promise((resolve, reject) => { 16 | if (!isLanguageSupported(language)) { 17 | return reject("Unsupported language"); 18 | } 19 | 20 | const matches = document.match(MODULE_REGEX[language][0]); 21 | if (matches === null) { 22 | return resolve(null); 23 | } 24 | 25 | var moduleText = ""; 26 | matches.forEach((match) => { 27 | moduleText += match.replace("\n", "") + "\n"; 28 | }); 29 | 30 | return resolve(moduleText); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/langs.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | const supportedLangs = ["csharp", "java", "javascript", "python", "typescript"]; 4 | 5 | const getExtensionType = (fileName: string) => { 6 | let ext = path.extname(fileName); 7 | if (ext === ".git") { 8 | let extParts = fileName.split("."); 9 | ext = extParts[extParts.length - 2]; 10 | } 11 | 12 | return ext; 13 | }; 14 | 15 | export function isLanguageSupported(languageId: string): boolean { 16 | return supportedLangs.includes(languageId); 17 | } 18 | 19 | export function getLanguageName(languageId: string, fileName?: string): string { 20 | if (!fileName) { 21 | return languageId; 22 | } 23 | 24 | const ext = getExtensionType(fileName); 25 | switch (ext) { 26 | case "cs": 27 | return "csharp"; 28 | case "java": 29 | return "java"; 30 | case "js": 31 | return "javascript"; 32 | case "py": 33 | return "python"; 34 | case "ts": 35 | return "typescript"; 36 | default: 37 | return languageId; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /react-help-app/src/components/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | interface AccordionProps { 4 | title: string; 5 | content: JSX.Element; 6 | defaultOpen?: boolean; 7 | } 8 | 9 | const Accordion: React.FC = ({ 10 | title, 11 | content, 12 | defaultOpen, 13 | }) => { 14 | const [isOpen, setIsOpen] = useState(defaultOpen || false); 15 | 16 | return ( 17 |
18 |
setIsOpen(!isOpen)} 21 | > 22 |

{title}

23 | 28 | ▼ 29 | 30 |
31 | {isOpen && ( 32 |
{content}
33 | )} 34 |
35 | ); 36 | }; 37 | 38 | export default Accordion; 39 | -------------------------------------------------------------------------------- /react-help-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "Trelent", 3 | "name": "trelent", 4 | "displayName": "Trelent - AI Docstrings on Demand", 5 | "description": "We write and maintain docstrings for your code automatically!", 6 | "version": "2.0.0-rc1", 7 | "repository": "https://github.com/Trelent/Trelent-VSCode-Extension", 8 | "icon": "../images/trelent-icon.png", 9 | "license": "We use the “Commons Clause” License Condition v1.0 with the MIT License.", 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "./build.sh", 13 | "eject": "react-scripts eject" 14 | }, 15 | "dependencies": { 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-syntax-highlighter": "^15.5.0", 19 | "web-vitals": "^3.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.25", 23 | "@types/react-dom": "^18.0.9", 24 | "@types/react-syntax-highlighter": "^15.5.6", 25 | "autoprefixer": "^10.4.13", 26 | "create-react-app": "^5.0.1", 27 | "postcss": "^8.4.19", 28 | "react-scripts": "^5.0.1", 29 | "rewire": "^6.0.0", 30 | "tailwindcss": "^3.2.4" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import * as glob from "glob"; 4 | import { createCodeParserService } from "../../services/codeParser"; 5 | import { extensions } from "vscode"; 6 | import { getTelemetryService } from "../../services/telemetry"; 7 | 8 | export function run(): Promise { 9 | // Create the mocha test 10 | const mocha = new Mocha({ 11 | ui: "tdd", 12 | color: true, 13 | }); 14 | 15 | const testsRoot = path.resolve(__dirname, ".."); 16 | 17 | return new Promise((c, e) => { 18 | glob("**/**.test.js", { cwd: testsRoot }, async (err, files) => { 19 | if (err) { 20 | return e(err); 21 | } 22 | 23 | // Add files to the test suite 24 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 25 | await extensions.getExtension("Trelent.trelent")?.activate(); 26 | 27 | try { 28 | mocha.timeout(0); 29 | // Run the mocha test 30 | mocha.run((failures) => { 31 | if (failures > 0) { 32 | e(new Error(`${failures} tests failed.`)); 33 | } else { 34 | c(); 35 | } 36 | }); 37 | } catch (err) { 38 | e(err); 39 | } 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/parser/util.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "web-tree-sitter"; 2 | 3 | export const getMultilineStringIndex = ( 4 | text: string, 5 | col: number, 6 | row: number 7 | ) => { 8 | let split = text.split("\n"); 9 | let index = 0; 10 | for (let i = 0; i < row; i++) { 11 | index += split[i].length + 1; 12 | } 13 | index += col; 14 | 15 | //console.log("TRELENT: Multiline string index", index); 16 | return index; 17 | }; 18 | 19 | export const getParams = (paramsText: string): string[] => { 20 | // remove the first and last parenthesis, and early return if empty 21 | paramsText = paramsText.substring(1, paramsText.length - 1); 22 | if (paramsText.length == 0) return []; 23 | 24 | // Split on commas 25 | let params = paramsText.split(",").map((param) => { 26 | return param.trim(); 27 | }); 28 | 29 | // Remove default values 30 | params = params.map((param) => { 31 | if (param.includes("=")) { 32 | return param.split("=")[0].trim(); 33 | } 34 | return param; 35 | }); 36 | 37 | return params; 38 | }; 39 | 40 | export const getTextBetweenPoints = ( 41 | text: string, 42 | start: Point, 43 | end: Point 44 | ) => { 45 | let startIndex = getMultilineStringIndex(text, start.column, start.row); 46 | let endIndex = getMultilineStringIndex(text, end.column, end.row); 47 | 48 | let result = text.substring(startIndex, endIndex); 49 | 50 | return result; 51 | }; 52 | -------------------------------------------------------------------------------- /src/test/suite/parser/parser-test-files/test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The x function does... 3 | * 4 | * 5 | * 6 | * @return A function 7 | * 8 | * @docauthor Trelent 9 | */ 10 | let a = (): void => {}; 11 | 12 | /** 13 | * The x function does... 14 | * 15 | * 16 | * 17 | * @return A function 18 | * 19 | * @docauthor Trelent 20 | */ 21 | let b = function (): void {}; 22 | 23 | let thinga = { 24 | /** 25 | * The val function is used to get the value of a variable. 26 | * 27 | * 28 | * 29 | * @return The value of the variable 30 | * 31 | * @docauthor Trelent 32 | */ 33 | val: function (): void {}, 34 | 35 | /** 36 | * The test function . 37 | * 38 | * 39 | * 40 | * @return Nothing 41 | * 42 | * @docauthor Trelent 43 | */ 44 | d(): void {}, 45 | }; 46 | 47 | /** 48 | * The x function returns a generator. 49 | * 50 | * 51 | * 52 | * @return A generator object 53 | * 54 | * @docauthor Trelent 55 | */ 56 | function* e(): any { 57 | yield 1; 58 | } 59 | 60 | /** 61 | * The x function does... 62 | * 63 | * 64 | * 65 | * @return A function 66 | * 67 | * @docauthor Trelent 68 | */ 69 | function f(): void {} 70 | 71 | let g = (): void => {}; 72 | 73 | let h = function (): void {}; 74 | 75 | let i = { 76 | val: function (): void {}, 77 | 78 | test(): void {}, 79 | }; 80 | 81 | function* j(): any { 82 | yield 1; 83 | } 84 | 85 | function k(): void {} 86 | -------------------------------------------------------------------------------- /src/services/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url"; 2 | import * as vscode from "vscode"; 3 | import { LOGIN_URL, LOGOUT_URL } from "../api/conf"; 4 | import { TokenManager } from "../helpers/token"; 5 | import { TelemetryService } from "./telemetry"; 6 | 7 | export class AuthenticationService { 8 | constructor(context: vscode.ExtensionContext, telemetry: TelemetryService) { 9 | // Register our auth-reated commands 10 | let loginCmd = vscode.commands.registerCommand("trelent.login", () => { 11 | try { 12 | this.authenticate("login"); 13 | } catch (err) { 14 | console.log(err); 15 | } 16 | }); 17 | 18 | let logoutCmd = vscode.commands.registerCommand("trelent.logout", () => { 19 | try { 20 | this.logout(context); 21 | } catch (err) { 22 | console.log(err); 23 | } 24 | }); 25 | 26 | let signupCmd = vscode.commands.registerCommand("trelent.signup", () => { 27 | try { 28 | this.authenticate("signup"); 29 | } catch (err) { 30 | // Something went wrong client-side 31 | telemetry.trackEvent("Client Error", { 32 | error: err, 33 | time: new Date().toISOString(), 34 | }); 35 | console.log(err); 36 | } 37 | }); 38 | 39 | context.subscriptions.push(loginCmd, logoutCmd, signupCmd); 40 | } 41 | 42 | public authenticate(mode: string): void { 43 | vscode.env.openExternal( 44 | vscode.Uri.parse( 45 | `${LOGIN_URL}?mode=${mode}&scheme=${vscode.env.uriScheme}` 46 | ) 47 | ); 48 | } 49 | 50 | public logout(context: vscode.ExtensionContext): void { 51 | TokenManager.setToken(context, "").then(() => { 52 | vscode.env.openExternal( 53 | vscode.Uri.parse(`${LOGOUT_URL}?scheme=${vscode.env.uriScheme}`) 54 | ); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/suite/parser/parser-test-files/test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The x function does... 4 | * 5 | * 6 | * 7 | * @return A function 8 | * 9 | * @docauthor Trelent 10 | */ 11 | let x = () => {} 12 | 13 | 14 | /** 15 | * The x function does... 16 | * 17 | * 18 | * 19 | * @return A function 20 | * 21 | * @docauthor Trelent 22 | */ 23 | let x = function(){ 24 | 25 | } 26 | 27 | let thing = { 28 | 29 | /** 30 | * The val function is used to get the value of a variable. 31 | * 32 | * 33 | * 34 | * @return The value of the variable 35 | * 36 | * @docauthor Trelent 37 | */ 38 | val: function(){ 39 | /** 40 | * The val function is used to get the value of a variable. 41 | * 42 | * 43 | * 44 | * @return The value of the variable 45 | * 46 | * @docauthor Trelent 47 | */ 48 | let func = () => { 49 | 50 | } 51 | }, 52 | 53 | 54 | /** 55 | * The test function . 56 | * 57 | * 58 | * 59 | * @return Nothing 60 | * 61 | * @docauthor Trelent 62 | */ 63 | test(){ 64 | 65 | } 66 | } 67 | 68 | /** 69 | * The x function returns a generator. 70 | * 71 | * 72 | * 73 | * @return A generator object 74 | * 75 | * @docauthor Trelent 76 | */ 77 | function* x(){ 78 | yield 1 79 | } 80 | 81 | /** 82 | * The x function does... 83 | * 84 | * 85 | * 86 | * @return A function 87 | * 88 | * @docauthor Trelent 89 | */ 90 | function x(){ 91 | 92 | } 93 | 94 | 95 | let x = () => {} 96 | 97 | let x = function(){ 98 | 99 | } 100 | 101 | let thing2 = { 102 | 103 | val: function(){ 104 | let func = () => { 105 | 106 | } 107 | }, 108 | 109 | 110 | test(){ 111 | 112 | } 113 | } 114 | 115 | function* x(){ 116 | yield 1 117 | } 118 | 119 | function x(){ 120 | 121 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | “Commons Clause” License Condition v1.0 2 | 3 | The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. 4 | 5 | Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. 6 | 7 | For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. 8 | 9 | Software: trelent 10 | 11 | License: MIT License 12 | 13 | Licensor: Trelent Inc. 14 | 15 | 16 | MIT License 17 | 18 | Copyright (c) 2021 Trelent Inc. 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. -------------------------------------------------------------------------------- /src/test/suite/parser/csharp.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import "mocha"; 5 | import { ExtensionContext } from "vscode"; 6 | import { 7 | CodeParserService, 8 | getCodeParserService, 9 | } from "../../../services/codeParser"; 10 | import { Function } from "../../../parser/types"; 11 | 12 | //TODO: Make test more robust, and less static (Right now it assumes the test files will be in order of 13 | //Documented functions, then undocumented functions, but should work regardless of the order) 14 | 15 | const LANG = "csharp"; 16 | const EXPECTED_FUNCTIONS = 6; 17 | const EXPECTED_DOCUMENTED = 3; 18 | const EXPECTED_UNDOCUMENTED = 3; 19 | const EXTENSION = ".cs"; 20 | 21 | suite("C# parser tests", () => { 22 | let extensionContext: ExtensionContext; 23 | let codeParserService: CodeParserService; 24 | let codeFile: string; 25 | let functions: Function[]; 26 | suiteSetup(async () => { 27 | // Trigger extension activation and grab the context as some tests depend on it 28 | extensionContext = (global as any).testExtensionContext; 29 | codeParserService = getCodeParserService(); 30 | 31 | let filePath: string = extensionContext.asAbsolutePath( 32 | path.join( 33 | "build", 34 | "src", 35 | "test", 36 | "suite", 37 | "parser", 38 | "parser-test-files", 39 | "test" + EXTENSION 40 | ) 41 | ); 42 | codeFile = fs.readFileSync(filePath, "utf8"); 43 | await codeParserService.safeParseText(codeFile, LANG); 44 | functions = codeParserService.getFunctions(); 45 | }); 46 | 47 | test("Parsing documented functions correctly", async () => { 48 | assert.strictEqual(functions.length, EXPECTED_FUNCTIONS); 49 | }); 50 | 51 | test("Reporting correct amount of documented & undocumented functions", async () => { 52 | for (let i = 0; i < EXPECTED_DOCUMENTED; i++) { 53 | assert.notStrictEqual(functions[i].docstring, undefined); 54 | } 55 | 56 | for ( 57 | let i = EXPECTED_DOCUMENTED; 58 | i < EXPECTED_UNDOCUMENTED + EXPECTED_UNDOCUMENTED; 59 | i++ 60 | ) { 61 | assert.strictEqual(functions[i].docstring, undefined); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/test/suite/parser/java.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import "mocha"; 5 | import { ExtensionContext } from "vscode"; 6 | import { 7 | CodeParserService, 8 | getCodeParserService, 9 | } from "../../../services/codeParser"; 10 | import { Function } from "../../../parser/types"; 11 | 12 | //TODO: Make test more robust, and less static (Right now it assumes the test files will be in order of 13 | //Documented functions, then undocumented functions, but should work regardless of the order) 14 | 15 | const LANG = "java"; 16 | const EXPECTED_FUNCTIONS = 4; 17 | const EXPECTED_DOCUMENTED = 2; 18 | const EXPECTED_UNDOCUMENTED = 2; 19 | const EXTENSION = ".java"; 20 | 21 | suite("Java parser tests", () => { 22 | let extensionContext: ExtensionContext; 23 | let codeParserService: CodeParserService; 24 | let codeFile: string; 25 | let functions: Function[]; 26 | suiteSetup(async () => { 27 | // Trigger extension activation and grab the context as some tests depend on it 28 | extensionContext = (global as any).testExtensionContext; 29 | codeParserService = getCodeParserService(); 30 | 31 | let filePath: string = extensionContext.asAbsolutePath( 32 | path.join( 33 | "build", 34 | "src", 35 | "test", 36 | "suite", 37 | "parser", 38 | "parser-test-files", 39 | "test" + EXTENSION 40 | ) 41 | ); 42 | codeFile = fs.readFileSync(filePath, "utf8"); 43 | await codeParserService.safeParseText(codeFile, LANG); 44 | functions = codeParserService.getFunctions(); 45 | }); 46 | 47 | test("Parsing documented functions correctly", async () => { 48 | assert.strictEqual(functions.length, EXPECTED_FUNCTIONS); 49 | }); 50 | 51 | test("Reporting correct amount of documented & undocumented functions", async () => { 52 | for (let i = 0; i < EXPECTED_DOCUMENTED; i++) { 53 | assert.notStrictEqual(functions[i].docstring, undefined); 54 | } 55 | 56 | for ( 57 | let i = EXPECTED_DOCUMENTED; 58 | i < EXPECTED_UNDOCUMENTED + EXPECTED_UNDOCUMENTED; 59 | i++ 60 | ) { 61 | assert.strictEqual(functions[i].docstring, undefined); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/test/suite/parser/python.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import "mocha"; 5 | import { ExtensionContext, extensions } from "vscode"; 6 | import { 7 | CodeParserService, 8 | createCodeParserService, 9 | getCodeParserService, 10 | } from "../../../services/codeParser"; 11 | import { Function } from "../../../parser/types"; 12 | import { getTelemetryService } from "../../../services/telemetry"; 13 | 14 | //TODO: Make test more robust, and less static (Right now it assumes the test files will be in order of 15 | //Documented functions, then undocumented functions, but should work regardless of the order) 16 | 17 | const LANG = "python"; 18 | const EXPECTED_FUNCTIONS = 4; 19 | const EXPECTED_DOCUMENTED = 2; 20 | const EXPECTED_UNDOCUMENTED = 2; 21 | const EXTENSION = ".py"; 22 | 23 | suite("Python parser tests", () => { 24 | let extensionContext: ExtensionContext; 25 | let codeParserService: CodeParserService; 26 | let codeFile: string; 27 | let functions: Function[]; 28 | suiteSetup(async () => { 29 | // Trigger extension activation and grab the context as some tests depend on it 30 | extensionContext = (global as any).testExtensionContext; 31 | codeParserService = getCodeParserService(); 32 | 33 | let filePath: string = extensionContext.asAbsolutePath( 34 | path.join( 35 | "build", 36 | "src", 37 | "test", 38 | "suite", 39 | "parser", 40 | "parser-test-files", 41 | "test" + EXTENSION 42 | ) 43 | ); 44 | codeFile = fs.readFileSync(filePath, "utf8"); 45 | await codeParserService.safeParseText(codeFile, LANG); 46 | functions = codeParserService.getFunctions(); 47 | }); 48 | 49 | test("Parsing documented functions correctly", async () => { 50 | assert.strictEqual(functions.length, EXPECTED_FUNCTIONS); 51 | }); 52 | 53 | test("Reporting correct amount of documented & undocumented functions", async () => { 54 | for (let i = 0; i < EXPECTED_DOCUMENTED; i++) { 55 | assert.notStrictEqual(functions[i].docstring, undefined); 56 | } 57 | 58 | for ( 59 | let i = EXPECTED_DOCUMENTED; 60 | i < EXPECTED_UNDOCUMENTED + EXPECTED_UNDOCUMENTED; 61 | i++ 62 | ) { 63 | assert.strictEqual(functions[i].docstring, undefined); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/suite/parser/javascript.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import "mocha"; 5 | import { ExtensionContext, extensions } from "vscode"; 6 | import { 7 | CodeParserService, 8 | createCodeParserService, 9 | getCodeParserService, 10 | } from "../../../services/codeParser"; 11 | import { Function } from "../../../parser/types"; 12 | import { getTelemetryService } from "../../../services/telemetry"; 13 | 14 | //TODO: Make test more robust, and less static (Right now it assumes the test files will be in order of 15 | //Documented functions, then undocumented functions, but should work regardless of the order) 16 | 17 | const LANG = "javascript"; 18 | const EXPECTED_FUNCTIONS = 14; 19 | const EXPECTED_DOCUMENTED = 7; 20 | const EXPECTED_UNDOCUMENTED = 7; 21 | const EXTENSION = ".js"; 22 | 23 | suite("JavaScript parser tests", () => { 24 | let extensionContext: ExtensionContext; 25 | let codeParserService: CodeParserService; 26 | let codeFile: string; 27 | let functions: Function[]; 28 | suiteSetup(async () => { 29 | // Trigger extension activation and grab the context as some tests depend on it 30 | extensionContext = (global as any).testExtensionContext; 31 | codeParserService = getCodeParserService(); 32 | 33 | let filePath: string = extensionContext.asAbsolutePath( 34 | path.join( 35 | "build", 36 | "src", 37 | "test", 38 | "suite", 39 | "parser", 40 | "parser-test-files", 41 | "test" + EXTENSION 42 | ) 43 | ); 44 | codeFile = fs.readFileSync(filePath, "utf8"); 45 | await codeParserService.safeParseText(codeFile, LANG); 46 | functions = codeParserService.getFunctions(); 47 | }); 48 | 49 | test("Parsing documented functions correctly", async () => { 50 | assert.strictEqual(functions.length, EXPECTED_FUNCTIONS); 51 | }); 52 | 53 | test("Reporting correct amount of documented & undocumented functions", async () => { 54 | for (let i = 0; i < EXPECTED_DOCUMENTED; i++) { 55 | assert.notStrictEqual(functions[i].docstring, undefined); 56 | } 57 | 58 | for ( 59 | let i = EXPECTED_DOCUMENTED; 60 | i < EXPECTED_UNDOCUMENTED + EXPECTED_UNDOCUMENTED; 61 | i++ 62 | ) { 63 | assert.strictEqual(functions[i].docstring, undefined); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/suite/parser/typescript.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import "mocha"; 5 | import { ExtensionContext, extensions } from "vscode"; 6 | import { 7 | CodeParserService, 8 | createCodeParserService, 9 | getCodeParserService, 10 | } from "../../../services/codeParser"; 11 | import { Function } from "../../../parser/types"; 12 | import { getTelemetryService } from "../../../services/telemetry"; 13 | 14 | //TODO: Make test more robust, and less static (Right now it assumes the test files will be in order of 15 | //Documented functions, then undocumented functions, but should work regardless of the order) 16 | 17 | const LANG = "typescript"; 18 | const EXPECTED_FUNCTIONS = 12; 19 | const EXPECTED_DOCUMENTED = 6; 20 | const EXPECTED_UNDOCUMENTED = 6; 21 | const EXTENSION = ".ts"; 22 | 23 | suite("Typescript parser tests", () => { 24 | let extensionContext: ExtensionContext; 25 | let codeParserService: CodeParserService; 26 | let codeFile: string; 27 | let functions: Function[]; 28 | suiteSetup(async () => { 29 | // Trigger extension activation and grab the context as some tests depend on it 30 | extensionContext = (global as any).testExtensionContext; 31 | codeParserService = getCodeParserService(); 32 | 33 | let filePath: string = extensionContext.asAbsolutePath( 34 | path.join( 35 | "build", 36 | "src", 37 | "test", 38 | "suite", 39 | "parser", 40 | "parser-test-files", 41 | "test" + EXTENSION 42 | ) 43 | ); 44 | codeFile = fs.readFileSync(filePath, "utf8"); 45 | await codeParserService.safeParseText(codeFile, LANG); 46 | functions = codeParserService.getFunctions(); 47 | }); 48 | 49 | test("Parsing documented functions correctly", async () => { 50 | assert.strictEqual(functions.length, EXPECTED_FUNCTIONS); 51 | }); 52 | 53 | test("Reporting correct amount of documented & undocumented functions", async () => { 54 | for (let i = 0; i < EXPECTED_DOCUMENTED; i++) { 55 | assert.notStrictEqual(functions[i].docstring, undefined); 56 | } 57 | 58 | for ( 59 | let i = EXPECTED_DOCUMENTED; 60 | i < EXPECTED_UNDOCUMENTED + EXPECTED_UNDOCUMENTED; 61 | i++ 62 | ) { 63 | assert.strictEqual(functions[i].docstring, undefined); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Range, Language, QueryCapture, Tree } from "web-tree-sitter"; 3 | import { getAllFuncsQuery } from "./queries"; 4 | import { parsePythonFunctions } from "./langs/python"; 5 | import { parseCSharpFunctions } from "./langs/csharp"; 6 | import { parseJavaFunctions } from "./langs/java"; 7 | import { parseJavaScriptFunctions } from "./langs/javascript"; 8 | import { Function } from "./types"; 9 | import { parseTypeScriptFunctions } from "./langs/typescript"; 10 | 11 | export const parseText = async ( 12 | text: string, 13 | language: Language, 14 | parser: any, 15 | tree: Tree | undefined = undefined 16 | ) => { 17 | // Set the parser language 18 | parser.setLanguage(language); 19 | 20 | //Parse the document 21 | if (!tree) { 22 | return parser.parse(text) as Tree; 23 | } 24 | return parser.parse(text, tree) as Tree; 25 | }; 26 | 27 | export const parseFunctions = async ( 28 | tree: Tree, 29 | lang: string, 30 | TSLanguage: Language 31 | ) => { 32 | let allFuncsQuery = getAllFuncsQuery(lang, TSLanguage); 33 | 34 | if (!allFuncsQuery) { 35 | console.error("Query not found for language", lang); 36 | return []; 37 | } 38 | 39 | let allFuncsCaptures = removeDuplicateNodes( 40 | allFuncsQuery.captures(tree.rootNode) 41 | ); 42 | 43 | //Get the parser for the language 44 | let parser = getParser(lang); 45 | 46 | if (!parser) { 47 | console.error(`Could not find parser for lang ${lang}`); 48 | return []; 49 | } 50 | 51 | let allFunctions = parser(allFuncsCaptures, tree); 52 | 53 | // Return our merged array of functions 54 | return allFunctions; 55 | }; 56 | 57 | const getParser = (lang: string) => { 58 | let parsers: { 59 | [key: string]: (captures: QueryCapture[], tree: Tree) => Function[]; 60 | } = { 61 | python: parsePythonFunctions, 62 | java: parseJavaFunctions, 63 | csharp: parseCSharpFunctions, 64 | javascript: parseJavaScriptFunctions, 65 | typescript: parseTypeScriptFunctions, 66 | }; 67 | return parsers[lang]; 68 | }; 69 | 70 | const removeDuplicateNodes = (captures: QueryCapture[]) => { 71 | let parsedNodeIds: number[] = []; 72 | captures = captures.filter((capture) => { 73 | if (parsedNodeIds.includes(capture.node.id)) { 74 | return false; 75 | } 76 | 77 | parsedNodeIds.push(capture.node.id); 78 | return true; 79 | }); 80 | 81 | return captures; 82 | }; 83 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eqeqeq */ 2 | import * as vscode from "vscode"; 3 | 4 | import { AuthenticationService } from "./services/authenticate"; 5 | import { BillingService } from "./services/billing"; 6 | import { DocsService } from "./services/docs"; 7 | import { ProgressService } from "./services/progress"; 8 | import { createTelemetryService } from "./services/telemetry"; 9 | import { URIService } from "./services/uri"; 10 | import { handleVersionChange } from "./helpers/util"; 11 | import { DevService } from "./services/dev"; 12 | import { openWebView } from "./helpers/webview"; 13 | import { 14 | CodeParserService, 15 | createCodeParserService, 16 | } from "./services/codeParser"; 17 | 18 | // Mixpanel Public Token 19 | var publicMPToken = "6a946c760957a81165973cc1ad5812ec"; 20 | 21 | // this method is called when your extension is activated 22 | // your extension is activated the very first time the command is executed 23 | export async function activate(context: vscode.ExtensionContext) { 24 | // Setup our Telemetry Service 25 | var telemetryService = createTelemetryService(publicMPToken); 26 | 27 | // Handle version changes 28 | handleVersionChange(context, telemetryService); 29 | 30 | // Setup our URI Handler 31 | var uriService = new URIService(context, telemetryService); 32 | 33 | // Setup our Auth service 34 | var authService = new AuthenticationService(context, telemetryService); 35 | 36 | // Setup our CodeParser service 37 | var codeParserService = await createCodeParserService( 38 | context, 39 | telemetryService 40 | ); 41 | 42 | //Setup progress Service 43 | var progressService = new ProgressService(context, codeParserService); 44 | 45 | //Setup our Docs Service 46 | var docsService = new DocsService( 47 | context, 48 | codeParserService, 49 | progressService, 50 | telemetryService 51 | ); 52 | 53 | // Setup our Billing Service 54 | var billingService = new BillingService(context, telemetryService); 55 | 56 | // Setup our Dev Service (for testing only, will confuse users) 57 | // var devService = new DevService(context); 58 | 59 | var helpCmd = vscode.commands.registerCommand("trelent.help", () => { 60 | openWebView(context); 61 | }); 62 | 63 | // Dispose of our command registration 64 | context.subscriptions.push(helpCmd); 65 | (global as any).testExtensionContext = context; 66 | } 67 | 68 | // this method is called when your extension is deactivated 69 | export function deactivate() {} 70 | -------------------------------------------------------------------------------- /src/services/progress.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CodeParserService } from "./codeParser"; 3 | import { Function } from "../parser/types"; 4 | 5 | export class ProgressService { 6 | progressBar: vscode.StatusBarItem; 7 | parser: CodeParserService; 8 | 9 | constructor(context: vscode.ExtensionContext, parser: CodeParserService) { 10 | this.parser = parser; 11 | 12 | this.progressBar = vscode.window.createStatusBarItem( 13 | vscode.StatusBarAlignment.Right, 14 | 10000 15 | ); 16 | this.progressBar.text = "File 0% Documented"; 17 | this.progressBar.backgroundColor = new vscode.ThemeColor( 18 | "statusBarItem.errorBackground" 19 | ); 20 | this.refresh(); 21 | 22 | context.subscriptions.push(this.progressBar); 23 | 24 | vscode.window.onDidChangeActiveTextEditor(this.refreshSoon); 25 | vscode.workspace.onDidSaveTextDocument(this.refreshSoon); 26 | vscode.workspace.onDidOpenTextDocument(this.refreshSoon); 27 | } 28 | 29 | refreshSoon = () => { 30 | setTimeout(() => this.refresh(), 500); 31 | }; 32 | 33 | public refresh() { 34 | let functions = this.parser.getFunctions(); 35 | let totalFunctions = functions.length; 36 | 37 | if (totalFunctions == 0) { 38 | this.progressBar.text = "File 0% Documented"; 39 | return; 40 | } 41 | 42 | let documentedFunctions = functions.filter( 43 | (f) => f.docstring != undefined 44 | ).length; 45 | let percentage = Math.round((documentedFunctions / totalFunctions) * 100); 46 | 47 | this.progressBar.text = `File ${percentage}% documented`; 48 | 49 | if (percentage == 0) { 50 | this.progressBar.backgroundColor = new vscode.ThemeColor( 51 | "statusBarItem.errorBackground" 52 | ); 53 | } else if (percentage > 0 && percentage < 100) { 54 | this.progressBar.backgroundColor = new vscode.ThemeColor( 55 | "statusBarItem.warningBackground" 56 | ); 57 | } else { 58 | this.progressBar.backgroundColor = new vscode.ThemeColor( 59 | "statusBarItem.successBackground" 60 | ); 61 | } 62 | 63 | this.progressBar.show(); 64 | } 65 | 66 | getDocCoverage = async (funcs: Function[]): Promise => { 67 | // get fraction of functions with docstrings 68 | let docCount = 0; 69 | for (let func of funcs) { 70 | if (func.docstring) docCount++; 71 | } 72 | let docCoverage = docCount / (funcs.length - 1); 73 | 74 | return docCoverage; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/autodoc/DocstringCodelens.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Function } from "../parser/types"; 3 | import { CancellationToken } from "vscode"; 4 | 5 | export default class DocstringCodelens implements vscode.Disposable { 6 | private codeLensRegistrationHandle?: vscode.Disposable | null; 7 | private provider: AutodocCodelensProvider; 8 | 9 | constructor() { 10 | console.log("DocstringCodelens.begin"); 11 | this.provider = new AutodocCodelensProvider(); 12 | this.codeLensRegistrationHandle = vscode.languages.registerCodeLensProvider( 13 | [ 14 | { scheme: "file" }, 15 | { scheme: "vscode-vfs" }, 16 | { scheme: "untitled" }, 17 | { scheme: "vscode-userdata" }, 18 | ], 19 | this.provider 20 | ); 21 | } 22 | 23 | dispose() { 24 | if (this.codeLensRegistrationHandle) { 25 | this.codeLensRegistrationHandle.dispose(); 26 | this.codeLensRegistrationHandle = null; 27 | } 28 | } 29 | 30 | public updateCodeLenses(highlightedFunctions: Function[]) { 31 | this.provider.highlightedFunctions = highlightedFunctions; 32 | this.provider.reload(); 33 | } 34 | } 35 | 36 | class AutodocCodelensProvider implements vscode.CodeLensProvider { 37 | private readonly _onChangeCodeLensesEmitter = new vscode.EventEmitter(); 38 | readonly onDidChangeCodeLenses = this._onChangeCodeLensesEmitter.event; 39 | 40 | reload() { 41 | this._onChangeCodeLensesEmitter.fire(); 42 | } 43 | public highlightedFunctions: Function[] = []; 44 | 45 | async provideCodeLenses( 46 | document: vscode.TextDocument, 47 | token: CancellationToken 48 | ): Promise { 49 | const items: vscode.CodeLens[] = []; 50 | 51 | this.highlightedFunctions.forEach((func: Function) => { 52 | const updateDocstringCommand: vscode.Command = { 53 | command: "trelent.autodoc.update", 54 | title: vscode.l10n.t("Update docstring"), 55 | arguments: [document, func], 56 | }; 57 | 58 | const ignoreCommand: vscode.Command = { 59 | command: "trelent.autodoc.ignore", 60 | title: vscode.l10n.t("Ignore"), 61 | arguments: [document, func], 62 | }; 63 | 64 | const range = new vscode.Range( 65 | document.positionAt(func.range[0]), 66 | document.positionAt(func.range[1]) 67 | ); 68 | items.push( 69 | new vscode.CodeLens(range, updateDocstringCommand), 70 | new vscode.CodeLens(range, ignoreCommand) 71 | ); 72 | }); 73 | 74 | return items; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/api/conf.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const CHECKOUT_REDIRECT = `${vscode.env.uriScheme}://trelent.trelent/checkout`; 4 | const PORTAL_REDIRECT = `${vscode.env.uriScheme}://trelent.trelent/portal`; 5 | 6 | // Prod api 7 | export const GET_USER_URL = "https://prod-api.trelent.net/me"; 8 | export const LOGIN_URL = "https://prod-api.trelent.net/auth/login"; 9 | export const LOGOUT_URL = "https://prod-api.trelent.net/auth/logout"; 10 | export const WRITE_DOCSTRING_URL = 11 | "https://prod-api.trelent.net/docs/docstring"; 12 | export const GET_CHECKOUT_URL = 13 | "https://prod-api.trelent.net/billing/checkout?billing_plan=1"; 14 | export const GET_PORTAL_URL = "https://prod-api.trelent.net/billing/portal"; 15 | export const CHECKOUT_RETURN_URL = `https://prod-api.trelent.net/redirect?redirect_url=${encodeURIComponent( 16 | CHECKOUT_REDIRECT 17 | )}`; 18 | export const PORTAL_RETURN_URL = `https://prod-api.trelent.net/redirect?redirect_url=${encodeURIComponent( 19 | PORTAL_REDIRECT 20 | )}`; 21 | 22 | // Dev api 23 | // export const GET_USER_URL = "https://dev-api.trelent.net/me"; 24 | // export const LOGIN_URL = "https://dev-api.trelent.net/auth/login"; 25 | // export const LOGOUT_URL = "https://dev-api.trelent.net/auth/logout"; 26 | // export const WRITE_DOCSTRING_URL = "https://dev-api.trelent.net/docs/docstring"; 27 | // export const GET_CHECKOUT_URL = 28 | // "https://dev-api.trelent.net/billing/checkout?billing_plan=1"; 29 | // export const GET_PORTAL_URL = "https://dev-api.trelent.net/billing/portal"; 30 | // export const CHECKOUT_RETURN_URL = `https://dev-api.trelent.net/redirect?redirect_url=${encodeURIComponent( 31 | // CHECKOUT_REDIRECT 32 | // )}`; 33 | // export const PORTAL_RETURN_URL = `https://dev-api.trelent.net/redirect?redirect_url=${encodeURIComponent( 34 | // PORTAL_REDIRECT 35 | // )}`; 36 | 37 | // Local api 38 | //export const GET_USER_URL = "http://localhost:8000/me"; 39 | //export const LOGIN_URL = "http://localhost:8000/auth/login"; 40 | //export const LOGOUT_URL = "http://localhost:8000/auth/logout"; 41 | //export const WRITE_DOCSTRING_URL = "http://localhost:8000/docs/docstring"; 42 | //export const GET_CHECKOUT_URL = 43 | // "http://localhost:8000/billing/checkout?billing_plan=1"; 44 | //export const GET_PORTAL_URL = "http://localhost:8000/billing/portal"; 45 | //export const CHECKOUT_RETURN_URL = `http://localhost:8000/redirect?redirect_url=${encodeURIComponent( 46 | // CHECKOUT_REDIRECT 47 | //)}`; 48 | //export const PORTAL_RETURN_URL = `http://localhost:8000/redirect?redirect_url=${encodeURIComponent( 49 | // PORTAL_REDIRECT 50 | //)}`; 51 | -------------------------------------------------------------------------------- /src/test/suite/autodoc/ModifyRanges.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import "mocha"; 3 | import { ExtensionContext } from "vscode"; 4 | import * as path from "path"; 5 | import * as assert from "assert"; 6 | import { 7 | CodeParserService, 8 | getCodeParserService, 9 | } from "../../../services/codeParser"; 10 | 11 | suite("Range offset tests", () => { 12 | let extensionContext: ExtensionContext; 13 | let codeParser: CodeParserService; 14 | let documents: vscode.TextDocument[] = []; 15 | let fileUris: vscode.Uri[] = []; 16 | 17 | const FILE_EXTENSIONS = [".cs", ".java", ".js", ".py", ".ts"]; 18 | const EXPECTED_FUNCTIONS = [6, 4, 14, 4, 12]; 19 | suiteSetup(async () => { 20 | // Trigger extension activation and grab the context as some tests depend on it 21 | extensionContext = (global as any).testExtensionContext; 22 | codeParser = getCodeParserService(); 23 | for (let extension of FILE_EXTENSIONS) { 24 | let filePath: string = extensionContext.asAbsolutePath( 25 | path.join( 26 | "build", 27 | "src", 28 | "test", 29 | "suite", 30 | "parser", 31 | "parser-test-files", 32 | "test" + extension 33 | ) 34 | ); 35 | fileUris.push(vscode.Uri.file(filePath)); 36 | } 37 | for (let uri of fileUris) { 38 | let document = await vscode.workspace.openTextDocument(uri); 39 | documents.push(document); 40 | } 41 | 42 | for (let document of documents) { 43 | await codeParser.parse(document); 44 | } 45 | }); 46 | 47 | test("Offsetting range does not cause changes", async () => { 48 | console.log("Hello from test offset range!"); 49 | for (let i = 0; i < documents.length; i++) { 50 | let document = documents[i]; 51 | let editor = await vscode.window.showTextDocument(document); 52 | await editor?.edit((editBuilder) => { 53 | editBuilder.insert(new vscode.Position(0, 0), " "); 54 | }); 55 | 56 | const docFunctionData = 57 | codeParser.changeDetectionService.getDocumentFunctionData(document); 58 | const allDocumentFunctions = docFunctionData.allFunctions; 59 | 60 | assert.strictEqual( 61 | allDocumentFunctions.length, 62 | EXPECTED_FUNCTIONS[i], 63 | "Functions being parsed after offsetting range" 64 | ); 65 | 66 | // TODO: Rewrite test to work without persisting changes 67 | /* 68 | const changedFunctions = docFunctionData.updates; 69 | const changesArray = Object.values(changedFunctions).flatMap( 70 | (val) => val 71 | ); 72 | 73 | assert.strictEqual(changesArray.length, 0, "No changes being reported"); 74 | */ 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trelent - Alt + D 2 | 3 | ![Ratings](https://img.shields.io/visual-studio-marketplace/r/Trelent.trelent) 4 | ![Size](https://img.shields.io/github/languages/code-size/Trelent/Trelent-VSCode-Extension) 5 | ![Installs](https://img.shields.io/visual-studio-marketplace/i/Trelent.trelent) 6 | [![Discord](https://img.shields.io/discord/832745466747420682?label=discord)](https://discord.gg/3gWUdP8EeC) 7 | 8 | Documentation sucks. Let us take care of it! Just click anywhere in your function and press `Alt + D` (`⌘ + D` on Mac). 9 | 10 | Trelent uses AI to write documentation for your functions instantly. 11 | 12 | ![Trelent writing an example docstring](images/trelent-example.gif) 13 | 14 | ### Supported Languages 15 | 16 | Trelent currently supports C#, Java, JavaScript and Python docstrings. We default to the standard formats in each language (XML, JavaDoc, JSDoc, and ReST respectively), and for Python we additionally support the Google and Numpy docstring formats. Support for additional languages is on our roadmap. 17 | 18 | If you have any other suggestions we would love to hear from you at [contact@trelent.net](mailto:contact@trelent.net)! 19 | 20 | _We cannot guaruntee 100% accuracy with the docstrings we write. Always review generated documentation to check for errors._ 21 | 22 | ### Command Reference 23 | 24 | `Trelent | Write Docstring`: Write a docstring for the function or method your cursor is in. 25 | `Trelent | Login`: Login to your Trelent account. 26 | `Trelent | Logout`: Logout from your Trelent account. 27 | `Trelent | Sign Up`: Sign Up for a Trelent account. 28 | `Trelent | Upgrade Account`: Upgrade to a paid Trelent account. 29 | `Trelent | Billing Portal`: Go to the billing portal to manage your Trelent account. 30 | 31 | ### Keybind Reference 32 | 33 | `Write Docstring | Trelent` is bound to `Alt + D` (`⌘ + D` on Mac). 34 | 35 | ### Menu Reference 36 | 37 | `Write Docstring | Trelent` can be found in the editor context (right-click) menu when in a text editor. 38 | 39 | ### Seals of Approval 40 | 41 | "This thing is crazy! For anyone programming in Python I would recommend giving it a look." - Wyatt, Student 42 | 43 | ### Disclaimer 44 | 45 | By installing our extension, you agree to our Terms of Use and Privacy Policy. Further, you understand that we may store anonymized source code in order to improve future versions of our service, and you ensure that you have the appropriate rights of ownership for this code. We may also share certain required data with our partners to provide our services (eg. web hosting, payment processing, user accounts). We do not sell your information to any third parties. If you have any questions or concerns, please contact us [by email](mailto:contact@trelent.net). To host locally, please [contact us](mailto:contact@trelent.net)to learn more about our enterprise plan. 46 | -------------------------------------------------------------------------------- /src/services/uri.ts: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url"; 2 | import * as vscode from "vscode"; 3 | import { TelemetryService } from "./telemetry"; 4 | import { TokenManager } from "../helpers/token"; 5 | 6 | export class URIService { 7 | constructor(context: vscode.ExtensionContext, telemetry: TelemetryService) { 8 | var handler = vscode.window.registerUriHandler({ 9 | async handleUri(uri: vscode.Uri) { 10 | try { 11 | if (uri.path === "/login") { 12 | const query = new URLSearchParams(uri.query); 13 | const token = query.get("token"); 14 | 15 | if (!token) { 16 | vscode.window.showErrorMessage( 17 | "Authentication failed! Please try again." 18 | ); 19 | return; 20 | } 21 | 22 | await TokenManager.setToken(context, token); 23 | vscode.window.showInformationMessage(`Login successful!`); 24 | } else if (uri.path === "/logout") { 25 | vscode.window.showInformationMessage( 26 | "You have been logged out of Trelent." 27 | ); 28 | } else if (uri.path === "/checkout") { 29 | const query = new URLSearchParams(uri.query); 30 | const event = query.get("event"); 31 | 32 | if (!event) { 33 | vscode.window.showInformationMessage( 34 | "Thank you for upgrading your account! Enjoy 1,000 docs/month, and more features coming every month! Please allow for up to 5 minutes for your account to be upgraded." 35 | ); 36 | return; 37 | } 38 | 39 | switch (event) { 40 | case "upgrade": 41 | vscode.window.showInformationMessage( 42 | "Thank you for upgrading your account! Enjoy 1,000 docs/month, and more features coming every month!" 43 | ); 44 | case "cancel": 45 | vscode.window.showInformationMessage( 46 | "Your subscription has been cancelled. You will not be charged again. You will still get 100 free docs/month." 47 | ); 48 | default: 49 | vscode.window.showInformationMessage( 50 | "Your billing information has been updated." 51 | ); 52 | } 53 | } else if (uri.path === "/portal") { 54 | vscode.window.showInformationMessage( 55 | "Your billing information has been updated." 56 | ); 57 | } 58 | } catch (error: any) { 59 | // Something went wrong client-side 60 | telemetry.trackEvent("Client Error", { 61 | error: error, 62 | time: new Date().toISOString(), 63 | }); 64 | 65 | vscode.window.showErrorMessage( 66 | "An error occurred while processing your request. Please try again." 67 | ); 68 | } 69 | }, 70 | }); 71 | 72 | context.subscriptions.push(handler); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /react-help-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Trelent Help 23 | 24 | 25 | 66 |
67 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // External imports 3 | const axios = require("axios").default; 4 | import * as vscode from "vscode"; 5 | 6 | // Internal imports 7 | import "./conf"; 8 | import { TokenManager } from "../helpers/token"; 9 | import { Function } from "../parser/types"; 10 | import { 11 | GET_CHECKOUT_URL, 12 | GET_PORTAL_URL, 13 | GET_USER_URL, 14 | WRITE_DOCSTRING_URL, 15 | CHECKOUT_RETURN_URL, 16 | PORTAL_RETURN_URL, 17 | } from "./conf"; 18 | 19 | export const getCheckoutUrl = async (token: string): Promise => { 20 | let result = await axios({ 21 | method: "GET", 22 | url: `${GET_CHECKOUT_URL}&return_url=${CHECKOUT_RETURN_URL}`, 23 | headers: { 24 | Authorization: `Bearer ${token}`, 25 | }, 26 | }); 27 | 28 | return result.data; 29 | }; 30 | 31 | export const getPortalUrl = async (token: string): Promise => { 32 | let result = await axios({ 33 | method: "GET", 34 | url: `${GET_PORTAL_URL}?return_url=${PORTAL_RETURN_URL}`, 35 | headers: { 36 | Authorization: `Bearer ${token}`, 37 | }, 38 | }); 39 | 40 | return result.data; 41 | }; 42 | 43 | export const getUser = async (token: string): Promise => { 44 | let user = null; 45 | await axios({ 46 | method: "GET", 47 | url: GET_USER_URL, 48 | headers: { 49 | Authorization: `Bearer ${token}`, 50 | }, 51 | }) 52 | .then((response: any) => { 53 | let result = response.data; 54 | user = result; 55 | }) 56 | .catch((error: any) => { 57 | console.error(error); 58 | }); 59 | 60 | return user; 61 | }; 62 | 63 | export const requestDocstrings = async ( 64 | context: vscode.ExtensionContext, 65 | format: string, 66 | funcs: Function[], 67 | user: string, 68 | language: string, 69 | modulesContext: string | null 70 | ): Promise => { 71 | let dataArr: { 72 | success: boolean; 73 | error: string; 74 | data: any; 75 | function: Function; 76 | }[] = []; 77 | 78 | let token = await TokenManager.getToken(context); 79 | 80 | // Get a docstring for each function 81 | await Promise.all( 82 | funcs.map(async (func: Function) => { 83 | // Setup our request body 84 | let reqBody = { 85 | context: modulesContext, 86 | format: format, 87 | function: { 88 | function_code: func.text, 89 | function_name: func.name, 90 | function_params: func.params, 91 | }, 92 | language: language, 93 | sender: "ext-vscode", 94 | user_id: user, 95 | }; 96 | 97 | let tokenHeader = `Bearer ${token}`; 98 | 99 | // Send the request 100 | await axios({ 101 | method: "POST", 102 | url: WRITE_DOCSTRING_URL, 103 | data: JSON.stringify(reqBody), 104 | headers: { 105 | Authorization: tokenHeader, 106 | "Content-Type": "application/json", 107 | }, 108 | }) 109 | .then((response: any) => { 110 | let result = response.data; 111 | dataArr.push({ 112 | ...result, 113 | function: func, 114 | }); 115 | }) 116 | .catch((error: any) => { 117 | console.log(error); 118 | console.error(error); 119 | }); 120 | }) 121 | ); 122 | 123 | return dataArr; 124 | }; -------------------------------------------------------------------------------- /src/services/telemetry.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | const mixpanel = require("mixpanel-browser"); 3 | 4 | const disabledTrackingMessage = 5 | "You have disabled tracking. Please understand that although Trelent " + 6 | "does not require tracking for tracking documentation progress, we do need" + 7 | "to send data server-side for generating docs. To use Trelent for generating" + 8 | "docstrings, please update your tracking settings."; 9 | const errorTrackingMessage = 10 | "You have disabled usage tracking. Please understand that although Trelent " + 11 | "does not require client-side usage tracking to function properly, we do track usage of our service " + 12 | "server-side. To opt-out of server-side usage tracking, please discontinue use."; 13 | const crashTrackingMessage = 14 | "You have disabled error tracking. Please understand that although Trelent " + 15 | "does not require client-side tracking to function properly, we do track usage of our service" + 16 | "server-side. To opt-out of server-side usage tracking, please discontinue use."; 17 | 18 | export class TelemetryService { 19 | public trackLevel: number; 20 | 21 | constructor(mixpanelToken: string) { 22 | // Retrieve current tracking level 23 | this.trackLevel = getTelemetrySettings(); 24 | 25 | // Initialize mixpanel 26 | mixpanel.init(mixpanelToken, { debug: true }); 27 | mixpanel.identify(vscode.env.machineId); 28 | 29 | // Persist tracking setting changes. 30 | vscode.workspace.onDidChangeConfiguration(() => { 31 | this.trackLevel = getTelemetrySettings(); 32 | }); 33 | 34 | vscode.env.onDidChangeTelemetryEnabled(() => { 35 | this.trackLevel = getTelemetrySettings(); 36 | }); 37 | } 38 | 39 | public trackEvent(eventName: string, properties: object) { 40 | if (this.trackLevel >= 2) { 41 | mixpanel.track(eventName, properties); 42 | } 43 | } 44 | 45 | public canSendServerData(): boolean { 46 | return this.trackLevel >= 1; 47 | } 48 | } 49 | 50 | function getTelemetrySettings(): number { 51 | // Is telemetry enabled? 52 | if (!vscode.env.isTelemetryEnabled) { 53 | // Disable all tracking, but let them know that server-side usage 54 | // tracking is not optional once again 55 | vscode.window.showWarningMessage(disabledTrackingMessage); 56 | return 0; 57 | } 58 | 59 | // Check deeper telem levels 60 | let telemLevel = vscode.workspace 61 | .getConfiguration("telemetry") 62 | .get("telemetryLevel"); 63 | switch (telemLevel) { 64 | case "all": 65 | // Good to go 66 | return 3; 67 | case "error": 68 | // Good to go, but we should let them know that server-side usage 69 | // tracking is not optional 70 | vscode.window.showWarningMessage(errorTrackingMessage); 71 | return 2; 72 | case "crash": 73 | // Disable error tracking, but let them know that server-side usage 74 | // tracking is not optional again 75 | vscode.window.showWarningMessage(crashTrackingMessage); 76 | return 1; 77 | case "off": 78 | // Disable all tracking, but let them know that server-side usage 79 | // tracking is not optional once again 80 | vscode.window.showWarningMessage(disabledTrackingMessage); 81 | return 0; 82 | default: 83 | // Good to go 84 | return 3; 85 | } 86 | } 87 | 88 | let service: TelemetryService; 89 | 90 | export let createTelemetryService = (mixpanelToken: string) => { 91 | if (!service) { 92 | service = new TelemetryService(mixpanelToken); 93 | } 94 | return service; 95 | } 96 | 97 | export let getTelemetryService = () => { 98 | return service; 99 | } -------------------------------------------------------------------------------- /src/parser/queries.ts: -------------------------------------------------------------------------------- 1 | import { Language } from "web-tree-sitter"; 2 | 3 | export const getAllFuncsQuery = (lang: string, TSLanguage: Language) => { 4 | let query = funcQeries[lang]; 5 | return TSLanguage.query(query); 6 | }; 7 | 8 | const csharpFuncQuery = ` 9 | ( 10 | (comment)* @function.docstrings 11 | . 12 | (constructor_declaration 13 | name: (identifier) @function.name 14 | parameters: (parameter_list) @function.params 15 | body: (block) @function.body 16 | ) @function.def 17 | ) 18 | ( 19 | (comment)* @function.docstrings 20 | . 21 | (method_declaration 22 | name: (identifier) @function.name 23 | parameters: (parameter_list) @function.params 24 | body: (block) @function.body 25 | ) @function.def 26 | ) 27 | ( 28 | (comment)* @function.docstrings 29 | . 30 | (local_function_statement 31 | name: (identifier) @function.name 32 | parameters: (parameter_list) @function.params 33 | body: (block) @function.body 34 | ) @function.def 35 | ) 36 | `; 37 | 38 | const javaFuncQuery = `( 39 | (block_comment)? @function.docstring 40 | . 41 | (method_declaration 42 | name: (identifier) @function.name 43 | parameters: (formal_parameters) @function.params 44 | body: (block) @function.body 45 | ) @function.def 46 | ) 47 | ( 48 | (block_comment)? @function.docstring 49 | . 50 | (constructor_declaration 51 | name: (identifier) @function.name 52 | parameters: (formal_parameters) @function.params 53 | body: (constructor_body) @function.body 54 | ) @function.def 55 | )`; 56 | 57 | const jsFuncQuery = ` 58 | ( 59 | (comment)? @function.docstring 60 | . 61 | (function_declaration 62 | name: (identifier) @function.name 63 | parameters: (formal_parameters) @function.params 64 | body: (statement_block) @function.body 65 | ) @function.def 66 | ) 67 | ( 68 | (comment)? @function.docstring 69 | . 70 | (generator_function_declaration 71 | name: (identifier) @function.name 72 | parameters: (formal_parameters) @function.params 73 | body: (statement_block) @function.body 74 | ) @function.def 75 | ) 76 | ( 77 | (comment)? @function.docstring 78 | . 79 | (lexical_declaration 80 | (variable_declarator 81 | name: (identifier) @function.name 82 | value: [ 83 | (arrow_function 84 | parameters: (formal_parameters) @function.params 85 | body: (_) @function.body 86 | ) 87 | (function 88 | parameters: (formal_parameters) @function.params 89 | body: (_) @function.body 90 | ) 91 | ] 92 | ) 93 | ) @function.def 94 | ) 95 | ( 96 | (comment)? @function.docstring 97 | . 98 | (pair 99 | key: (property_identifier) @function.name 100 | value: (function 101 | parameters: (formal_parameters) @function.params 102 | body: (statement_block) @function.body 103 | ) 104 | ) @function.def 105 | ) 106 | ( 107 | (comment)? @function.docstring 108 | . 109 | (method_definition 110 | name: (property_identifier) @function.name 111 | parameters: (formal_parameters) @function.params 112 | body: (statement_block) @function.body 113 | ) @function.def 114 | )`; 115 | 116 | const pythonFuncQuery = ` 117 | (function_definition 118 | name: (identifier) @function.name 119 | parameters: (parameters) @function.params 120 | body: (block 121 | . 122 | (expression_statement 123 | (string) @function.docstring 124 | )? 125 | (_)* 126 | ) @function.body 127 | ) @function.def 128 | `; 129 | 130 | const tsFuncQuery = jsFuncQuery; 131 | 132 | const funcQeries: any = { 133 | csharp: csharpFuncQuery, 134 | java: javaFuncQuery, 135 | javascript: jsFuncQuery, 136 | python: pythonFuncQuery, 137 | typescript: tsFuncQuery, 138 | }; 139 | -------------------------------------------------------------------------------- /src/autodoc/DocstringDecorator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Function } from "../parser/types"; 3 | 4 | export default class DocstringDecorator implements vscode.Disposable { 5 | private decorations: { [key: string]: vscode.TextEditorDecorationType } = {}; 6 | private decorationUsesWholeLine: boolean = true; 7 | 8 | constructor() { 9 | this.registerDocrationTypes(); 10 | } 11 | 12 | private registerDocrationTypes() { 13 | Object.keys(this.decorations).forEach((key) => { 14 | this.decorations[key].dispose(); 15 | }); 16 | 17 | this.decorations = {}; 18 | 19 | this.decorations["function.body"] = 20 | vscode.window.createTextEditorDecorationType({ 21 | isWholeLine: this.decorationUsesWholeLine, 22 | backgroundColor: new vscode.ThemeColor("trelent.autodoc.functionColor"), 23 | }); 24 | 25 | this.decorations["function.header"] = 26 | vscode.window.createTextEditorDecorationType({ 27 | backgroundColor: new vscode.ThemeColor( 28 | "trelent.autodoc.functionHeadColor" 29 | ), 30 | isWholeLine: this.decorationUsesWholeLine, 31 | after: { 32 | contentText: " " + vscode.l10n.t("(Trelent: Outdated docstring)"), 33 | color: new vscode.ThemeColor("descriptionForeground"), 34 | }, 35 | }); 36 | } 37 | 38 | public applyDocstringRecommendations( 39 | functions: Function[], 40 | doc: vscode.TextDocument 41 | ) { 42 | if (!functions || functions.length === 0) { 43 | return; 44 | } 45 | 46 | const editor = vscode.window.visibleTextEditors.find( 47 | (editor) => editor.document === doc 48 | ); 49 | if (!editor) { 50 | return; 51 | } 52 | 53 | try { 54 | const matchDecorations: { [key: string]: vscode.Range[] } = {}; 55 | let pushDecorationByRange = (key: string, range: vscode.Range) => { 56 | let currDecoration = (matchDecorations[key] = 57 | matchDecorations[key] || []); 58 | currDecoration.push(range); 59 | }; 60 | 61 | let pushDecorationByPosition = (key: string, pos: vscode.Position) => { 62 | pushDecorationByRange(key, new vscode.Range(pos, pos)); 63 | }; 64 | 65 | functions.forEach((func) => { 66 | let recommendedHeaderPosition = new vscode.Position( 67 | func.definition_line, 68 | 0 69 | ); 70 | let recommendedDocstringPos = doc.offsetAt( 71 | recommendedHeaderPosition.translate(1, 0) 72 | ); 73 | let recommendedDocstringRange = new vscode.Range( 74 | doc.positionAt(recommendedDocstringPos), 75 | doc.positionAt(func.range[1]) 76 | ); 77 | //Insert Decorations 78 | 79 | pushDecorationByPosition("function.header", recommendedHeaderPosition); 80 | pushDecorationByRange("function.body", recommendedDocstringRange); 81 | }); 82 | 83 | Object.keys(matchDecorations).forEach((key) => { 84 | let decorationType = this.decorations[key]; 85 | if (decorationType) { 86 | editor.setDecorations(decorationType, matchDecorations[key]); 87 | } 88 | }); 89 | } finally { 90 | } 91 | } 92 | 93 | public clearDecorations(doc: vscode.TextDocument) { 94 | const editor = vscode.window.visibleTextEditors.find( 95 | (editor) => editor.document === doc 96 | ); 97 | if (!editor) { 98 | return; 99 | } 100 | Object.values(this.decorations).forEach((decoration) => { 101 | editor.setDecorations(decoration, []); 102 | }); 103 | } 104 | 105 | dispose() { 106 | Object.keys(this.decorations).forEach((name) => { 107 | this.decorations[name].dispose(); 108 | }); 109 | this.decorations = {}; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/services/billing.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getCheckoutUrl, getPortalUrl } from "../api/api"; 3 | import { TokenManager } from "../helpers/token"; 4 | import { TelemetryService } from "./telemetry"; 5 | 6 | export class BillingService { 7 | constructor(context: vscode.ExtensionContext, telemetry: TelemetryService) { 8 | // Register our billing-related commands 9 | let portalCmd = vscode.commands.registerCommand("trelent.portal", () => { 10 | try { 11 | this.portal(context, telemetry); 12 | } catch (err) { 13 | console.log(err); 14 | } 15 | }); 16 | 17 | let upgradeCmd = vscode.commands.registerCommand("trelent.upgrade", () => { 18 | try { 19 | this.upgrade(context, telemetry); 20 | } catch (err) { 21 | console.log(err); 22 | } 23 | }); 24 | 25 | let upgradeInfoCmd = vscode.commands.registerCommand( 26 | "trelent.upgrade_info", 27 | () => { 28 | try { 29 | vscode.env.openExternal( 30 | vscode.Uri.parse("https://trelent.net/#pricing") 31 | ); 32 | } catch (err) { 33 | console.log(err); 34 | } 35 | } 36 | ); 37 | 38 | context.subscriptions.push(portalCmd, upgradeCmd, upgradeInfoCmd); 39 | } 40 | 41 | public async upgrade( 42 | context: vscode.ExtensionContext, 43 | telemetry: TelemetryService 44 | ): Promise { 45 | let token = await TokenManager.getToken(context); 46 | if (!token) { 47 | vscode.window.showErrorMessage( 48 | "You must be logged in to upgrade your plan." 49 | ); 50 | return; 51 | } 52 | 53 | vscode.window 54 | .withProgress( 55 | { 56 | location: vscode.ProgressLocation.Notification, 57 | title: "Loading checkout...", 58 | }, 59 | async () => { 60 | return await getCheckoutUrl(token); 61 | } 62 | ) 63 | .then((checkoutURL) => { 64 | if (checkoutURL.success && checkoutURL.success === true) { 65 | vscode.env.openExternal(vscode.Uri.parse(checkoutURL.session)); 66 | } else { 67 | let actions = [ 68 | { title: "Login", command: "trelent.login" }, 69 | { title: "Try Again", command: "trelent.upgrade" }, 70 | ]; 71 | 72 | vscode.window 73 | .showErrorMessage( 74 | "Failed to upgrade your account. Try logging in again and retrying.", 75 | ...actions 76 | ) 77 | .then(async (action) => { 78 | if (action) { 79 | vscode.commands.executeCommand(action!.command); 80 | } 81 | }); 82 | } 83 | }); 84 | } 85 | 86 | public async portal( 87 | context: vscode.ExtensionContext, 88 | telemetry: TelemetryService 89 | ): Promise { 90 | let token = await TokenManager.getToken(context); 91 | if (!token) { 92 | vscode.window.showErrorMessage( 93 | "You must be logged in to access the billing portal." 94 | ); 95 | return; 96 | } 97 | 98 | vscode.window 99 | .withProgress( 100 | { 101 | location: vscode.ProgressLocation.Notification, 102 | title: "Loading billing portal...", 103 | }, 104 | async () => { 105 | return await getPortalUrl(token); 106 | } 107 | ) 108 | .then((portalURL) => { 109 | if ((portalURL.success = true)) { 110 | vscode.env.openExternal(vscode.Uri.parse(portalURL.session)); 111 | } else { 112 | let actions = [ 113 | { title: "Login", command: "trelent.login" }, 114 | { title: "Try Again", command: "trelent.portal" }, 115 | ]; 116 | 117 | vscode.window 118 | .showErrorMessage( 119 | "Failed to access the billing portal. Try logging in again and retrying.", 120 | ...actions 121 | ) 122 | .then(async (action) => { 123 | if (action) { 124 | vscode.commands.executeCommand(action!.command); 125 | } 126 | }); 127 | } 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/parser/langs/java.ts: -------------------------------------------------------------------------------- 1 | import { QueryCapture, SyntaxNode, Tree } from "web-tree-sitter"; 2 | import { QueryGroup, Function } from "../types"; 3 | import { getTextBetweenPoints, getParams } from "../util"; 4 | 5 | /* 6 | * Overall structure of Java captures 7 | * 8 | * @function.docstring 9 | * @function.def 10 | * @function.name 11 | * @function.params 12 | * @function.body 13 | * 14 | */ 15 | 16 | export const parseJavaFunctions = ( 17 | captures: QueryCapture[], 18 | tree: Tree 19 | ): Function[] => { 20 | const functions: Function[] = []; 21 | 22 | //Get query groups from captures 23 | const queryGroups: QueryGroup[] = groupFunction(captures); 24 | 25 | queryGroups.forEach((queryGroup) => { 26 | let defNode: SyntaxNode, 27 | nameNode: SyntaxNode, 28 | paramsNode: SyntaxNode, 29 | bodyNode: SyntaxNode, 30 | docNode: SyntaxNode | undefined; 31 | 32 | //Grab the 4 required nodes 33 | defNode = queryGroup.defNode; 34 | nameNode = queryGroup.nameNode; 35 | paramsNode = queryGroup.paramsNode; 36 | bodyNode = queryGroup.bodyNode; 37 | 38 | //If a docNode exists, grab it 39 | if (queryGroup.docNodes.length > 0) { 40 | docNode = queryGroup.docNodes[0]; 41 | } 42 | let func: Function = { 43 | body: "", 44 | definition: "", 45 | definition_line: nameNode.startPosition.row, 46 | docstring: undefined, 47 | docstring_offset: defNode.startIndex, 48 | docstring_range: undefined, 49 | name: "", 50 | params: [], 51 | range: [], 52 | text: "", 53 | }; 54 | 55 | //Define bounds of the function 56 | let start = defNode.startIndex; 57 | let end = defNode.endIndex; 58 | 59 | //Define the fields of the function 60 | func.body = bodyNode.text; 61 | 62 | func.definition = func.definition = getTextBetweenPoints( 63 | tree.rootNode.text, 64 | defNode.startPosition, 65 | bodyNode.startPosition 66 | ); 67 | 68 | //if there is a docNode present, populate the docstring field 69 | if (docNode) { 70 | func.docstring = docNode.text.trim(); 71 | func.docstring_range = [docNode.startIndex, docNode.endIndex]; 72 | } 73 | 74 | func.name = nameNode.text; 75 | 76 | func.params = getParams(paramsNode.text); 77 | 78 | func.range = [start, end]; 79 | 80 | func.text = defNode.text; 81 | 82 | functions.push(func); 83 | }); 84 | 85 | return functions; 86 | }; 87 | 88 | const groupFunction = (captures: QueryCapture[]): QueryGroup[] => { 89 | let queryGroups: QueryGroup[] = []; 90 | for (let i = 0; i < captures.length; i++) { 91 | if (captures[i].name !== "function.def") { 92 | continue; 93 | } 94 | let defNode: QueryCapture | undefined; 95 | let nameNode: QueryCapture | undefined; 96 | let paramsNode: QueryCapture | undefined; 97 | let bodyNode: QueryCapture | undefined; 98 | let docNode: SyntaxNode[] = []; 99 | 100 | //Init base nodes 101 | captures.slice(i, i + 4).forEach((capture) => { 102 | switch (capture.name) { 103 | case "function.def": 104 | defNode = capture; 105 | break; 106 | case "function.name": 107 | nameNode = capture; 108 | break; 109 | case "function.params": 110 | paramsNode = capture; 111 | break; 112 | case "function.body": 113 | bodyNode = capture; 114 | break; 115 | default: 116 | console.error(`Found node out of place ${capture.name}`); 117 | break; 118 | } 119 | }); 120 | 121 | //verify all necessary nodes exist 122 | if (!(defNode && nameNode && paramsNode && bodyNode)) { 123 | console.error( 124 | `Missing node type (defNode: ${!!defNode}, nameNode: ${!!nameNode}, paramsNode: ${!!paramsNode}, bodyNode: ${!!bodyNode})` 125 | ); 126 | continue; 127 | } 128 | 129 | //Grab documentation node if it exists 130 | if (i - 1 >= 0 && captures[i - 1].name === "function.docstring") { 131 | docNode = [captures[i - 1].node]; 132 | } 133 | 134 | let queryGroup: QueryGroup = { 135 | defNode: defNode.node, 136 | nameNode: nameNode.node, 137 | paramsNode: paramsNode.node, 138 | bodyNode: bodyNode.node, 139 | docNodes: docNode, 140 | }; 141 | 142 | queryGroups.push(queryGroup); 143 | } 144 | return queryGroups; 145 | }; 146 | -------------------------------------------------------------------------------- /src/parser/langs/javascript.ts: -------------------------------------------------------------------------------- 1 | import { QueryCapture, SyntaxNode, Tree } from "web-tree-sitter"; 2 | import { QueryGroup, Function } from "../types"; 3 | import { getTextBetweenPoints, getParams } from "../util"; 4 | 5 | /* 6 | * Overall structure of JavaScript captures 7 | * 8 | * @function.docstring 9 | * @function.def 10 | * @function.name 11 | * @function.params 12 | * @function.body 13 | * 14 | */ 15 | 16 | export const parseJavaScriptFunctions = ( 17 | captures: QueryCapture[], 18 | tree: Tree 19 | ): Function[] => { 20 | const functions: Function[] = []; 21 | 22 | //Get query groups from captures 23 | const queryGroups: QueryGroup[] = groupFunction(captures); 24 | 25 | queryGroups.forEach((queryGroup) => { 26 | let defNode: SyntaxNode, 27 | nameNode: SyntaxNode, 28 | paramsNode: SyntaxNode, 29 | bodyNode: SyntaxNode, 30 | docNode: SyntaxNode | undefined; 31 | 32 | //Grab the 4 required nodes 33 | defNode = queryGroup.defNode; 34 | nameNode = queryGroup.nameNode; 35 | paramsNode = queryGroup.paramsNode; 36 | bodyNode = queryGroup.bodyNode; 37 | 38 | //If a docNode exists, grab it 39 | if (queryGroup.docNodes.length > 0) { 40 | docNode = queryGroup.docNodes[0]; 41 | } 42 | let func: Function = { 43 | body: "", 44 | definition: "", 45 | definition_line: nameNode.startPosition.row, 46 | docstring: undefined, 47 | docstring_offset: defNode.startIndex, 48 | docstring_range: undefined, 49 | name: "", 50 | params: [], 51 | range: [], 52 | text: "", 53 | }; 54 | 55 | //Define bounds of the function 56 | let start = defNode.startIndex; 57 | let end = defNode.endIndex; 58 | 59 | //Define the fields of the function 60 | func.body = bodyNode.text; 61 | 62 | func.definition = func.definition = getTextBetweenPoints( 63 | tree.rootNode.text, 64 | defNode.startPosition, 65 | bodyNode.startPosition 66 | ); 67 | 68 | //if there is a docNode present, populate the docstring field 69 | if (docNode) { 70 | func.docstring = docNode.text.trim(); 71 | func.docstring_range = [docNode.startIndex, docNode.endIndex]; 72 | } 73 | 74 | func.name = nameNode.text; 75 | 76 | func.params = getParams(paramsNode.text); 77 | 78 | func.range = [start, end]; 79 | 80 | func.text = defNode.text; 81 | 82 | functions.push(func); 83 | }); 84 | 85 | return functions; 86 | }; 87 | 88 | const groupFunction = (captures: QueryCapture[]): QueryGroup[] => { 89 | let queryGroups: QueryGroup[] = []; 90 | for (let i = 0; i < captures.length; i++) { 91 | if (captures[i].name !== "function.def") { 92 | continue; 93 | } 94 | 95 | let defNode: QueryCapture | undefined; 96 | let nameNode: QueryCapture | undefined; 97 | let paramsNode: QueryCapture | undefined; 98 | let bodyNode: QueryCapture | undefined; 99 | let docNode: SyntaxNode[] = []; 100 | 101 | //Init base nodes 102 | captures.slice(i, i + 4).forEach((capture) => { 103 | switch (capture.name) { 104 | case "function.def": 105 | defNode = capture; 106 | break; 107 | case "function.name": 108 | nameNode = capture; 109 | break; 110 | case "function.params": 111 | paramsNode = capture; 112 | break; 113 | case "function.body": 114 | bodyNode = capture; 115 | break; 116 | default: 117 | console.error(`Found node out of place ${capture.name}`); 118 | break; 119 | } 120 | }); 121 | 122 | //verify all necessary nodes exist 123 | if (!(defNode && nameNode && paramsNode && bodyNode)) { 124 | console.error( 125 | `Missing node type (defNode: ${!!defNode}, nameNode: ${!!nameNode}, paramsNode: ${!!paramsNode}, bodyNode: ${!!bodyNode})` 126 | ); 127 | continue; 128 | } 129 | 130 | //Grab documentation node if it exists 131 | if (i - 1 >= 0 && captures[i - 1].name === "function.docstring") { 132 | docNode = [captures[i - 1].node]; 133 | } 134 | 135 | let queryGroup: QueryGroup = { 136 | defNode: defNode.node, 137 | nameNode: nameNode.node, 138 | paramsNode: paramsNode.node, 139 | bodyNode: bodyNode.node, 140 | docNodes: docNode, 141 | }; 142 | 143 | queryGroups.push(queryGroup); 144 | } 145 | return queryGroups; 146 | }; 147 | -------------------------------------------------------------------------------- /src/parser/langs/typescript.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QueryCapture, SyntaxNode, Tree } from "web-tree-sitter"; 3 | import { QueryGroup, Function } from "../types"; 4 | import { getTextBetweenPoints, getParams } from "../util"; 5 | 6 | /* 7 | * Overall structure of TypeScript captures 8 | * 9 | * @function.docstring 10 | * @function.def 11 | * @function.name 12 | * @function.params 13 | * @function.body 14 | * 15 | */ 16 | 17 | export const parseTypeScriptFunctions = ( 18 | captures: QueryCapture[], 19 | tree: Tree, 20 | doc?: vscode.TextDocument 21 | ): Function[] => { 22 | const functions: Function[] = []; 23 | 24 | //Get query groups from captures 25 | const queryGroups: QueryGroup[] = groupFunction(captures); 26 | 27 | queryGroups.forEach((queryGroup) => { 28 | let defNode: SyntaxNode, 29 | nameNode: SyntaxNode, 30 | paramsNode: SyntaxNode, 31 | bodyNode: SyntaxNode, 32 | docNode: SyntaxNode | undefined; 33 | 34 | //Grab the 4 required nodes 35 | defNode = queryGroup.defNode; 36 | nameNode = queryGroup.nameNode; 37 | paramsNode = queryGroup.paramsNode; 38 | bodyNode = queryGroup.bodyNode; 39 | 40 | //If a docNode exists, grab it 41 | if (queryGroup.docNodes.length > 0) { 42 | docNode = queryGroup.docNodes[0]; 43 | } 44 | let func: Function = { 45 | body: "", 46 | definition: "", 47 | definition_line: nameNode.startPosition.row, 48 | docstring: undefined, 49 | docstring_offset: defNode.startIndex, 50 | docstring_range: undefined, 51 | name: "", 52 | params: [], 53 | range: [], 54 | text: "", 55 | }; 56 | 57 | //Define bounds of the function 58 | let start = defNode.startIndex; 59 | let end = defNode.endIndex; 60 | 61 | //Define the fields of the function 62 | func.body = bodyNode.text; 63 | 64 | func.definition = func.definition = getTextBetweenPoints( 65 | tree.rootNode.text, 66 | defNode.startPosition, 67 | bodyNode.startPosition 68 | ); 69 | 70 | //if there is a docNode present, populate the docstring field 71 | if (docNode) { 72 | func.docstring = docNode.text.trim(); 73 | func.docstring_range = [docNode.startIndex, docNode.endIndex]; 74 | } 75 | 76 | func.name = nameNode.text; 77 | 78 | func.params = getParams(paramsNode.text); 79 | 80 | func.range = [start, end]; 81 | 82 | func.text = defNode.text; 83 | 84 | functions.push(func); 85 | }); 86 | 87 | return functions; 88 | }; 89 | 90 | const groupFunction = (captures: QueryCapture[]): QueryGroup[] => { 91 | let queryGroups: QueryGroup[] = []; 92 | for (let i = 0; i < captures.length; i++) { 93 | if (captures[i].name !== "function.def") { 94 | continue; 95 | } 96 | 97 | let defNode: QueryCapture | undefined; 98 | let nameNode: QueryCapture | undefined; 99 | let paramsNode: QueryCapture | undefined; 100 | let bodyNode: QueryCapture | undefined; 101 | let docNode: SyntaxNode[] = []; 102 | 103 | //Init base nodes 104 | captures.slice(i, i + 4).forEach((capture) => { 105 | switch (capture.name) { 106 | case "function.def": 107 | defNode = capture; 108 | break; 109 | case "function.name": 110 | nameNode = capture; 111 | break; 112 | case "function.params": 113 | paramsNode = capture; 114 | break; 115 | case "function.body": 116 | bodyNode = capture; 117 | break; 118 | default: 119 | console.error(`Found node out of place ${capture.name}`); 120 | break; 121 | } 122 | }); 123 | 124 | //verify all necessary nodes exist 125 | if (!(defNode && nameNode && paramsNode && bodyNode)) { 126 | console.error( 127 | `Missing node type (defNode: ${!!defNode}, nameNode: ${!!nameNode}, paramsNode: ${!!paramsNode}, bodyNode: ${!!bodyNode})` 128 | ); 129 | continue; 130 | } 131 | 132 | //Grab documentation node if it exists 133 | if (i - 1 >= 0 && captures[i - 1].name === "function.docstring") { 134 | docNode = [captures[i - 1].node]; 135 | } 136 | 137 | let queryGroup: QueryGroup = { 138 | defNode: defNode.node, 139 | nameNode: nameNode.node, 140 | paramsNode: paramsNode.node, 141 | bodyNode: bodyNode.node, 142 | docNodes: docNode, 143 | }; 144 | 145 | queryGroups.push(queryGroup); 146 | } 147 | return queryGroups; 148 | }; 149 | -------------------------------------------------------------------------------- /src/parser/langs/python.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QueryCapture, SyntaxNode, Tree } from "web-tree-sitter"; 3 | import { QueryGroup, Function } from "../types"; 4 | import { getTextBetweenPoints, getParams } from "../util"; 5 | 6 | /* 7 | * Overall structure of Python captures 8 | * 9 | * @function.def 10 | * @function.name 11 | * @function.params 12 | * @function.body 13 | * @function.docstring 14 | */ 15 | 16 | export const parsePythonFunctions = ( 17 | captures: QueryCapture[], 18 | tree: Tree, 19 | doc?: vscode.TextDocument 20 | ): Function[] => { 21 | const functions: Function[] = []; 22 | 23 | //Get query groups from captures 24 | const queryGroups: QueryGroup[] = groupFunction(captures); 25 | 26 | queryGroups.forEach((queryGroup) => { 27 | let defNode: SyntaxNode, 28 | nameNode: SyntaxNode, 29 | paramsNode: SyntaxNode, 30 | bodyNode: SyntaxNode, 31 | docNode: SyntaxNode | undefined; 32 | 33 | //Grab the 4 required nodes 34 | defNode = queryGroup.defNode; 35 | nameNode = queryGroup.nameNode; 36 | paramsNode = queryGroup.paramsNode; 37 | bodyNode = queryGroup.bodyNode; 38 | 39 | //If a docNode exists, grab it 40 | if (queryGroup.docNodes.length > 0) { 41 | docNode = queryGroup.docNodes[0]; 42 | } 43 | let func: Function = { 44 | body: "", 45 | definition: "", 46 | definition_line: nameNode.startPosition.row, 47 | docstring: undefined, 48 | docstring_offset: bodyNode.startIndex, 49 | docstring_range: undefined, 50 | name: "", 51 | params: [], 52 | range: [], 53 | text: "", 54 | }; 55 | 56 | //Define bounds of the function 57 | let start = defNode.startIndex; 58 | let end = defNode.endIndex; 59 | 60 | func.definition = func.definition = getTextBetweenPoints( 61 | tree.rootNode.text, 62 | defNode.startPosition, 63 | bodyNode.startPosition 64 | ); 65 | 66 | //if there is a docNode present, populate the docstring field 67 | if (docNode) { 68 | func.docstring = docNode.text.trim(); 69 | func.docstring_range = [docNode.startIndex, docNode.endIndex]; 70 | } 71 | 72 | //Extract docstring from the body (since we are storing it in docstring anyways) 73 | let docText = bodyNode.text; 74 | if (func.docstring_range) { 75 | docText = docText.substring( 76 | func.docstring_range[1] - func.docstring_range[0], 77 | docText.length 78 | ); 79 | } 80 | func.body = docText; 81 | 82 | func.name = nameNode.text; 83 | 84 | func.params = getParams(paramsNode.text); 85 | 86 | func.range = [start, end]; 87 | 88 | func.text = defNode.text; 89 | 90 | functions.push(func); 91 | }); 92 | 93 | return functions; 94 | }; 95 | 96 | const groupFunction = (captures: QueryCapture[]): QueryGroup[] => { 97 | let queryGroups: QueryGroup[] = []; 98 | for (let i = 0; i < captures.length; i++) { 99 | if (captures[i].name !== "function.def") { 100 | continue; 101 | } 102 | 103 | let defNode: QueryCapture | undefined; 104 | let nameNode: QueryCapture | undefined; 105 | let paramsNode: QueryCapture | undefined; 106 | let bodyNode: QueryCapture | undefined; 107 | let docNode: SyntaxNode[] = []; 108 | 109 | //Init base nodes 110 | captures.slice(i, i + 4).forEach((capture) => { 111 | switch (capture.name) { 112 | case "function.def": 113 | defNode = capture; 114 | break; 115 | case "function.name": 116 | nameNode = capture; 117 | break; 118 | case "function.params": 119 | paramsNode = capture; 120 | break; 121 | case "function.body": 122 | bodyNode = capture; 123 | break; 124 | default: 125 | console.error(`Found node out of place ${capture.name}`); 126 | break; 127 | } 128 | }); 129 | 130 | //verify all necessary nodes exist 131 | if (!(defNode && nameNode && paramsNode && bodyNode)) { 132 | console.error( 133 | `Missing node type (defNode: ${!!defNode}, nameNode: ${!!nameNode}, paramsNode: ${!!paramsNode}, bodyNode: ${!!bodyNode})` 134 | ); 135 | continue; 136 | } 137 | 138 | //Grab documentation node if it exists 139 | if ( 140 | i + 4 < captures.length && 141 | captures[i + 4].name === "function.docstring" 142 | ) { 143 | docNode = [captures[i + 4].node]; 144 | } 145 | 146 | let queryGroup: QueryGroup = { 147 | defNode: defNode.node, 148 | nameNode: nameNode.node, 149 | paramsNode: paramsNode.node, 150 | bodyNode: bodyNode.node, 151 | docNodes: docNode, 152 | }; 153 | 154 | queryGroups.push(queryGroup); 155 | } 156 | return queryGroups; 157 | }; 158 | -------------------------------------------------------------------------------- /src/parser/langs/csharp.ts: -------------------------------------------------------------------------------- 1 | import { QueryCapture, SyntaxNode, Tree } from "web-tree-sitter"; 2 | import { QueryGroup, Function } from "../types"; 3 | import { getTextBetweenPoints, getParams } from "../util"; 4 | 5 | /* 6 | * Overall structure of C# captures 7 | * 8 | * (@function.docstrings) any amount 9 | * @function.def 10 | * @function.name 11 | * @function.params 12 | * @function.body 13 | * 14 | */ 15 | 16 | export const parseCSharpFunctions = ( 17 | captures: QueryCapture[], 18 | tree: Tree 19 | ): Function[] => { 20 | const functions: Function[] = []; 21 | 22 | //Get query groups from captures 23 | const queryGroups: QueryGroup[] = groupFunction(captures); 24 | 25 | queryGroups.forEach((queryGroup) => { 26 | let defNode: SyntaxNode, 27 | nameNode: SyntaxNode, 28 | paramsNode: SyntaxNode, 29 | bodyNode: SyntaxNode, 30 | docNodes: SyntaxNode[]; 31 | 32 | //Grab the 4 required nodes 33 | defNode = queryGroup.defNode; 34 | nameNode = queryGroup.nameNode; 35 | paramsNode = queryGroup.paramsNode; 36 | bodyNode = queryGroup.bodyNode; 37 | docNodes = queryGroup.docNodes; 38 | 39 | let func: Function = { 40 | body: "", 41 | definition: "", 42 | definition_line: nameNode.startPosition.row, 43 | docstring: undefined, 44 | docstring_offset: defNode.startIndex, 45 | docstring_range: undefined, 46 | name: "", 47 | params: [], 48 | range: [0, 0], 49 | text: "", 50 | }; 51 | 52 | //Define bounds of the function 53 | let start = defNode.startIndex; 54 | let end = bodyNode.endIndex; 55 | 56 | //Define the fields of the function 57 | func.body = bodyNode.text; 58 | 59 | func.definition = func.definition = getTextBetweenPoints( 60 | tree.rootNode.text, 61 | defNode.startPosition, 62 | bodyNode.startPosition 63 | ); 64 | 65 | //if there is a docNode present, populate the docstring field 66 | if (docNodes.length > 0) { 67 | let docText = ""; 68 | docNodes.forEach((docNode) => { 69 | docText += docNode.text.trim() + "\n"; 70 | }); 71 | //Chop off trailing newline 72 | docText = docText.trimEnd(); 73 | 74 | //get docstring position 75 | func.docstring = docText; 76 | const docStart = Math.min( 77 | ...docNodes.map((node) => { 78 | return node.startIndex; 79 | }) 80 | ); 81 | const docEnd = Math.max( 82 | ...docNodes.map((node) => { 83 | return node.endIndex; 84 | }) 85 | ); 86 | func.docstring_range = [docStart, docEnd]; 87 | } 88 | 89 | func.name = nameNode.text; 90 | 91 | func.params = getParams(paramsNode.text); 92 | 93 | func.range = [start, end]; 94 | 95 | func.text = defNode.text; 96 | 97 | functions.push(func); 98 | }); 99 | 100 | return functions; 101 | }; 102 | 103 | const groupFunction = (captures: QueryCapture[]): QueryGroup[] => { 104 | let queryGroups: QueryGroup[] = []; 105 | for (let i = 0; i < captures.length; i++) { 106 | let docNodes: SyntaxNode[] = []; 107 | while (i < captures.length && captures[i].name === "function.docstrings") { 108 | docNodes.push(captures[i++].node); 109 | } 110 | 111 | if (captures[i].name !== "function.def") { 112 | continue; 113 | } 114 | 115 | let defNode: QueryCapture | undefined; 116 | let nameNode: QueryCapture | undefined; 117 | let paramsNode: QueryCapture | undefined; 118 | let bodyNode: QueryCapture | undefined; 119 | 120 | //Init base nodes 121 | captures.slice(i, i + 4).forEach((capture) => { 122 | switch (capture.name) { 123 | case "function.def": 124 | defNode = capture; 125 | break; 126 | case "function.name": 127 | nameNode = capture; 128 | break; 129 | case "function.params": 130 | paramsNode = capture; 131 | break; 132 | case "function.body": 133 | bodyNode = capture; 134 | break; 135 | default: 136 | console.error(`Found node out of place ${capture.name}`); 137 | break; 138 | } 139 | }); 140 | 141 | //verify all necessary nodes exist 142 | if (!(defNode && nameNode && paramsNode && bodyNode)) { 143 | console.error( 144 | `Missing node type (defNode: ${!!defNode}, nameNode: ${!!nameNode}, paramsNode: ${!!paramsNode}, bodyNode: ${!!bodyNode})` 145 | ); 146 | continue; 147 | } 148 | 149 | let queryGroup: QueryGroup = { 150 | defNode: defNode.node, 151 | nameNode: nameNode.node, 152 | paramsNode: paramsNode.node, 153 | bodyNode: bodyNode.node, 154 | docNodes: docNodes, 155 | }; 156 | 157 | queryGroups.push(queryGroup); 158 | } 159 | return queryGroups; 160 | }; 161 | -------------------------------------------------------------------------------- /src/helpers/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { TelemetryService } from "../services/telemetry"; 3 | import { openWebView } from "./webview"; 4 | 5 | function isMinorUpdate(previousVersion: string, currentVersion: string) { 6 | // Check for malformed string 7 | if (previousVersion.indexOf(".") === -1) { 8 | return true; 9 | } 10 | 11 | // returns array like [1, 1, 1] corresponding to [major, minor, patch] 12 | var previousVerArr = previousVersion.split(".").map(Number); 13 | var currentVerArr = currentVersion.split(".").map(Number); 14 | 15 | // Check major and minor versions 16 | if ( 17 | currentVerArr[0] > previousVerArr[0] || 18 | currentVerArr[1] > previousVerArr[1] || 19 | currentVerArr[2] > previousVerArr[2] 20 | ) { 21 | return true; 22 | } else { 23 | return false; 24 | } 25 | } 26 | 27 | async function showWelcomePage(context: vscode.ExtensionContext) { 28 | //openWebView("https://trelent-extension-welcome-site.pages.dev/", undefined); 29 | openWebView(context); 30 | } 31 | 32 | async function showVersionPopup( 33 | context: vscode.ExtensionContext, 34 | currentVersion: string 35 | ) { 36 | const result = await vscode.window.showInformationMessage( 37 | `Trelent v${currentVersion} — Automatic docstrings! 🎉`, 38 | ...[ 39 | { 40 | title: "Enable Auto-Mode", 41 | }, 42 | { 43 | title: "Join Community", 44 | }, 45 | ] 46 | ); 47 | 48 | if (result?.title === "Join Community") { 49 | vscode.commands.executeCommand( 50 | "vscode.open", 51 | vscode.Uri.parse("https://discord.com/invite/trelent") 52 | ); 53 | } else if (result?.title === "Enable Auto-Mode") { 54 | vscode.commands.executeCommand("trelent.help"); 55 | } else if (result?.title === "Get Started") { 56 | vscode.commands.executeCommand("_workbench.openWorkspaceSettingsEditor"); 57 | } 58 | } 59 | 60 | export async function handleVersionChange( 61 | context: vscode.ExtensionContext, 62 | telemetry: TelemetryService 63 | ) { 64 | const previousVersion = context.globalState.get("Trelent.trelent"); 65 | const currentVersion = 66 | vscode.extensions.getExtension("Trelent.trelent")!.packageJSON.version; 67 | 68 | // store latest version 69 | context.globalState.update("Trelent.trelent", currentVersion); 70 | 71 | if (previousVersion === undefined) { 72 | // First time install 73 | showWelcomePage(context); 74 | 75 | // Fire an event 76 | telemetry.trackEvent("Install", { 77 | version: currentVersion, 78 | sender: "vs-code", 79 | }); 80 | } else if (isMinorUpdate(previousVersion, currentVersion)) { 81 | showVersionPopup(context, currentVersion); 82 | } 83 | } 84 | 85 | export async function showPopupContent(message: string) { 86 | await vscode.window.showInformationMessage(message); 87 | } 88 | 89 | export function compareDocstringPoints( 90 | docStrA: { point: number[] }, 91 | docStrB: { point: number[] } 92 | ) { 93 | if (docStrA.point[0] < docStrB.point[0]) { 94 | return -1; 95 | } 96 | if (docStrA.point[0] > docStrB.point[0]) { 97 | return 1; 98 | } 99 | return 0; 100 | } 101 | 102 | export async function insertDocstrings( 103 | docstrings: any[], 104 | editor: vscode.TextEditor, 105 | languageId: string 106 | ) { 107 | // First, sort our docstrings by first insertion so that when we account 108 | // for newly-inserted lines, we aren't mismatching docstring locations 109 | docstrings.sort(compareDocstringPoints); 110 | 111 | for (let docstring of docstrings) { 112 | let docPoint = docstring["point"]; 113 | let docStr = docstring["docstring"]; 114 | 115 | // Add an extra line if we are at the top of the file. 116 | if (docPoint[0] < 0) { 117 | const newLinePos = new vscode.Position(0, 0); 118 | const newLine = new vscode.SnippetString(`\n`); 119 | 120 | editor?.insertSnippet(newLine, newLinePos); 121 | docPoint[0] = 0; 122 | } 123 | 124 | // If this is a c-style language, add a newline above the docstring. Otherwise, add one below. 125 | // This prevents overwriting the line before or after the docstring. Also check if we need an extra 126 | // line if there is non-whitespace above the insert location. 127 | const insertPosition = new vscode.Position(docPoint[0], docPoint[1]); 128 | 129 | //editor?.insertSnippet(snippet, insertPosition); 130 | let indent = new vscode.Range( 131 | new vscode.Position(insertPosition.line, 0), 132 | insertPosition 133 | ); 134 | let indentStr = " ".repeat(editor?.document.getText(indent).length); 135 | let snippetStr = (docStr.trimEnd() + "\n") 136 | .split("\n") 137 | .join("\n" + indentStr); 138 | 139 | await editor?.edit((editBuilder) => { 140 | editBuilder.replace(insertPosition, snippetStr.trimStart()); 141 | }); 142 | 143 | // DEBUG 144 | // console.log(docPoint[0]+insertedLines + " " + docPoint[1]); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/helpers/webview.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | 4 | //const axios = require("axios").default; 5 | /** 6 | * Manages react webview panels 7 | */ 8 | class ReactPanel { 9 | /** 10 | * Track the currently panel. Only allow a single panel to exist at a time. 11 | */ 12 | public static currentPanel: ReactPanel | undefined; 13 | 14 | private static readonly viewType = "react"; 15 | 16 | private readonly _panel: vscode.WebviewPanel; 17 | private readonly _extensionPath: string; 18 | private _disposables: vscode.Disposable[] = []; 19 | 20 | public static createOrShow(extensionPath: string) { 21 | const column = vscode.window.activeTextEditor 22 | ? vscode.window.activeTextEditor.viewColumn 23 | : undefined; 24 | 25 | // If we already have a panel, show it. 26 | // Otherwise, create a new panel. 27 | if (ReactPanel.currentPanel) { 28 | ReactPanel.currentPanel._panel.reveal(column); 29 | } else { 30 | ReactPanel.currentPanel = new ReactPanel( 31 | extensionPath, 32 | column || vscode.ViewColumn.One 33 | ); 34 | } 35 | } 36 | 37 | private constructor(extensionPath: string, column: vscode.ViewColumn) { 38 | this._extensionPath = extensionPath; 39 | 40 | // Create and show a new webview panel 41 | this._panel = vscode.window.createWebviewPanel( 42 | ReactPanel.viewType, 43 | "Trelent Help", 44 | column, 45 | { 46 | // Enable javascript in the webview 47 | enableScripts: true, 48 | 49 | // And restric the webview to only loading content from our extension's `media` directory. 50 | localResourceRoots: [ 51 | vscode.Uri.file(path.join(this._extensionPath, "build")), 52 | ], 53 | } 54 | ); 55 | 56 | // Set the webview's initial html content 57 | this._panel.webview.html = this._getHtmlForWebview(); 58 | 59 | // Listen for when the panel is disposed 60 | // This happens when the user closes the panel or when the panel is closed programatically 61 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 62 | 63 | // Handle messages from the webview 64 | this._panel.webview.onDidReceiveMessage( 65 | (message) => { 66 | switch (message.command) { 67 | case "alert": 68 | vscode.window.showErrorMessage(message.text); 69 | return; 70 | } 71 | }, 72 | null, 73 | this._disposables 74 | ); 75 | } 76 | 77 | public doRefactor() { 78 | // Send a message to the webview webview. 79 | // You can send any JSON serializable data. 80 | this._panel.webview.postMessage({ command: "refactor" }); 81 | } 82 | 83 | public dispose() { 84 | ReactPanel.currentPanel = undefined; 85 | 86 | // Clean up our resources 87 | this._panel.dispose(); 88 | 89 | while (this._disposables.length) { 90 | const x = this._disposables.pop(); 91 | if (x) { 92 | x.dispose(); 93 | } 94 | } 95 | } 96 | 97 | private _getHtmlForWebview() { 98 | const manifest = require(path.join( 99 | this._extensionPath, 100 | "build", 101 | "react-help-app", 102 | "asset-manifest.json" 103 | )); 104 | const mainScript = manifest["files"]["main.js"]; 105 | const mainStyle = manifest["files"]["main.css"]; 106 | 107 | const scriptPathOnDisk = vscode.Uri.file( 108 | path.join(this._extensionPath, "build", "react-help-app", mainScript) 109 | ); 110 | const scriptUri = this._panel.webview.asWebviewUri(scriptPathOnDisk); 111 | 112 | const stylePathOnDisk = vscode.Uri.file( 113 | path.join(this._extensionPath, "build", "react-help-app", mainStyle) 114 | ); 115 | const styleUri = this._panel.webview.asWebviewUri(stylePathOnDisk); 116 | 117 | // Use a nonce to whitelist which scripts can be run 118 | const nonce = getNonce(); 119 | 120 | return ` 121 | 122 | 123 | 124 | 125 | 126 | Trelent Help 127 | 128 | 129 | 134 | 135 | 136 | 137 | 138 |
139 | 140 | 141 | 142 | `; 143 | } 144 | } 145 | 146 | function getNonce() { 147 | let text = ""; 148 | const possible = 149 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 150 | for (let i = 0; i < 32; i++) { 151 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 152 | } 153 | return text; 154 | } 155 | 156 | export const openWebView = (context: vscode.ExtensionContext) => { 157 | // Create and show panel 158 | ReactPanel.createOrShow(context.extensionPath); 159 | }; -------------------------------------------------------------------------------- /src/services/docs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eqeqeq */ 2 | import * as vscode from "vscode"; 3 | 4 | import { requestDocstrings } from "../api/api"; 5 | import { isLanguageSupported } from "../helpers/langs"; 6 | import { ModuleGatherer } from "../helpers/modules"; 7 | import { TelemetryService } from "./telemetry"; 8 | import { ProgressService } from "./progress"; 9 | import { CodeParserService } from "./codeParser"; 10 | import { Function } from "../parser/types"; 11 | 12 | export class DocsService { 13 | counter: number; 14 | constructor( 15 | context: vscode.ExtensionContext, 16 | parser: CodeParserService, 17 | progress: ProgressService, 18 | telemetry: TelemetryService 19 | ) { 20 | this.counter = 0; 21 | var writeDocstringCmd = vscode.commands.registerCommand( 22 | "trelent.writeDocstring", 23 | () => { 24 | writeDocstring(context, parser, telemetry); 25 | if (progress) { 26 | this.counter++; 27 | progress.refresh(); 28 | } 29 | } 30 | ); 31 | 32 | // Dispose of our command registration 33 | context.subscriptions.push(writeDocstringCmd); 34 | } 35 | } 36 | 37 | let writeDocstring = async ( 38 | context: vscode.ExtensionContext, 39 | parser: CodeParserService, 40 | telemetry: TelemetryService 41 | ) => { 42 | // Check if telemetry is too strict 43 | if (!telemetry.canSendServerData()) { 44 | vscode.window.showErrorMessage( 45 | "Due to your telemetry settings, we cannot " + "fulfill your request." 46 | ); 47 | return; 48 | } 49 | 50 | // Get the editor instance 51 | let editor = vscode.window.activeTextEditor; 52 | if (editor == undefined) { 53 | vscode.window.showErrorMessage("You don't have an editor open."); 54 | return; 55 | } 56 | 57 | if (!isLanguageSupported(editor.document.languageId)) { 58 | vscode.window.showErrorMessage("We don't support that language."); 59 | return; 60 | } 61 | 62 | // Get the cursor position 63 | let cursorPosition = editor.selection.active; 64 | 65 | await parser.parse(editor.document); 66 | // Get currently selected function 67 | let functions = parser.changeDetectionService.getDocumentFunctionData( 68 | editor.document 69 | ).allFunctions; 70 | 71 | // Check if our cursor is within any of those functions 72 | let currentFunction = isCursorWithinFunction( 73 | editor.document.offsetAt(cursorPosition), 74 | functions 75 | ); 76 | if (currentFunction == undefined) { 77 | vscode.window.showErrorMessage( 78 | "We couldn't find a function at your cursor. Try highlighting your function instead, or move your cursor a bit." 79 | ); 80 | return; 81 | } 82 | 83 | vscode.commands.executeCommand( 84 | "trelent.autodoc.update", 85 | editor.document, 86 | currentFunction 87 | ); 88 | }; 89 | 90 | const isCursorWithinFunction = ( 91 | cursorPosition: number, 92 | functions: Function[] 93 | ): Function | undefined => { 94 | let validFuncs: Function[] = []; 95 | for (let func of functions) { 96 | if (cursorPosition >= func.range[0] && cursorPosition <= func.range[1]) { 97 | validFuncs.push(func); 98 | } 99 | } 100 | 101 | // Search for the one with the greatest indentation, ie func.range[0] is the greatest 102 | let greatestOffset = -1; 103 | let greatestOffsetFunc: Function | undefined = undefined; 104 | for (let func of validFuncs) { 105 | if (func.range[0] > greatestOffset) { 106 | greatestOffset = func.range[0]; 107 | greatestOffsetFunc = func; 108 | } 109 | } 110 | 111 | return greatestOffsetFunc; 112 | }; 113 | 114 | export let writeDocstringsFromParsedDocument = async ( 115 | context: vscode.ExtensionContext, 116 | doc: vscode.TextDocument, 117 | functions: Function[], 118 | telemetry: TelemetryService 119 | ) => { 120 | if (!telemetry.canSendServerData()) { 121 | console.log("Not sending docstrings to server because user has opted out"); 122 | return []; 123 | } 124 | const editor = vscode.window.visibleTextEditors.find( 125 | (editor) => editor.document === doc 126 | ); 127 | if (!editor) { 128 | console.error("Could not find editor"); 129 | return []; 130 | } 131 | 132 | if (!isLanguageSupported(editor.document.languageId)) { 133 | console.log(`Language ${editor.document.languageId} is not supported`); 134 | return []; 135 | } 136 | 137 | let languageId = editor.document.languageId; 138 | if (languageId == "typescript") { 139 | languageId = "javascript"; 140 | } 141 | 142 | let documentContent = editor.document.getText(); 143 | 144 | let format = 145 | vscode.workspace 146 | .getConfiguration("trelent") 147 | .get(`docs.format.${languageId}`) || "rest"; 148 | 149 | let modulesText = await ModuleGatherer.getModules( 150 | documentContent, 151 | languageId 152 | ); 153 | 154 | if (typeof format != "string") { 155 | console.error("Invalid format"); 156 | return []; 157 | } 158 | 159 | let responses: { docstring: string; function: Function }[] = []; 160 | 161 | await requestDocstrings( 162 | context, 163 | format, 164 | functions, 165 | vscode.env.machineId, 166 | languageId, 167 | modulesText 168 | ).then( 169 | ( 170 | result: { 171 | success: boolean; 172 | error: string; 173 | data: any; 174 | function: Function; 175 | }[] 176 | ) => { 177 | if (result == null) { 178 | console.error("No results from documentation call"); 179 | return; 180 | } 181 | result.forEach((docstringData) => { 182 | responses.push({ 183 | docstring: docstringData.data.docstring, 184 | function: docstringData.function, 185 | }); 186 | }); 187 | } 188 | ); 189 | 190 | return responses; 191 | }; 192 | -------------------------------------------------------------------------------- /src/services/codeParser.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | const Parser = require("web-tree-sitter"); 4 | import { getLanguageName, isLanguageSupported } from "../helpers/langs"; 5 | import { parseFunctions, parseText } from "../parser/parser"; 6 | import { Function } from "../parser/types"; 7 | import { 8 | ChangeDetectionService, 9 | getChangeDetectionService, 10 | } from "../autodoc/changeDetection"; 11 | import DocstringInsertService from "../autodoc/DocstringInsertService"; 12 | import { TelemetryService } from "./telemetry"; 13 | import { Tree } from "web-tree-sitter"; 14 | 15 | const getGrammarPath = (context: vscode.ExtensionContext, language: string) => { 16 | let grammarPath = context.asAbsolutePath( 17 | path.join("grammars", "tree-sitter-" + language + ".wasm") 18 | ); 19 | 20 | return grammarPath; 21 | }; 22 | 23 | export class CodeParserService { 24 | //Format: First key is file uri hash, second key is function name + params hash, value is recommended docstring 25 | parser: any; 26 | loadedLanguages: any = { 27 | csharp: null, 28 | java: null, 29 | javascript: null, 30 | python: null, 31 | typescript: null, 32 | }; 33 | parsedFunctions: Function[] = []; 34 | changeDetectionService: ChangeDetectionService; 35 | autodocService: DocstringInsertService; 36 | telemetryService: TelemetryService; 37 | 38 | constructor( 39 | context: vscode.ExtensionContext, 40 | telemetryService: TelemetryService 41 | ) { 42 | this.telemetryService = telemetryService; 43 | this.changeDetectionService = getChangeDetectionService(); 44 | this.autodocService = new DocstringInsertService( 45 | context, 46 | this, 47 | telemetryService 48 | ); 49 | // Initialize our TS Parser 50 | return Parser.init({ 51 | locateFile(scriptName: string, scriptDirectory: string) { 52 | let scriptPath = context.asAbsolutePath( 53 | path.join("grammars", scriptName) 54 | ); 55 | return scriptPath; 56 | }, 57 | }) 58 | .then(async () => { 59 | // Load our language grammars 60 | this.loadedLanguages.csharp = await Parser.Language.load( 61 | getGrammarPath(context, "c_sharp") 62 | ); 63 | this.loadedLanguages.java = await Parser.Language.load( 64 | getGrammarPath(context, "java") 65 | ); 66 | this.loadedLanguages.javascript = await Parser.Language.load( 67 | getGrammarPath(context, "javascript") 68 | ); 69 | this.loadedLanguages.python = await Parser.Language.load( 70 | getGrammarPath(context, "python") 71 | ); 72 | this.loadedLanguages.typescript = await Parser.Language.load( 73 | getGrammarPath(context, "typescript") 74 | ); 75 | }) 76 | .then(async () => { 77 | this.parser = await new Parser(); 78 | 79 | // Now parse when the active editor changes, a document is saved, or a document is opened 80 | vscode.window.onDidChangeActiveTextEditor( 81 | (editor: vscode.TextEditor | undefined) => { 82 | const doc = editor?.document; 83 | if (doc) { 84 | this.parse(doc); 85 | } 86 | } 87 | ); 88 | return this; 89 | }); 90 | } 91 | 92 | public parseNoTrack = async ( 93 | doc: vscode.TextDocument 94 | ): Promise => { 95 | // Language check 96 | const lang = getLanguageName(doc.languageId, doc.fileName); 97 | if (!isLanguageSupported(lang)) return []; 98 | 99 | let tree: Tree | undefined = 100 | this.changeDetectionService.getDocumentFunctionData(doc).tree; 101 | // Parse the document 102 | await this.safeParseText(doc.getText(), lang, tree); 103 | 104 | // Return the functions that are now stored in the service 105 | return this.getFunctions(); 106 | }; 107 | 108 | public parse = async (doc: vscode.TextDocument) => { 109 | const lang = getLanguageName(doc.languageId, doc.fileName); 110 | 111 | // Filter bad input (mostly for supported languages etc) 112 | if (!isLanguageSupported(lang)) return; 113 | 114 | let tree: Tree | undefined = 115 | this.changeDetectionService.getDocumentFunctionData(doc).tree; 116 | 117 | let newTree = await this.safeParseText(doc.getText(), lang, tree); 118 | if (!newTree) { 119 | return {}; 120 | } 121 | let functions = this.getFunctions(); 122 | if (functions.length == 0) { 123 | return {}; 124 | } 125 | 126 | // This returns a list of the functions we want to highlight or update 127 | return this.changeDetectionService.trackState(doc, functions, newTree); 128 | }; 129 | 130 | public safeParseText = async ( 131 | text: string, 132 | lang: string, 133 | tree: Tree | undefined = undefined 134 | ) => { 135 | if (!this.loadedLanguages[lang]) return; 136 | if (!this.parser) return; 137 | let retTree: Tree | undefined = undefined; 138 | await parseText(text, this.loadedLanguages[lang], this.parser, tree) 139 | .then((tree) => { 140 | retTree = tree; 141 | return parseFunctions(tree, lang, this.loadedLanguages[lang]); 142 | }) 143 | .then((functions) => { 144 | this.parsedFunctions = functions; 145 | }) 146 | .catch((err) => { 147 | console.error(err); 148 | }); 149 | return retTree; 150 | }; 151 | 152 | public getFunctions() { 153 | return this.parsedFunctions; 154 | } 155 | } 156 | 157 | let service: CodeParserService | undefined; 158 | 159 | export let createCodeParserService = async ( 160 | context: vscode.ExtensionContext, 161 | telemetryService: TelemetryService 162 | ) => { 163 | if (!service) { 164 | service = await new CodeParserService(context, telemetryService); 165 | } 166 | return service!; 167 | }; 168 | 169 | export let getCodeParserService = () => { 170 | return service!; 171 | }; 172 | -------------------------------------------------------------------------------- /react-help-app/src/app.tsx: -------------------------------------------------------------------------------- 1 | import packageData from "../package.json"; 2 | import Accordion from "./components/Accordion"; 3 | import SyntaxHighlighter from "react-syntax-highlighter"; 4 | import { vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs"; 5 | 6 | const autoModeExample = `def maintained(): # @trelent-auto 7 | """ 8 | This function will have its docstring kept 9 | up to date automatically! 10 | """ 11 | print("A utopian world")`; 12 | 13 | const highlightPerFunctionExample = `def highlighted(): # @trelent-highlight 14 | """This function will be highlighted!""" 15 | print("A pleasant world")`; 16 | 17 | const disablePerFunctionExample = `def disabled(): # @trelent-disable 18 | """ 19 | This function will never be highlighted 20 | nor have its docstring updated. 21 | """ 22 | print("A bleak world")`; 23 | 24 | const theme = vs2015; 25 | 26 | const GettingStarted = () => { 27 | return ( 28 |

29 | Let's get you documenting your code! First up, get a Python or 30 | JavaScript file ready in your editor. Then, click on a function you want 31 | to document! 32 |
33 |
34 | Now, just press 35 | Alt + D or 36 | ⌘ + D 37 | on a Mac. 38 |
39 |
40 | Next, let's configure Autodoc to automatically update your docstrings! 41 | Find the 42 | 43 | trelent.autodoc.mode 44 | 45 | setting in your VS Code configuration, and change it to 46 | 47 | "Maintain Docstrings" 48 | 49 | . Now, whenever we detect a substantial code change, we'll automatically 50 | update your docstrings for you! 51 |
52 |
53 | To find this page again, you may run the 54 | Trelent: Help 55 | command from the Command Palette. 56 |
57 |
58 | We also roughly estimate how much of your code is documented! Just check 59 | out the status bar at the bottom of the editor once you open a supported 60 | file type! 61 |
62 |

63 | ); 64 | }; 65 | 66 | const UsingAutodoc = () => { 67 | return ( 68 |
69 |

70 | Autodoc simplifies code documentation by automatically keeping 71 | docstrings updated, or highlighting the function they belong to when 72 | they become outdated. This helps you easily maintain clear, accurate 73 | documentation. The Autodoc feature has three modes: Highlight 74 | Per-Function, Highlight Globally, and Maintain Docstrings. 75 |

76 | 77 |

78 | Highlight Per-Function 79 |

80 |

81 | This mode highlights functions marked with the "@trelent-highlight" tag 82 | that have outdated docstrings. Functions marked with the "@trelent-auto" 83 | tag will have their docstrings automatically updated. 84 |

85 | 86 |

Highlight Globally

87 |

88 | This mode highlights all functions with outdated docstrings, except 89 | those with the "@trelent-ignore" tag. Functions marked with the 90 | "@trelent-auto" tag will have their docstrings automatically updated. 91 |

92 | 93 |

Maintain Docstrings

94 |

95 | This mode automatically updates all outdated docstrings, except those 96 | with the "@trelent-ignore" or "@trelent-highlight" tags. 97 |

98 | 99 |

100 | You can configure the Autodoc mode and other settings in the VSCode 101 | extension settings. 102 |

103 | 104 |

Tag Examples

105 |

106 | Here are some examples of how to use the Autodoc tags. Note the tags in 107 | green on the same line as the function definition. 108 |

109 |
@trelent-auto
110 | 115 | {autoModeExample} 116 | 117 |
@trelent-highlight
118 | 123 | {highlightPerFunctionExample} 124 | 125 |
@trelent-disable
126 | 131 | {disablePerFunctionExample} 132 | 133 |
134 | ); 135 | }; 136 | 137 | const WhatsNext = () => { 138 | return ( 139 |
140 |

141 | We're continuously working on improving Trelent and adding more 142 | features. Keep an eye on our updates and release notes to stay informed 143 | about the latest enhancements and changes. 144 |

145 |

146 | If you have any suggestions or feedback, don't hesitate to reach out to 147 | us. We appreciate your support and are eager to make Trelent the best 148 | documentation tool for developers! 149 |

150 |
151 | ); 152 | }; 153 | 154 | export default function app() { 155 | return ( 156 |
160 |
161 |

162 | Welcome to Trelent 163 |

164 |
165 | } 168 | title="Getting Started" 169 | /> 170 | } 173 | title="Using Autodoc" 174 | /> 175 | } 178 | title="What's Next" 179 | /> 180 |
181 |
182 |
183 |

184 | Join our{" "} 185 | 186 | Discord community 187 | {" "} 188 | to help shape the future of Trelent! 189 |
190 |
191 | Version: 192 | 193 | {packageData.version} 194 | 195 |

196 |
197 |
198 |
199 |
200 | ); 201 | } 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "Trelent", 3 | "name": "trelent", 4 | "displayName": "Trelent - AI Docstrings on Demand", 5 | "description": "We write and maintain docstrings for your code automatically!", 6 | "version": "2.0.0-rc2", 7 | "repository": "https://github.com/Trelent/Trelent-VSCode-Extension", 8 | "icon": "images/trelent-icon.png", 9 | "galleryBanner": { 10 | "color": "#333333", 11 | "theme": "dark" 12 | }, 13 | "license": "We use the “Commons Clause” License Condition v1.0 with the MIT License.", 14 | "engines": { 15 | "vscode": "^1.60.0" 16 | }, 17 | "categories": [ 18 | "Education", 19 | "Machine Learning", 20 | "Other", 21 | "Programming Languages" 22 | ], 23 | "keywords": [ 24 | "ai", 25 | "artificial intelligence", 26 | "docstring", 27 | "documentation", 28 | "docs", 29 | "productivity", 30 | "javadoc", 31 | "jsdoc", 32 | "docblock" 33 | ], 34 | "activationEvents": [ 35 | "onStartupFinished" 36 | ], 37 | "main": "./build/src/extension.js", 38 | "contributes": { 39 | "commands": [ 40 | { 41 | "command": "trelent.login", 42 | "title": "Trelent: Login" 43 | }, 44 | { 45 | "command": "trelent.logout", 46 | "title": "Trelent: Logout" 47 | }, 48 | { 49 | "command": "trelent.portal", 50 | "title": "Trelent: Billing Portal" 51 | }, 52 | { 53 | "command": "trelent.upgrade", 54 | "title": "Trelent: Upgrade Account" 55 | }, 56 | { 57 | "command": "trelent.signup", 58 | "title": "Trelent: Sign Up" 59 | }, 60 | { 61 | "command": "trelent.writeDocstring", 62 | "title": "Trelent: Write Docstring" 63 | }, 64 | { 65 | "command": "trelent.help", 66 | "title": "Trelent: Help" 67 | }, 68 | { 69 | "command": "trelent.dev.clearTrelentContext", 70 | "title": "Trelent DEV: Clear Trelent Context" 71 | } 72 | ], 73 | "keybindings": [ 74 | { 75 | "command": "trelent.writeDocstring", 76 | "key": "alt+d", 77 | "mac": "cmd+d", 78 | "when": "editorTextFocus" 79 | } 80 | ], 81 | "menus": { 82 | "editor/context": [ 83 | { 84 | "when": "editorTextFocus", 85 | "command": "trelent.writeDocstring" 86 | } 87 | ] 88 | }, 89 | "configuration": { 90 | "type": "object", 91 | "title": "Trelent", 92 | "properties": { 93 | "trelent.docs.format.csharp": { 94 | "type": "string", 95 | "default": "XML", 96 | "enum": [ 97 | "XML" 98 | ], 99 | "enumDescriptions": [ 100 | "Standard C# documentation format" 101 | ], 102 | "description": "C# docstring format" 103 | }, 104 | "trelent.docs.format.java": { 105 | "type": "string", 106 | "default": "JavaDoc", 107 | "enum": [ 108 | "JavaDoc" 109 | ], 110 | "enumDescriptions": [ 111 | "Standard Java documentation format" 112 | ], 113 | "description": "Java docstring format" 114 | }, 115 | "trelent.docs.format.javascript": { 116 | "type": "string", 117 | "default": "JSDoc", 118 | "enum": [ 119 | "JSDoc" 120 | ], 121 | "enumDescriptions": [ 122 | "Standard JS documentation format" 123 | ], 124 | "description": "JS docstring format" 125 | }, 126 | "trelent.docs.format.python": { 127 | "type": "string", 128 | "default": "ReST", 129 | "enum": [ 130 | "ReST", 131 | "Google", 132 | "Numpy" 133 | ], 134 | "enumDescriptions": [ 135 | "ReStructuredText Python documentation format.", 136 | "Google style Python documentation format.", 137 | "Numpy style Python documentation format." 138 | ], 139 | "description": "Python docstring format" 140 | }, 141 | "trelent.autodoc.mode": { 142 | "type": "string", 143 | "default": "Highlight Globally", 144 | "enum": [ 145 | "Highlight Per-Function", 146 | "Highlight Globally", 147 | "Maintain Docstrings" 148 | ], 149 | "enumDescriptions": [ 150 | "-----\nWe will look for functions that are specifically marked with the \"@trelent-highlight\" tag, and highlight those with outdated docstrings.\n\nFunctions marked with the \"@trelent-auto\" tag will have their docstrings automatically kept up to date, replacing them in-place, with no manual action required.", 151 | "-----\nWe will highlight all functions with outdated docstrings, except for those with the \"@trelent-ignore\" tag.\n\nFunctions marked with the \"@trelent-auto\" tag will have their docstrings automatically kept up to date, replacing them in-place, with no manual action required.", 152 | "-----\nWe will automatically keep all outdated docstrings up to date, replacing them in-place, with no manual action required.\n\nWe will skip those with the \"@trelent-ignore\" or \"@trelent-highlight\" tag." 153 | ], 154 | "description": "Defines whether we should automatically keep docstrings up to date, highlight old ones, or do either of these using comments. Please read each description carefully." 155 | }, 156 | "trelent.autodoc.changeThreshold": { 157 | "type": "string", 158 | "default": "Passive", 159 | "enum": [ 160 | "Passive", 161 | "Neutral", 162 | "Aggressive" 163 | ] 164 | } 165 | } 166 | }, 167 | "colors": [ 168 | { 169 | "id": "trelent.autodoc.functionColor", 170 | "description": "Color used to highlight recommended docstrings", 171 | "defaults": { 172 | "dark": "#63B9D611", 173 | "light": "#63B9D611" 174 | } 175 | }, 176 | { 177 | "id": "trelent.autodoc.functionHeadColor", 178 | "description": "Color used as the header for highlighting functions", 179 | "defaults": { 180 | "dark": "#63B9D622", 181 | "light": "#63B9D622" 182 | } 183 | } 184 | ], 185 | "icons": { 186 | "trelent-dark": { 187 | "description": "Trelent icon", 188 | "default": { 189 | "fontPath": "./assets/icons/trelent.woff", 190 | "fontCharacter": "\\1f5ff" 191 | } 192 | } 193 | } 194 | }, 195 | "scripts": { 196 | "vscode:prepublish": "./buildAll.sh", 197 | "start": "react-scripts start", 198 | "build": "tsc -p tsconfig.extension.json && cp -a ./grammars/. ./build/src/grammars/ && cp node_modules/web-tree-sitter/tree-sitter.wasm build/src/grammars/ && cp -a ./src/test/suite/parser/parser-test-files/. ./build/src/test/suite/parser/parser-test-files/", 199 | "eject": "react-scripts eject", 200 | "test": "tsc -p tsconfig.extension.json && node ./build/src/test/runTest.js" 201 | }, 202 | "devDependencies": { 203 | "@types/chai": "^4.3.4", 204 | "@types/fast-levenshtein": "^0.0.2", 205 | "@types/glob": "^7.1.3", 206 | "@types/md5": "^2.3.2", 207 | "@types/mixpanel-browser": "^2.38.0", 208 | "@types/mocha": "^8.2.2", 209 | "@types/node": "^14.18.5", 210 | "@types/polka": "^0.5.4", 211 | "@types/react": "^18.0.25", 212 | "@types/react-dom": "^18.0.9", 213 | "@types/vscode": "^1.60.0", 214 | "@typescript-eslint/eslint-plugin": "^4.26.0", 215 | "@typescript-eslint/parser": "^4.26.0", 216 | "@vscode/test-electron": "^2.2.3", 217 | "@vscode/vsce": "^2.18.0", 218 | "autoprefixer": "^10.4.13", 219 | "chai": "^4.3.7", 220 | "create-react-app": "^5.0.1", 221 | "electron-rebuild": "^3.2.3", 222 | "eslint": "^7.27.0", 223 | "glob": "^7.1.7", 224 | "mocha": "^10.2.0", 225 | "react": "^18.2.0", 226 | "react-dom": "^18.2.0", 227 | "rewire": "^6.0.0", 228 | "tree-sitter-c-sharp": "^0.20.0", 229 | "tree-sitter-cli": "^0.20.7", 230 | "tree-sitter-java": "https://github.com/tree-sitter/tree-sitter-java", 231 | "tree-sitter-javascript": "^0.19.0", 232 | "tree-sitter-python": "^0.20.1", 233 | "tree-sitter-typescript": "^0.20.1", 234 | "typescript": "^4.3.2" 235 | }, 236 | "dependencies": { 237 | "axios": "^0.22.0", 238 | "fast-levenshtein": "^3.0.0", 239 | "md5": "^2.3.0", 240 | "mixpanel-browser": "^2.45.0", 241 | "polka": "^0.5.2", 242 | "ts-node": "^10.9.1", 243 | "web-tree-sitter": "0.20.7", 244 | "web-vitals": "^3.1.0" 245 | }, 246 | "browserslist": [ 247 | ">0.2%", 248 | "not dead", 249 | "not ie <= 11", 250 | "not op_mini all" 251 | ], 252 | "resolveSourceMapLocations": [ 253 | "${workspaceFolder}/**", 254 | "!**/node_modules/**" 255 | ] 256 | } 257 | -------------------------------------------------------------------------------- /src/autodoc/changeDetection.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Function } from "../parser/types"; 3 | import * as md5 from "md5"; 4 | import * as levenshtein from "fast-levenshtein"; 5 | import { Edit, Point, Tree } from "web-tree-sitter"; 6 | 7 | export class ChangeDetectionService { 8 | openDocuments: { 9 | [key: string]: { 10 | allFunctions: Function[]; 11 | }; 12 | } = {}; 13 | private changedFunctions: { [key: string]: { [key: number]: Function } } = {}; 14 | 15 | openDocumentTrees: { 16 | [key: string]: Tree; 17 | } = {}; 18 | 19 | levenshtein_update_threshold: number = 50; 20 | 21 | constructor() { 22 | // Configure the change threshold, and make sure we listen to 23 | // changes to the configuration users may make in the future. 24 | this.refreshThreshold(); 25 | vscode.workspace.onDidChangeConfiguration(this.refreshThreshold); 26 | } 27 | 28 | private refreshThreshold = () => { 29 | const trelentConfig = vscode.workspace.getConfiguration("trelent"); 30 | const autodocThreshold = trelentConfig.get("autodoc.changeThreshold"); 31 | switch (autodocThreshold) { 32 | case "Passive": 33 | this.levenshtein_update_threshold = 1000; 34 | break; 35 | case "Neutral": 36 | this.levenshtein_update_threshold = 500; 37 | break; 38 | case "Aggressive": 39 | this.levenshtein_update_threshold = 250; 40 | break; 41 | default: 42 | // Default to passive 43 | this.levenshtein_update_threshold = 1000; 44 | break; 45 | } 46 | }; 47 | 48 | public trackState( 49 | doc: vscode.TextDocument, 50 | functions: Function[], 51 | tree: Tree 52 | ) { 53 | let documentId = hashDocumentPath(doc); 54 | if (!(documentId in this.openDocuments)) { 55 | this.openDocuments[documentId] = { 56 | allFunctions: functions, 57 | }; 58 | } 59 | 60 | let functionsToUpdate = this.getChangedFunctions(doc, functions); 61 | 62 | // Remove deleted functions from docstring recommendations 63 | functionsToUpdate.deleted.forEach((func) => { 64 | this.deleteChangedFunctionForDocument(doc, func); 65 | }); 66 | 67 | // Update function updates 68 | Object.keys(functionsToUpdate) 69 | .filter((title) => title != "deleted" && title != "all") 70 | .flatMap((title) => functionsToUpdate[title]) 71 | .forEach((func) => { 72 | this.addChangedFunctionForDocument(doc, func); 73 | }); 74 | 75 | this.openDocuments[documentId] = { 76 | allFunctions: functionsToUpdate["all"], 77 | }; 78 | this.openDocumentTrees[documentId] = tree; 79 | return functionsToUpdate; 80 | } 81 | 82 | public closeFile(doc: vscode.Uri) { 83 | let documentId = hashUri(doc); 84 | delete this.openDocuments[documentId]; 85 | delete this.openDocumentTrees[documentId]; 86 | } 87 | 88 | public updateRange( 89 | doc: vscode.TextDocument, 90 | changes: readonly vscode.TextDocumentContentChangeEvent[] 91 | ) { 92 | let documentId = hashDocumentPath(doc); 93 | let edits: Edit[] = []; 94 | let tree = this.openDocumentTrees[documentId]; 95 | if (!tree) { 96 | return; 97 | } 98 | changes.forEach((change) => { 99 | let startIndex = change.rangeOffset; 100 | let oldEndIndex = startIndex + change.rangeLength; 101 | let newEndIndex = startIndex + change.text.length; 102 | let startPosition: Point = { 103 | row: change.range.start.line, 104 | column: change.range.start.character, 105 | }; 106 | let oldEndPosition: Point = { 107 | row: change.range.end.line, 108 | column: change.range.end.character, 109 | }; 110 | let vscodeEnd = doc.positionAt(newEndIndex); 111 | let newEndPosition: Point = { 112 | row: vscodeEnd.line, 113 | column: vscodeEnd.character, 114 | }; 115 | let edit = { 116 | startIndex: startIndex, 117 | oldEndIndex: oldEndIndex, 118 | newEndIndex: newEndIndex, 119 | startPosition: startPosition, 120 | oldEndPosition: oldEndPosition, 121 | newEndPosition: newEndPosition, 122 | }; 123 | tree.edit(edit); 124 | edits.push(edit); 125 | }); 126 | if (this.openDocuments[documentId]) { 127 | this.openDocuments[documentId].allFunctions.forEach((func) => { 128 | updateFunctionRange(func, edits); 129 | }); 130 | } 131 | this.refreshDocChanges(doc); 132 | } 133 | 134 | private refreshDocChanges(doc: vscode.TextDocument) { 135 | let documentId = hashDocumentPath(doc); 136 | 137 | // Skip if the document is no longer open 138 | if (!(documentId in this.changedFunctions)) { 139 | this.changedFunctions[documentId] = {}; 140 | } 141 | 142 | let changedFunctions = Object.values(this.changedFunctions[documentId]); 143 | for (let index in this.changedFunctions[documentId]) { 144 | delete this.changedFunctions[documentId][index]; 145 | } 146 | 147 | changedFunctions.forEach((func) => { 148 | this.changedFunctions[hashFunction(func)] = func; 149 | }); 150 | } 151 | 152 | public getDocumentFunctionData(doc: vscode.TextDocument): { 153 | allFunctions: Function[]; 154 | tree: Tree | undefined; 155 | } { 156 | let documentId = hashDocumentPath(doc); 157 | if (!(documentId in this.openDocuments)) { 158 | return { 159 | allFunctions: [], 160 | tree: undefined, 161 | }; 162 | } 163 | return { 164 | ...this.openDocuments[documentId], 165 | tree: this.openDocumentTrees[documentId], 166 | }; 167 | } 168 | 169 | public getChangedFunctionsForDocument(doc: vscode.TextDocument): { 170 | [key: number]: Function; 171 | } { 172 | let documentId = hashDocumentPath(doc); 173 | if (!(documentId in this.changedFunctions)) { 174 | this.changedFunctions[documentId] = {}; 175 | } 176 | return this.changedFunctions[documentId]; 177 | } 178 | 179 | public deleteChangedFunctionForDocument( 180 | doc: vscode.TextDocument, 181 | func: Function 182 | ) { 183 | let funcId = hashFunction(func); 184 | let documentId = hashDocumentPath(doc); 185 | 186 | // Reset lev distance sum to 0 when a change is ignored 187 | this.openDocuments[documentId].allFunctions = this.openDocuments[ 188 | documentId 189 | ].allFunctions.filter((f) => hashFunction(f) != funcId); 190 | 191 | this.openDocuments[documentId].allFunctions.push({ 192 | ...func, 193 | levenshteinDistanceSum: 0, 194 | }); 195 | 196 | // Delete the function from the doc changes list 197 | let docChanges = this.getChangedFunctionsForDocument(doc); 198 | if (docChanges[funcId]) { 199 | delete docChanges[funcId]; 200 | return true; 201 | } 202 | return false; 203 | } 204 | 205 | public addChangedFunctionForDocument( 206 | doc: vscode.TextDocument, 207 | func: Function 208 | ): boolean { 209 | let docChanges = this.getChangedFunctionsForDocument(doc); 210 | let funcId = hashFunction(func); 211 | docChanges[funcId] = func; 212 | return docChanges[funcId] != undefined; 213 | } 214 | 215 | /** 216 | * Determines whether or not we should notify the user that their document has been updated, and should be re-documented 217 | * @param doc 218 | * @param functions 219 | * @returns 220 | */ 221 | private getChangedFunctions( 222 | doc: vscode.TextDocument, 223 | functions: Function[] 224 | ): { [key: string]: Function[] } { 225 | let history = this.getDocumentFunctionData(doc).allFunctions; 226 | 227 | let returnObj: { [key: string]: Function[] } = { 228 | all: functions, 229 | new: [], 230 | deleted: [], 231 | updated: [], 232 | }; 233 | 234 | //The format of the idMatching object is key: Hash of the name + params, value object with keys "old", and "new" 235 | let idMatching: { [key: string]: { [key: string]: Function } } = {}; 236 | 237 | //Fill idMatching with new functions 238 | functions.forEach((func) => { 239 | let id = hashFunction(func); 240 | if (!(id in idMatching)) { 241 | idMatching[id] = {}; 242 | } 243 | idMatching[id]["new"] = func; 244 | }); 245 | 246 | //Fil idMatching with old functions 247 | history.forEach((func: Function) => { 248 | let id = hashFunction(func); 249 | if (!(id in idMatching)) { 250 | idMatching[id] = {}; 251 | } 252 | idMatching[id]["old"] = func; 253 | }); 254 | 255 | var ids = Object.keys(idMatching); 256 | 257 | ids.forEach((id) => { 258 | let functionPair = idMatching[id]; 259 | if ("old" in functionPair) { 260 | //If the function still exists 261 | if ("new" in functionPair) { 262 | // Calculate the levenshtein distance between the old and new function 263 | const newLevDistance = compareFunctions( 264 | functionPair["old"], 265 | functionPair["new"] 266 | ); 267 | 268 | // Remove the previous version of the function from the history 269 | returnObj.all = returnObj.all.filter( 270 | (f) => hashFunction(f) != hashFunction(functionPair["old"]) 271 | ); 272 | 273 | // Backfill the levenshtein distance sum for the new function into the file history 274 | let funcWithLevenshteinDistance: Function = { 275 | ...functionPair["new"], 276 | levenshteinDistanceSum: newLevDistance, 277 | }; 278 | returnObj.all.push(funcWithLevenshteinDistance); 279 | 280 | if (newLevDistance > this.levenshtein_update_threshold) { 281 | // If beyond the threshold, add to updated list 282 | console.log( 283 | "Updated function: " + funcWithLevenshteinDistance.name, 284 | "passed", 285 | this.levenshtein_update_threshold 286 | ); 287 | returnObj.updated.push(funcWithLevenshteinDistance); 288 | } 289 | } 290 | //If the function was deleted 291 | else { 292 | returnObj.deleted.push(functionPair["old"]); 293 | } 294 | } 295 | //If this is a new function 296 | else { 297 | returnObj.new.push(functionPair["new"]); 298 | returnObj.all.push(functionPair["new"]); 299 | } 300 | }); 301 | 302 | return returnObj; 303 | } 304 | } 305 | 306 | let updateFunctionRange = (func: Function, changes: Edit[]) => { 307 | // Offset vars 308 | let totDocTopOffset = 0, 309 | totDocBottomOffset = 0, 310 | totDocstringPointOffset = 0, 311 | totFuncTopOffset = 0, 312 | totFuncBottomOffset = 0, 313 | totDefLineOffset = 0; 314 | 315 | changes.forEach((edit) => { 316 | // Init needed values lmao 317 | let offsetDiff = edit.newEndIndex - edit.oldEndIndex; 318 | let lineOffset = edit.newEndPosition.row - edit.oldEndPosition.row; 319 | let bottomOffset = func.range[1]; 320 | 321 | // If the changes happened above the function, we need to update the range 322 | if (edit.oldEndIndex <= bottomOffset) { 323 | //Check if the docstring was affected 324 | if (func.docstring_range) { 325 | //Determine if either of the docstring range points were affected 326 | if (edit.oldEndIndex <= func.docstring_range[0]) { 327 | totDocTopOffset += offsetDiff; 328 | } 329 | if (edit.oldEndIndex <= func.docstring_range[1]) { 330 | totDocBottomOffset += offsetDiff; 331 | } 332 | } 333 | 334 | //Check if the docstring point was affected 335 | if (edit.oldEndIndex <= func.docstring_offset) { 336 | totDocstringPointOffset += offsetDiff; 337 | } 338 | 339 | //Determine if either of the function range points were affected 340 | if (edit.oldEndIndex <= func.range[0]) { 341 | totFuncTopOffset += offsetDiff; 342 | } 343 | if (edit.oldEndIndex <= func.range[1]) { 344 | totFuncBottomOffset += offsetDiff; 345 | } 346 | 347 | //Check if defline was affected 348 | if (edit.oldEndPosition.row <= func.definition_line) { 349 | totDefLineOffset += lineOffset; 350 | } 351 | } 352 | }); 353 | if (func.docstring_range) { 354 | //Define the docstring range in terms of offsets 355 | 356 | func.docstring_range[0] += totDocTopOffset; 357 | func.docstring_range[1] += totDocBottomOffset; 358 | } 359 | if (func.docstring_offset) { 360 | func.docstring_offset += totDocstringPointOffset; 361 | } 362 | 363 | func.range[0] += totFuncTopOffset; 364 | func.range[1] += totFuncBottomOffset; 365 | 366 | func.definition_line += totDefLineOffset; 367 | }; 368 | 369 | let compareFunctions = (function1: Function, function2: Function): number => { 370 | let sum = function1.levenshteinDistanceSum || 0; 371 | sum += levenshtein.get(function1.body, function2.body); 372 | return sum; 373 | }; 374 | 375 | export let hashFunction = (func: Function): number => { 376 | return func.definition_line; 377 | }; 378 | 379 | export let hashDocumentPath = (doc: vscode.TextDocument): string => { 380 | return md5(doc.uri.path.toString()); 381 | }; 382 | 383 | export let hashUri = (uri: vscode.Uri): string => { 384 | return md5(uri.path.toString()); 385 | }; 386 | 387 | let changeDetectionService: ChangeDetectionService = 388 | new ChangeDetectionService(); 389 | 390 | export let getChangeDetectionService = () => { 391 | return changeDetectionService; 392 | }; 393 | -------------------------------------------------------------------------------- /src/autodoc/DocstringInsertService.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CodeParserService } from "../services/codeParser"; 3 | import { DocTag, Function } from "../parser/types"; 4 | import { writeDocstringsFromParsedDocument } from "../services/docs"; 5 | import { TelemetryService } from "../services/telemetry"; 6 | import { hashFunction } from "./changeDetection"; 7 | import DocstringDecorator from "./DocstringDecorator"; 8 | import { insertDocstrings } from "../helpers/util"; 9 | import DocstringCodelens from "./DocstringCodelens"; 10 | 11 | export default class DocstringInsertService { 12 | private codeParserService: CodeParserService; 13 | private telemetryService: TelemetryService; 14 | private docstringDecorator: DocstringDecorator; 15 | private docstringCodelens: DocstringCodelens; 16 | private documentationWidget: vscode.StatusBarItem; 17 | 18 | private updating = new Set(); 19 | 20 | public AUTODOC_AUTO_TAG = "@trelent-auto"; 21 | public AUTODOC_IGNORE_TAG = "@trelent-ignore"; 22 | public AUTO_DOC_HIGHLIGHT_TAG = "@trelent-highlight"; 23 | private CHANGE_TIME_THRESHOLD = 500; 24 | 25 | constructor( 26 | private context: vscode.ExtensionContext, 27 | codeParserService: CodeParserService, 28 | telemetryService: TelemetryService 29 | ) { 30 | this.codeParserService = codeParserService; 31 | this.telemetryService = telemetryService; 32 | this.docstringDecorator = new DocstringDecorator(); 33 | this.docstringCodelens = new DocstringCodelens(); 34 | 35 | //Create documentation widget 36 | this.documentationWidget = vscode.window.createStatusBarItem( 37 | "trelent.document", 38 | vscode.StatusBarAlignment.Right, 39 | 9999 40 | ); 41 | 42 | this.widgetLoadingState(false); 43 | context.subscriptions.push(this.documentationWidget); 44 | this.documentationWidget.show(); 45 | 46 | // Register the update and ignore commands for our codelens 47 | vscode.commands.registerCommand( 48 | "trelent.autodoc.update", 49 | this.onAutodocUpdate, 50 | this 51 | ); 52 | 53 | vscode.commands.registerCommand( 54 | "trelent.autodoc.ignore", 55 | this.onAutodocIgnore, 56 | this 57 | ); 58 | 59 | //When the file changes 60 | let timeoutId: NodeJS.Timeout | undefined = undefined; 61 | vscode.workspace.onDidChangeTextDocument( 62 | async (event: vscode.TextDocumentChangeEvent) => { 63 | try { 64 | //Update range references 65 | this.codeParserService.changeDetectionService.updateRange( 66 | event.document, 67 | event.contentChanges 68 | ); 69 | 70 | //Apply highlights to the document after the ranges have been updated 71 | this.applyHighlights(event.document); 72 | 73 | //Clear the timeout if it exists 74 | if (timeoutId) { 75 | clearTimeout(timeoutId); 76 | } 77 | 78 | //Set a new timeout 79 | timeoutId = setTimeout( 80 | (e) => { 81 | if ( 82 | event.reason != vscode.TextDocumentChangeReason.Undo && 83 | event.reason != vscode.TextDocumentChangeReason.Redo 84 | ) { 85 | this.updateDocstrings(e.document); 86 | } 87 | }, 88 | this.CHANGE_TIME_THRESHOLD, 89 | event 90 | ); 91 | } finally { 92 | } 93 | }, 94 | null, 95 | this.context.subscriptions 96 | ); 97 | 98 | vscode.window.onDidChangeVisibleTextEditors( 99 | (e) => { 100 | // Any of which could be new (not just the active one). 101 | e.forEach((editor) => { 102 | this.updateDocstrings(editor.document); 103 | }); 104 | }, 105 | null, 106 | this.context.subscriptions 107 | ); 108 | 109 | vscode.workspace.onDidOpenTextDocument( 110 | (doc) => { 111 | this.codeParserService.parse(doc); 112 | }, 113 | null, 114 | this.context.subscriptions 115 | ); 116 | 117 | vscode.workspace.onDidCloseTextDocument((event) => { 118 | this.codeParserService.changeDetectionService.closeFile(event.uri); 119 | }); 120 | } 121 | 122 | private onAutodocUpdate( 123 | document: vscode.TextDocument, 124 | functionToUpdate: Function 125 | ) { 126 | const editor = vscode.window.visibleTextEditors.find( 127 | (editor) => editor.document === document 128 | ); 129 | if (!editor) { 130 | return; 131 | } 132 | 133 | vscode.commands.executeCommand( 134 | "trelent.autodoc.ignore", 135 | document, 136 | functionToUpdate 137 | ); 138 | this.documentFunctions([functionToUpdate], editor, document); 139 | } 140 | 141 | private onAutodocIgnore( 142 | document: vscode.TextDocument, 143 | functionToIgnore: Function 144 | ) { 145 | this.codeParserService.changeDetectionService.deleteChangedFunctionForDocument( 146 | document, 147 | functionToIgnore 148 | ); 149 | this.applyHighlights(document); 150 | } 151 | 152 | private async updateDocstrings(document: vscode.TextDocument) { 153 | const editor = vscode.window.visibleTextEditors.find( 154 | (editor) => editor.document === document 155 | ); 156 | if (!editor) { 157 | return; 158 | } 159 | 160 | await this.codeParserService.parse(document); 161 | 162 | this.applyHighlights(document); 163 | 164 | let fileHistory = 165 | this.codeParserService.changeDetectionService.getDocumentFunctionData( 166 | document 167 | ); 168 | 169 | let allFunctions = fileHistory.allFunctions; 170 | if (this.updating.has(document)) { 171 | return; 172 | } 173 | this.updating.add(document); 174 | 175 | try { 176 | let functionsToDocument = Object.values( 177 | this.codeParserService.changeDetectionService.getChangedFunctionsForDocument( 178 | document 179 | ) 180 | ); 181 | // Get tagged functions, and remove any that should be ignored 182 | let taggedFunctions = this.getFunctionTags(functionsToDocument); 183 | if (taggedFunctions.length == 0) { 184 | this.updating.delete(document); 185 | return; 186 | } 187 | 188 | // Get functions to be documented, and proceed 189 | let autoFunctions = taggedFunctions 190 | .filter((tagFunc) => tagFunc.tag == DocTag.AUTO) 191 | .map((tagFunc) => tagFunc.function); 192 | 193 | await this.documentFunctions(autoFunctions, editor, document); 194 | } finally { 195 | this.applyHighlights(document, allFunctions); 196 | this.updating.delete(document); 197 | } 198 | } 199 | 200 | /** 201 | * The documentFunctions function is responsible for inserting docstrings into a document. 202 | * 203 | * 204 | * @param functions: Function[] Pass in the functions that were parsed from the document 205 | * @param editor: vscode.TextEditor Get the current editor 206 | * @param document: vscode.TextDocument Get the document that is currently open in the editor 207 | * 208 | * @return A promise that resolves to void 209 | * 210 | * @docauthor Trelent 211 | */ 212 | public async documentFunctions( 213 | functions: Function[], 214 | editor: vscode.TextEditor, 215 | document: vscode.TextDocument 216 | ) { 217 | //Notify the widget to update 218 | this.widgetLoadingState(true); 219 | 220 | try { 221 | // If we have functions to document, write the docstrings 222 | if (functions.length > 0) { 223 | // Write the docstrings 224 | let docstrings = await writeDocstringsFromParsedDocument( 225 | this.context, 226 | document, 227 | functions, 228 | this.telemetryService 229 | ); 230 | 231 | //Reparse in case something changed 232 | 233 | for (let docstring of docstrings) { 234 | let func = docstring.function; 235 | try { 236 | if (func.docstring_range) { 237 | let docstringStartOffset = document.offsetAt( 238 | document.lineAt(document.positionAt(func.docstring_range[0])) 239 | .range.start 240 | ); 241 | let docstringStartPoint = document.positionAt( 242 | docstringStartOffset - 1 243 | ); 244 | let docstringEndPoint = document.positionAt( 245 | func.docstring_range[1] 246 | ); 247 | let range = new vscode.Range( 248 | docstringStartPoint, 249 | docstringEndPoint 250 | ); 251 | await editor?.edit((editBuilder) => { 252 | editBuilder.replace(range, ""); 253 | }); 254 | } 255 | } finally { 256 | this.markAsChanged(document, func); 257 | } 258 | } 259 | let insertionDocstrings: { docstring: string; point: number[] }[] = []; 260 | 261 | docstrings = docstrings.filter((pair) => { 262 | return pair.function.docstring_offset; 263 | }); 264 | for (let docstring of docstrings) { 265 | let funct = docstring.function; 266 | let pos = document.positionAt(funct.docstring_offset); 267 | insertionDocstrings.push({ 268 | docstring: docstring.docstring, 269 | point: [pos.line, pos.character], 270 | }); 271 | } 272 | 273 | await insertDocstrings( 274 | insertionDocstrings, 275 | editor, 276 | document.languageId 277 | ); 278 | } 279 | this.widgetLoadingState(false); 280 | } catch (e) { 281 | this.widgetLoadingState(false, true); 282 | } finally { 283 | // TODO: We probably want to do this somewhere else? 284 | this.applyHighlights(document); 285 | } 286 | } 287 | 288 | private getFunctionTags( 289 | functions: Function[] 290 | ): { function: Function; tag: DocTag }[] { 291 | let tagMatching: { function: Function; tag: DocTag }[] = []; 292 | 293 | for (let func of functions) { 294 | let matchString = func.text; 295 | if (func.docstring) { 296 | matchString += func.docstring; 297 | } 298 | let match = matchString.match( 299 | new RegExp( 300 | this.AUTODOC_AUTO_TAG + 301 | "|" + 302 | this.AUTODOC_IGNORE_TAG + 303 | "|" + 304 | this.AUTO_DOC_HIGHLIGHT_TAG, 305 | "g" 306 | ) 307 | ); 308 | if (match != null) { 309 | switch (match[0]) { 310 | case this.AUTODOC_AUTO_TAG: { 311 | tagMatching.push({ function: func, tag: DocTag.AUTO }); 312 | break; 313 | } 314 | case this.AUTODOC_IGNORE_TAG: { 315 | tagMatching.push({ function: func, tag: DocTag.IGNORE }); 316 | break; 317 | } 318 | case this.AUTO_DOC_HIGHLIGHT_TAG: { 319 | tagMatching.push({ function: func, tag: DocTag.HIGHLIGHT }); 320 | break; 321 | } 322 | default: { 323 | tagMatching.push({ function: func, tag: DocTag.NONE }); 324 | break; 325 | } 326 | } 327 | } else { 328 | tagMatching.push({ function: func, tag: DocTag.NONE }); 329 | } 330 | } 331 | //Remove ignored functions 332 | 333 | const trelentConfig = vscode.workspace.getConfiguration("trelent"); 334 | const autodocMode = trelentConfig.get("autodoc.mode"); 335 | 336 | const configTag: DocTag = ((): DocTag => { 337 | switch (autodocMode) { 338 | case "Highlight Per-Function": { 339 | return DocTag.IGNORE; 340 | } 341 | case "Highlight Globally": { 342 | return DocTag.HIGHLIGHT; 343 | } 344 | case "Maintain Docstrings": { 345 | return DocTag.AUTO; 346 | } 347 | default: { 348 | return DocTag.IGNORE; 349 | } 350 | } 351 | })(); 352 | 353 | //Assign default value to NONE tags, and remove IGNORE tags 354 | return tagMatching 355 | .map((tagFunc) => { 356 | let tag = tagFunc.tag === DocTag.NONE ? configTag : tagFunc.tag; 357 | return { function: tagFunc.function, tag: tag }; 358 | }) 359 | .filter((tagFunc) => { 360 | return tagFunc.tag != DocTag.IGNORE; 361 | }); 362 | } 363 | 364 | public markAsChanged(doc: vscode.TextDocument, func: Function) { 365 | this.codeParserService.changeDetectionService.deleteChangedFunctionForDocument( 366 | doc, 367 | func 368 | ); 369 | } 370 | 371 | private async applyHighlights( 372 | doc: vscode.TextDocument, 373 | allFunctions: Function[] | undefined = undefined 374 | ) { 375 | if (!allFunctions) { 376 | allFunctions = 377 | this.codeParserService.changeDetectionService.getDocumentFunctionData( 378 | doc 379 | ).allFunctions; 380 | } 381 | if (allFunctions.length == 0) { 382 | let parsed = await this.codeParserService.parse(doc); 383 | if (!parsed) { 384 | return; 385 | } 386 | allFunctions = 387 | this.codeParserService.changeDetectionService.getDocumentFunctionData( 388 | doc 389 | ).allFunctions; 390 | } 391 | //Get tagged functions 392 | let functionsToDocument: Function[] = Object.values( 393 | this.codeParserService.changeDetectionService.getChangedFunctionsForDocument( 394 | doc 395 | ) 396 | ).map( 397 | (oldFunc) => 398 | allFunctions?.find( 399 | (func) => hashFunction(func) == hashFunction(oldFunc) 400 | )! 401 | ); 402 | 403 | let taggedFunctions = this.getFunctionTags(functionsToDocument); 404 | //Highlight Functions 405 | let highlightFunctions = taggedFunctions 406 | .filter((tagFunc) => tagFunc.tag == DocTag.HIGHLIGHT) 407 | .map((tagFunc) => tagFunc.function); 408 | this.docstringCodelens.updateCodeLenses(highlightFunctions); 409 | if (highlightFunctions.length > 0) { 410 | this.docstringDecorator.applyDocstringRecommendations( 411 | highlightFunctions, 412 | doc 413 | ); 414 | } else { 415 | this.docstringDecorator.clearDecorations(doc); 416 | } 417 | } 418 | 419 | private widgetLoadingState( 420 | documenting: boolean, 421 | error: boolean = false, 422 | errorMessage?: string 423 | ) { 424 | if (error) { 425 | this.widgetErrorState(errorMessage ? errorMessage : "Error Documenting!"); 426 | return; 427 | } 428 | this.documentationWidget.backgroundColor = new vscode.ThemeColor( 429 | documenting 430 | ? "statusBarItem.hoverBackground" 431 | : "statusBarItem.remoteBackground" 432 | ); 433 | this.documentationWidget.text = documenting 434 | ? "$(sync~spin)" 435 | : "$(trelent-dark)"; 436 | this.documentationWidget.tooltip = documenting 437 | ? "Documenting..." 438 | : "Trelent Documentation"; 439 | } 440 | 441 | private widgetErrorState(errMsg: string) { 442 | this.documentationWidget.backgroundColor = new vscode.ThemeColor( 443 | "statusBarItem.errorBackground" 444 | ); 445 | this.documentationWidget.tooltip = errMsg; 446 | } 447 | } 448 | --------------------------------------------------------------------------------