├── .github └── workflows │ ├── image_upload.yml │ ├── mintlify_update.yml │ ├── openapi-spec-check.yml │ ├── openapi-version-check.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lsproxy ├── Cargo.toml ├── Dockerfile ├── scripts │ └── process_monitor.sh ├── src │ ├── api_types.rs │ ├── ast_grep │ │ ├── client.rs │ │ ├── identifier │ │ │ ├── config.yml │ │ │ └── rules │ │ │ │ ├── cpp │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── csharp │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── go │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── java │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── javascript │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── php │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── python │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── ruby │ │ │ │ └── all-identifiers.yml │ │ │ │ ├── rust │ │ │ │ └── all-identifiers.yml │ │ │ │ └── tsx │ │ │ │ └── all-identifiers.yml │ │ ├── mod.rs │ │ ├── reference │ │ │ ├── config.yml │ │ │ └── rules │ │ │ │ ├── csharp │ │ │ │ ├── all-references.yml │ │ │ │ ├── class-instantiation.yml │ │ │ │ └── function-call.yml │ │ │ │ ├── php │ │ │ │ ├── attribute-usage.yml │ │ │ │ └── function-call.yml │ │ │ │ ├── python │ │ │ │ ├── all-references.yml │ │ │ │ ├── decorator.yml │ │ │ │ └── function-call.yml │ │ │ │ └── tsx │ │ │ │ ├── all-references.yml │ │ │ │ ├── component-render.yml │ │ │ │ ├── decorator.yml │ │ │ │ └── function-call.yml │ │ ├── symbol │ │ │ ├── config.yml │ │ │ ├── rules │ │ │ │ ├── cpp │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── function-declaration.yml │ │ │ │ │ ├── function-definition.yml │ │ │ │ │ └── type.yml │ │ │ │ ├── csharp │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── enum.yml │ │ │ │ │ ├── field.yml │ │ │ │ │ ├── interface.yml │ │ │ │ │ ├── local-variable.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ └── property.yml │ │ │ │ ├── go │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ └── type.yml │ │ │ │ ├── java │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── interface.yml │ │ │ │ │ └── method.yml │ │ │ │ ├── javascript │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ └── variable.yml │ │ │ │ ├── php │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── constant.yml │ │ │ │ │ ├── enum.yml │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── global.yml │ │ │ │ │ ├── interface.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ ├── property.yml │ │ │ │ │ └── trait.yml │ │ │ │ ├── python │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── local-variable.yml │ │ │ │ │ └── variable.yml │ │ │ │ ├── ruby │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ └── module.yml │ │ │ │ ├── rust │ │ │ │ │ ├── enum.yml │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── implementation.yml │ │ │ │ │ ├── struct.yml │ │ │ │ │ ├── trait.yml │ │ │ │ │ └── type.yml │ │ │ │ └── tsx │ │ │ │ │ ├── class.yml │ │ │ │ │ ├── function.yml │ │ │ │ │ ├── interface.yml │ │ │ │ │ ├── local-variable.yml │ │ │ │ │ ├── method.yml │ │ │ │ │ └── variable.yml │ │ │ └── utils │ │ │ │ └── python │ │ │ │ ├── function-call.yml │ │ │ │ └── function.yml │ │ └── types.rs │ ├── handlers │ │ ├── definitions_in_file.rs │ │ ├── error.rs │ │ ├── find_definition.rs │ │ ├── find_identifier.rs │ │ ├── find_referenced_symbols.rs │ │ ├── find_references.rs │ │ ├── health.rs │ │ ├── list_files.rs │ │ ├── mod.rs │ │ ├── read_source_code.rs │ │ └── utils.rs │ ├── lib.rs │ ├── lsp │ │ ├── client.rs │ │ ├── json_rpc.rs │ │ ├── languages │ │ │ ├── clang.rs │ │ │ ├── csharp.rs │ │ │ ├── golang.rs │ │ │ ├── java.rs │ │ │ ├── mod.rs │ │ │ ├── php.rs │ │ │ ├── python.rs │ │ │ ├── ruby.rs │ │ │ ├── rust.rs │ │ │ └── typescript.rs │ │ ├── manager │ │ │ ├── language_tests │ │ │ │ ├── c_tests.rs │ │ │ │ ├── cpp_tests.rs │ │ │ │ ├── csharp_tests.rs │ │ │ │ ├── golang_tests.rs │ │ │ │ ├── java_tests.rs │ │ │ │ ├── js_tests.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── php_tests.rs │ │ │ │ ├── python_tests.rs │ │ │ │ ├── rust_tests.rs │ │ │ │ ├── tsx_tests.rs │ │ │ │ └── typescript_tests.rs │ │ │ ├── manager.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── process.rs │ ├── main.rs │ ├── middleware │ │ ├── jwt.rs │ │ ├── mod.rs │ │ └── tests.rs │ ├── test_utils.rs │ └── utils │ │ ├── file_utils.rs │ │ ├── mod.rs │ │ └── workspace_documents.rs └── tests │ ├── java_test.rs │ └── python_test.rs ├── openapi.json ├── performance └── process_resource_csv.py ├── release ├── Dockerfile ├── build-and-extract.sh └── install-lsproxy.sh ├── sample_project ├── bash │ └── astar_search.sh ├── c │ ├── main.c │ ├── map.c │ ├── map.h │ ├── pathfinding.c │ └── pathfinding.h ├── cpp │ ├── astar_search.cpp │ └── cpp_classes │ │ ├── astar.cpp │ │ ├── map.cpp │ │ ├── node.cpp │ │ └── point.cpp ├── csharp │ ├── .csproj │ ├── AStar.cs │ ├── Node.cs │ └── Program.cs ├── go │ ├── go.mod │ ├── golang_astar │ │ ├── astar.go │ │ ├── grid.go │ │ ├── node.go │ │ └── search.go │ └── main.go ├── java │ ├── AStar.java │ ├── Main.java │ └── Node.java ├── js │ ├── astar_search.js │ ├── functions.js │ └── methods.js ├── perl │ └── astar_search.pl ├── php │ ├── AStar.php │ ├── Comparable.php │ ├── Node.php │ └── main.php ├── python │ ├── __init__.py │ ├── decorators.py │ ├── graph.py │ ├── main.py │ └── search.py ├── readme.md ├── ruby │ ├── decorators.rb │ ├── graph.rb │ ├── main.rb │ └── search.rb ├── rust │ ├── Cargo.toml │ └── src │ │ ├── astar.rs │ │ ├── main.rs │ │ ├── map.rs │ │ ├── node.rs │ │ └── point.rs └── typescript │ ├── package.json │ ├── src │ ├── Controls.tsx │ ├── GridDisplay.tsx │ ├── InfoPanel.tsx │ ├── PathfinderDisplay.tsx │ ├── astar.ts │ ├── main.ts │ ├── node.ts │ ├── types.ts │ └── visualization.ts │ └── tsconfig.json └── scripts ├── build.sh ├── coverage_test.sh ├── fmt.sh ├── generate_jwt.py ├── generate_spec.sh ├── run.sh └── test.sh /.github/workflows/image_upload.yml: -------------------------------------------------------------------------------- 1 | name: Release LSProxy 2 | on: 3 | push: 4 | tags: 5 | - "[0-9]+.[0-9]+.[0-9]+*" # Matches PEP 440 versions like 0.1.0a1 6 | permissions: 7 | contents: write # For creating releases 8 | packages: write # For container registry operations 9 | jobs: 10 | validate-and-release: 11 | runs-on: 12 | labels: large 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Validate install script version 18 | run: | 19 | VERSION=${GITHUB_REF#refs/tags/} 20 | SCRIPT_VERSION=$(grep "LSPROXY_VERSION=" release/install-lsproxy.sh | cut -d'"' -f2) 21 | if [ "$VERSION" != "$SCRIPT_VERSION" ]; then 22 | echo "Error: Script version ($SCRIPT_VERSION) does not match release tag ($VERSION)" 23 | exit 1 24 | fi 25 | 26 | - name: Validate Cargo.toml version 27 | run: | 28 | VERSION=${GITHUB_REF#refs/tags/} 29 | CARGO_VERSION=$(grep '^version = ' lsproxy/Cargo.toml | cut -d'"' -f2) 30 | if [ "$VERSION" != "$CARGO_VERSION" ]; then 31 | echo "Error: Cargo.toml version ($CARGO_VERSION) does not match release tag ($VERSION)" 32 | exit 1 33 | fi 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | - name: Cache Docker layers 42 | uses: actions/cache@v4 43 | with: 44 | path: /tmp/.buildx-cache 45 | key: ${{ runner.os }}-buildx-${{ github.sha }} 46 | restore-keys: | 47 | ${{ runner.os }}-buildx- 48 | 49 | - name: Extract version from tag 50 | id: get_version 51 | run: | 52 | VERSION=${GITHUB_REF#refs/tags/} 53 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 54 | 55 | - name: Login to Docker Hub 56 | uses: docker/login-action@v3 57 | with: 58 | username: agenticlabs 59 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 60 | 61 | - name: Build and push Docker image 62 | uses: docker/build-push-action@v5 63 | with: 64 | context: ./lsproxy 65 | file: ./release/Dockerfile 66 | push: true 67 | platforms: linux/amd64,linux/arm64 68 | tags: | 69 | agenticlabs/lsproxy:${{ steps.get_version.outputs.VERSION }} 70 | agenticlabs/lsproxy:latest 71 | cache-from: type=local,src=/tmp/.buildx-cache 72 | cache-to: type=local,dest=/tmp/.buildx-cache-new 73 | 74 | - name: Extract binaries from images 75 | run: | 76 | mkdir -p release-binaries 77 | VERSION=${GITHUB_REF#refs/tags/} 78 | # Copy install script 79 | cp release/install-lsproxy.sh release-binaries/ 80 | # Extract AMD64 binary 81 | docker pull agenticlabs/lsproxy:${VERSION} 82 | docker create --name amd64-container --platform linux/amd64 agenticlabs/lsproxy:${VERSION} 83 | docker cp amd64-container:/usr/local/bin/lsproxy-bin release-binaries/lsproxy-${VERSION}-linux-amd64 84 | docker rm amd64-container 85 | # Extract ARM64 binary 86 | docker create --name arm64-container --platform linux/arm64 agenticlabs/lsproxy:${VERSION} 87 | docker cp arm64-container:/usr/local/bin/lsproxy-bin release-binaries/lsproxy-${VERSION}-linux-arm64 88 | docker rm arm64-container 89 | chmod +x release-binaries/* 90 | 91 | - name: Prepare ast-grep rules 92 | run: | 93 | VERSION=${GITHUB_REF#refs/tags/} 94 | mkdir -p release-binaries 95 | cd lsproxy/src 96 | find ast_grep -type f -print0 | tar -czvf ../../release-binaries/lsproxy-${VERSION}-ast-grep-rules.tar.gz --null -T - 97 | 98 | - name: Validate release files 99 | run: | 100 | VERSION=${GITHUB_REF#refs/tags/} 101 | REQUIRED_FILES=( 102 | "release-binaries/lsproxy-${VERSION}-linux-arm64" 103 | "release-binaries/lsproxy-${VERSION}-linux-amd64" 104 | "release-binaries/install-lsproxy.sh" 105 | "release-binaries/lsproxy-${VERSION}-ast-grep-rules.tar.gz" 106 | ) 107 | 108 | for file in "${REQUIRED_FILES[@]}"; do 109 | if [ ! -f "$file" ]; then 110 | echo "Error: Required file $file is missing" 111 | exit 1 112 | fi 113 | done 114 | echo "All required files are present" 115 | 116 | - name: Create Release 117 | uses: softprops/action-gh-release@v1 118 | with: 119 | files: | 120 | release-binaries/lsproxy-${{ github.ref_name }}-linux-arm64 121 | release-binaries/lsproxy-${{ github.ref_name }}-linux-amd64 122 | release-binaries/install-lsproxy.sh 123 | release-binaries/lsproxy-${{ github.ref_name }}-ast-grep-rules.tar.gz 124 | generate_release_notes: true 125 | 126 | - name: Move cache 127 | run: | 128 | rm -rf /tmp/.buildx-cache 129 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 130 | -------------------------------------------------------------------------------- /.github/workflows/mintlify_update.yml: -------------------------------------------------------------------------------- 1 | name: Upload OpenAPI spec and Docs to Mintlify 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'openapi.json' 7 | - 'README.md' 8 | workflow_dispatch: 9 | jobs: 10 | upload-openapi: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Push documented spec to docs repo 15 | uses: dmnemec/copy_file_to_another_repo_action@main 16 | env: 17 | API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} 18 | with: 19 | source_file: 'openapi.json' 20 | destination_repo: 'robmck1995/mintlify-docs' 21 | user_email: 'rmcknight451@gmail.com' 22 | user_name: 'robmck1995' 23 | commit_message: 'Updated openapi.json from lsproxy' 24 | 25 | upload-docs: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Push README to docs repo as introduction.mdx 30 | uses: dmnemec/copy_file_to_another_repo_action@main 31 | env: 32 | API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} 33 | with: 34 | source_file: 'README.md' 35 | destination_file: 'get-started/introduction.mdx' 36 | destination_repo: 'robmck1995/mintlify-docs' 37 | user_email: 'rmcknight451@gmail.com' 38 | user_name: 'robmck1995' 39 | commit_message: 'Updated introduction.mdx from lsproxy README' 40 | -------------------------------------------------------------------------------- /.github/workflows/openapi-spec-check.yml: -------------------------------------------------------------------------------- 1 | name: OpenAPI Spec Check 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "lsproxy/src/**/*.rs" 7 | - "openapi.json" 8 | 9 | jobs: 10 | check-spec: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | - name: Cache Docker layers 19 | uses: actions/cache@v4 20 | with: 21 | path: /tmp/.buildx-cache 22 | key: ${{ runner.os }}-buildx-${{ github.sha }} 23 | restore-keys: | 24 | ${{ runner.os }}-buildx- 25 | - name: Build Base Docker Image 26 | uses: docker/build-push-action@v2 27 | with: 28 | context: ./lsproxy 29 | file: ./release/Dockerfile 30 | load: true 31 | tags: lsproxy-release 32 | cache-from: type=local,src=/tmp/.buildx-cache 33 | cache-to: type=local,dest=/tmp/.buildx-cache-new 34 | - name: Move cache 35 | run: | 36 | rm -rf /tmp/.buildx-cache 37 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 38 | 39 | - name: Generate fresh OpenAPI spec 40 | run: | 41 | mkdir -p temp_spec 42 | 43 | # Run the application to generate the OpenAPI spec 44 | docker run --name temp_box lsproxy-release lsproxy -w 45 | 46 | # Copy the generated OpenAPI spec from the container to the host 47 | docker cp temp_box:/openapi.json temp_spec/openapi.json 48 | 49 | # Remove the temporary container 50 | docker rm temp_box 51 | 52 | - name: Compare specs 53 | run: | 54 | if ! diff -u openapi.json temp_spec/openapi.json; then 55 | echo "::error::OpenAPI spec is out of sync with code! Please run ./scripts/generate_spec.sh" 56 | echo "::error::See diff above for details" 57 | exit 1 58 | fi 59 | -------------------------------------------------------------------------------- /.github/workflows/openapi-version-check.yml: -------------------------------------------------------------------------------- 1 | name: OpenAPI Version Check 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'openapi.json' 7 | 8 | jobs: 9 | check-version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 # This ensures we can access the base branch 16 | 17 | - name: Check OpenAPI version update 18 | run: | 19 | # Get the base branch version 20 | BASE_VERSION=$(git show "origin/${{ github.base_ref }}:openapi.json" | jq -r '.info.version') 21 | 22 | # Get the PR version 23 | PR_VERSION=$(jq -r '.info.version' openapi.json) 24 | 25 | echo "Base version: $BASE_VERSION" 26 | echo "PR version: $PR_VERSION" 27 | 28 | if [ "$BASE_VERSION" = "$PR_VERSION" ]; then 29 | echo "Error: OpenAPI version must be updated when modifying the spec" 30 | echo "Please update the version number in openapi.json" 31 | exit 1 32 | else 33 | echo "✅ OpenAPI version has been updated" 34 | fi 35 | 36 | - name: Check version format 37 | run: | 38 | PR_VERSION=$(jq -r '.info.version' openapi.json) 39 | 40 | # Check if version matches format x.y.z[alpha|beta|rc][n] where x,y,z are numbers 41 | if ! [[ $PR_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+((alpha|beta|rc)[0-9]*)?$ ]]; then 42 | echo "Error: Version format must be x.y.z or x.y.zalpha[n], e.g. 0.1.0 or 0.1.0alpha6" 43 | exit 1 44 | else 45 | echo "✅ Version format is correct" 46 | fi 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - "lsproxy/**" 7 | pull_request: 8 | branches: [main, dev] 9 | paths: 10 | - "lsproxy/**" 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v1 20 | - name: Cache Docker layers 21 | uses: actions/cache@v4 22 | with: 23 | path: /tmp/.buildx-cache 24 | key: ${{ runner.os }}-buildx-${{ github.sha }} 25 | restore-keys: | 26 | ${{ runner.os }}-buildx- 27 | - name: Build Base Docker Image 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: ./lsproxy 31 | load: true 32 | tags: lsproxy-dev 33 | cache-from: type=local,src=/tmp/.buildx-cache 34 | cache-to: type=local,dest=/tmp/.buildx-cache-new 35 | - name: Move cache 36 | run: | 37 | rm -rf /tmp/.buildx-cache 38 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 39 | - name: Run Cargo Test in Docker Container 40 | run: | 41 | mkdir -p shared 42 | docker run -v "${PWD}/lsproxy:/usr/src/app" -v "${PWD}:/mnt/lsproxy_root" lsproxy-dev cargo test --lib --bins --tests 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ 22 | .aider* 23 | 24 | .vscode* 25 | 26 | # Python ignores 27 | *.pyc 28 | **/__pycache__/ 29 | 30 | tags 31 | .lsproxy_tags 32 | .vscode 33 | compile_commands.json 34 | **/*.idx 35 | .metadata 36 | **/jdt.ls-java-project/** 37 | .project 38 | .classpath 39 | .env 40 | .phpactor.json 41 | package-lock.json 42 | node_modules 43 | 44 | # macOS specific files 45 | .DS_Store 46 | **/.DS_Store 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | We welcome all kinds of contributions. _You don't need to be an expert 4 | in API or rust development to help out._ 5 | 6 | Contributions to the build-test-run system and github workflows are also welcome! We love to learn from the expertise of the community 😄 7 | 8 | ## Checklist 9 | 10 | Contributions are made through 11 | [pull requests](https://help.github.com/articles/using-pull-requests/) from a fork of the repo. 12 | Before sending a pull request, make sure to do the following: 13 | 14 | - [Build and format](#build-format) your code 15 | - [Write tests](#tests) 16 | - [Run tests](#tests) and check that they pass 17 | 18 | _Please reach out to the lsproxy team before starting work on a large 19 | contribution._ Get in touch at 20 | [GitHub issues](https://github.com/agentic-labs/lsproxy/issues) 21 | or [on Discord](https://discord.gg/WafeS3jN). 22 | 23 | ## Prerequisites 24 | 25 | To build lsproxy from source, all you need is docker. But rust + rust-analyzer is recommended for getting lints in your IDE: 26 | 27 | - [Docker installation instructions](https://docs.docker.com/engine/install/) 28 | - [Rust installation instructions](https://www.rust-lang.org/tools/install) 29 | 30 | ## Building and formatting 31 | 32 | Building using docker is pretty simple. We have a build script for this purpose. 33 | 34 | ``` 35 | ./scripts/build.sh 36 | ``` 37 | 38 | > :warning: The build script executes the build inside the docker container. It is not recommended and we do not officially support building `lsproxy` locally. 39 | 40 | Before committing, make sure to format the code with 41 | 42 | ``` 43 | ./scripts/fmt.sh 44 | ``` 45 | 46 | ## Running locally 47 | 48 | Running is also pretty simple (the no auth is optional but is easier to interact with locally). 49 | 50 | ``` 51 | ./scripts/run.sh $WORKSPACE_PATH --no-auth 52 | ``` 53 | 54 | The run script also builds as well, so you can just use this single script if you're iterating in development. 55 | 56 | > :warning: Like above the run script runs `lsproxy` inside the docker container, we do not support running `lsproxy` directly on the host. 57 | 58 | ## Testing 59 | 60 | Good testing is essential to ensuring `lsproxy` continues to work for everyone! Whenever you're adding features or changing code please make sure to: 61 | - Create new tests for any features or cases you add 62 | - Ensure existing tests all pass 63 | 64 | Test writing and adjustments should be done **before** opening a PR. 65 | 66 | --- 67 | 68 | To run tests: 69 | 70 | ``` 71 | ./scripts/test.sh 72 | ``` 73 | 74 | Ideally we would like to keep code coverage at the same level or higher. You can generate a code coverage report with the following: 75 | ``` 76 | ./scripts/coverage_test.sh 77 | ``` 78 | 79 | This will create a coverage report that can be viewed with 80 | 81 | ``` 82 | open lsproxy/target/llvm-cov/html/index.html 83 | ``` 84 | 85 | Thanks in advance for your contributions and, most importantly, HAVE FUN! 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Agentic Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lsproxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lsproxy" 3 | version = "0.4.3" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "lsproxy" 8 | path = "src/lib.rs" 9 | 10 | 11 | [dependencies] 12 | actix-web = "4.0" 13 | actix-files = "0.6" 14 | actix-cors = "0.6" 15 | serde = { version = "1.0", features = ["derive"] } 16 | log = "0.4" 17 | env_logger = "0.10" 18 | tracing = "0.1" 19 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 20 | url = "2.3" 21 | regex = { version = "1.8", features = ["std"] } 22 | serde_json = "1.0" 23 | strum = "0.25" 24 | strum_macros = "0.25" 25 | tokio = { version = "1.0", features = ["full", "process"] } 26 | utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "cecda0531bf7d90800af66b186055932ee730526", features = ["actix_extras"] } 27 | utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa.git", rev = "cecda0531bf7d90800af66b186055932ee730526", features = ["actix-web"] } 28 | rand = "0.8" 29 | async-trait = "0.1" 30 | futures = "0.3" 31 | lsp-types = "0.94" 32 | glob = "0.3.1" 33 | uuid = { version = "1.10.0", features = ["v4"] } 34 | ignore = "0.4.23" 35 | clap = { version = "4.3", features = ["derive"] } 36 | notify = "6.1.1" 37 | notify-debouncer-mini = "0.4.1" 38 | fs_extra = "1.3.0" 39 | json5 = "0.4.1" 40 | jsonwebtoken = "9.2" 41 | futures-util = "0.3" 42 | 43 | [dev-dependencies] 44 | tempfile = "3.8.1" 45 | reqwest = { version = "0.11", features = ["blocking", "json"] } 46 | 47 | [profile.release] 48 | incremental = true 49 | 50 | [profile.dev] 51 | incremental = true 52 | -------------------------------------------------------------------------------- /lsproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.82.0-slim-bookworm 2 | 3 | # Set the working directory in the container 4 | WORKDIR /usr/src/app 5 | 6 | # Install runtime and development dependencies, Python, Node.js, PHP, and npm 7 | RUN apt-get update && apt-get install \ 8 | -y --no-install-recommends \ 9 | pkg-config \ 10 | libssl-dev \ 11 | libssl3 \ 12 | ca-certificates \ 13 | git \ 14 | procps \ 15 | python3 \ 16 | python3-pip \ 17 | python3-venv \ 18 | curl \ 19 | clangd \ 20 | build-essential \ 21 | gcc \ 22 | g++ \ 23 | php \ 24 | php-xml \ 25 | php-mbstring \ 26 | php-curl \ 27 | php-zip \ 28 | unzip \ 29 | && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ 30 | && apt-get install -y --no-install-recommends nodejs \ 31 | && apt-get clean \ 32 | && rm -rf /var/lib/apt/lists/* 33 | 34 | # Install Composer 35 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 36 | 37 | RUN cd /usr/src && \ 38 | git clone https://github.com/phpactor/phpactor.git && \ 39 | cd /usr/src/phpactor && \ 40 | composer install 41 | 42 | ENV PATH="/usr/src/phpactor/bin:${PATH}" 43 | 44 | # Config Python 45 | RUN ln -sf /usr/bin/python3 /usr/bin/python 46 | RUN rm /usr/lib/python3.11/EXTERNALLY-MANAGED 47 | 48 | # Install Java 49 | RUN apt-get update && \ 50 | apt-get install -y --no-install-recommends wget gnupg software-properties-common && \ 51 | wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | apt-key add - && \ 52 | echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list && \ 53 | apt-get update && \ 54 | apt-get install -y --no-install-recommends \ 55 | temurin-21-jdk \ 56 | gradle \ 57 | maven \ 58 | && apt-get clean \ 59 | && rm -rf /var/lib/apt/lists/* \ 60 | && \ 61 | curl -L -o /tmp/jdt-language-server.tar.gz https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz && \ 62 | mkdir -p /opt/jdtls && \ 63 | tar -xzf /tmp/jdt-language-server.tar.gz -C /opt/jdtls --no-same-owner && \ 64 | rm /tmp/jdt-language-server.tar.gz 65 | 66 | # Add jdtls to PATH 67 | ENV PATH="/opt/jdtls/bin:${PATH}" 68 | RUN chmod -R +rw /opt/jdtls/config_* 69 | 70 | # Install rust-analyzer and rustfmt 71 | RUN rustup component add rust-analyzer 72 | RUN rustup component add rustfmt 73 | 74 | # Install coverage tools 75 | RUN cargo install cargo-llvm-cov 76 | RUN rustup component add llvm-tools-preview 77 | 78 | # Install jedi python language server, ast grep for tree sitter 79 | RUN pip install jedi-language-server ast-grep-cli 80 | 81 | # Install global npm packages 82 | RUN npm install -g typescript-language-server typescript 83 | 84 | # Install go and Gopls 85 | ARG TARGETARCH 86 | RUN curl -O -L "https://go.dev/dl/go1.23.5.linux-${TARGETARCH}.tar.gz" && \ 87 | tar -C /usr/local -xzf go1.23.5.linux-${TARGETARCH}.tar.gz && \ 88 | rm go1.23.5.linux-${TARGETARCH}.tar.gz && \ 89 | /usr/local/go/bin/go install golang.org/x/tools/gopls@latest && \ 90 | cp ~/go/bin/gopls /usr/bin/gopls 91 | 92 | ENV GOROOT=/usr/local/go 93 | ENV GOPATH=/home/user/go 94 | ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH 95 | 96 | # Install ruby and ruby-lsp 97 | RUN apt update && apt install -y ruby-full \ 98 | && apt-get clean \ 99 | && rm -rf /var/lib/apt/lists/* 100 | 101 | RUN gem install ruby-lsp 102 | 103 | ENV HOME=/home/user 104 | 105 | # Download and run dotnet install script 106 | RUN curl -fsSL https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh -o dotnet-install.sh \ 107 | && chmod +x dotnet-install.sh \ 108 | && ./dotnet-install.sh --channel 8.0 --install-dir /home/user/.dotnet \ 109 | && ./dotnet-install.sh --channel 9.0 --install-dir /home/user/.dotnet \ 110 | && rm dotnet-install.sh 111 | 112 | # Add .NET to PATH 113 | ENV PATH="${PATH}:/home/user/.dotnet" 114 | ENV DOTNET_ROOT=/home/user/.dotnet 115 | 116 | # Install csharp-ls globally 117 | RUN dotnet tool install --global csharp-ls 118 | 119 | # Add .NET tools to PATH 120 | ENV PATH="${PATH}:/home/user/.dotnet/tools" 121 | 122 | 123 | COPY Cargo.toml . 124 | 125 | RUN mkdir src && echo "fn main() {}" > src/main.rs 126 | RUN touch src/lib.rs 127 | RUN cargo build --release 128 | 129 | RUN cargo install ast-grep --locked 130 | COPY ./src/ast_grep /usr/src/ast_grep 131 | 132 | # Document that the container listens on port 4444 133 | EXPOSE 4444 134 | 135 | # Set environment variables for logging and backtrace 136 | ENV RUST_LOG=debug,glob=info,ignore=info 137 | ENV RUST_BACKTRACE=1 138 | ENV RA_LOG="/tmp/rust-analyzer.log" 139 | 140 | CMD /bin/bash -c "touch Cargo.toml && cargo build --release" 141 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/config.yml: -------------------------------------------------------------------------------- 1 | ruleDirs: 2 | - rules 3 | languageGlobs: 4 | cpp: ['*.c', '*.h'] 5 | tsx: ['*.ts', '*.cts', '*.mts'] 6 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/cpp/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: cpp 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: type_identifier 8 | - kind: field_identifier 9 | 10 | 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/csharp/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/go/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: go 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: type_identifier 8 | - kind: field_identifier 9 | 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/java/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: java 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: type_identifier 8 | 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/javascript/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: javascript 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: property_identifier 8 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/php/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: php 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: name -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/python/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | 7 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/ruby/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: ruby 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: constant 8 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/rust/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: field_identifier 8 | - kind: type_identifier 9 | - kind: self 10 | - kind: super -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/identifier/rules/tsx/all-identifiers.yml: -------------------------------------------------------------------------------- 1 | id: all-identifiers 2 | language: tsx 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | - kind: property_identifier 8 | - kind: type_identifier 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | pub(crate) mod types; 3 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/config.yml: -------------------------------------------------------------------------------- 1 | ruleDirs: 2 | - rules 3 | languageGlobs: 4 | cpp: ['*.c', '*.h'] 5 | tsx: ['*.ts', '*.cts', '*.mts'] 6 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/csharp/all-references.yml: -------------------------------------------------------------------------------- 1 | id: all-references 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | not: 7 | any: 8 | - inside: 9 | any: 10 | - kind: delegate_declaration 11 | - kind: interface_declaration 12 | - kind: class_declaration 13 | - kind: struct_declaration 14 | - kind: method_declaration 15 | - kind: enum_declaration 16 | - kind: enum_member_declaration 17 | - kind: property_declaration 18 | - kind: namespace_declaration 19 | - kind: using_directive 20 | - kind: qualified_name 21 | inside: 22 | kind: using_directive 23 | - kind: qualified_name 24 | inside: 25 | kind: qualified_name 26 | inside: 27 | kind: using_directive 28 | - inside: 29 | any: 30 | - kind: variable_declarator 31 | - kind: parameter 32 | field: name 33 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/csharp/class-instantiation.yml: -------------------------------------------------------------------------------- 1 | id: class-instantiation 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: object_creation_expression 8 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/csharp/function-call.yml: -------------------------------------------------------------------------------- 1 | id: function-call 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | any: 7 | - inside: 8 | kind: invocation_expression 9 | - inside: 10 | kind: member_access_expression 11 | field: name 12 | inside: 13 | kind: invocation_expression 14 | - inside: 15 | kind: generic_name 16 | inside: 17 | kind: member_access_expression 18 | inside: 19 | kind: invocation_expression 20 | - inside: 21 | kind: generic_name 22 | inside: 23 | kind: invocation_expression 24 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/php/attribute-usage.yml: -------------------------------------------------------------------------------- 1 | id: attribute-usage 2 | language: php 3 | rule: 4 | kind: name 5 | inside: 6 | kind: attribute 7 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/php/function-call.yml: -------------------------------------------------------------------------------- 1 | id: function-call 2 | language: php 3 | rule: 4 | any: 5 | - kind: name 6 | pattern: $NAME 7 | inside: 8 | any: 9 | - kind: function_call_expression 10 | - kind: member_call_expression 11 | - kind: object_creation_expression 12 | - kind: name 13 | pattern: $NAME 14 | inside: 15 | kind: scoped_call_expression 16 | field: name 17 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/python/all-references.yml: -------------------------------------------------------------------------------- 1 | id: all-references 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | not: 7 | any: 8 | - inside: 9 | any: 10 | - kind: function_definition 11 | - kind: class_definition 12 | - kind: return_statement 13 | - kind: parameters 14 | - inside: 15 | any: 16 | - kind: assignment 17 | - kind: augmented_assignment 18 | field: left 19 | - inside: 20 | any: 21 | - kind: default_parameter 22 | - kind: typed_default_parameter 23 | field: name 24 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/python/decorator.yml: -------------------------------------------------------------------------------- 1 | id: decorator 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: decorator 8 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/python/function-call.yml: -------------------------------------------------------------------------------- 1 | id: function-call 2 | language: python 3 | rule: 4 | any: 5 | - kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: call 9 | - kind: identifier 10 | pattern: $NAME 11 | inside: 12 | kind: attribute 13 | field: attribute 14 | inside: 15 | kind: call 16 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/tsx/all-references.yml: -------------------------------------------------------------------------------- 1 | id: all-references 2 | language: tsx 3 | rule: 4 | any: 5 | - kind: identifier 6 | - kind: property_identifier 7 | - kind: type_identifier 8 | pattern: $NAME 9 | not: 10 | any: 11 | - inside: 12 | any: 13 | - kind: array_pattern 14 | - kind: variable_declarator 15 | - kind: required_parameter 16 | - kind: jsx_attribute 17 | - kind: jsx_closing_element 18 | - kind: import_specifier 19 | - kind: import_clause 20 | - kind: property_signature 21 | - kind: function_declaration 22 | - kind: interface_declaration 23 | - inside: 24 | kind: pair 25 | field: key 26 | - inside: 27 | any: 28 | - kind: for_in_statement 29 | - kind: assignment_expression 30 | field: left 31 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/tsx/component-render.yml: -------------------------------------------------------------------------------- 1 | id: component-render 2 | language: tsx 3 | rule: 4 | any: 5 | - kind: identifier 6 | pattern: $NAME 7 | inside: 8 | any: 9 | - kind: jsx_self_closing_element 10 | - kind: jsx_opening_element 11 | - kind: property_identifier 12 | pattern: $NAME 13 | inside: 14 | kind: member_expression 15 | not: 16 | inside: 17 | kind: member_expression 18 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/tsx/decorator.yml: -------------------------------------------------------------------------------- 1 | id: decorator 2 | language: tsx 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: decorator 8 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/reference/rules/tsx/function-call.yml: -------------------------------------------------------------------------------- 1 | id: function-call 2 | language: tsx 3 | rule: 4 | any: 5 | - kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: call_expression 9 | - kind: property_identifier 10 | pattern: $NAME 11 | inside: 12 | kind: member_expression 13 | inside: 14 | kind: call_expression 15 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/config.yml: -------------------------------------------------------------------------------- 1 | ruleDirs: 2 | - rules 3 | languageGlobs: 4 | cpp: ['*.c', '*.h'] 5 | tsx: ['*.ts', '*.cts', '*.mts'] 6 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/cpp/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: cpp 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: type_identifier 7 | inside: 8 | any: 9 | - kind: struct_specifier 10 | - kind: class_specifier 11 | pattern: $CONTEXT 12 | - kind: type_identifier 13 | inside: 14 | kind: union_specifier 15 | inside: 16 | kind: declaration 17 | pattern: $CONTEXT 18 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/cpp/function-declaration.yml: -------------------------------------------------------------------------------- 1 | id: function-declaration 2 | language: cpp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: function_declarator 8 | inside: 9 | kind: declaration 10 | pattern: $CONTEXT 11 | not: 12 | inside: 13 | kind: compound_statement 14 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/cpp/function-definition.yml: -------------------------------------------------------------------------------- 1 | id: function-definition 2 | language: cpp 3 | rule: 4 | any: 5 | - kind: identifier 6 | - kind: field_identifier 7 | pattern: $NAME 8 | inside: 9 | kind: function_declarator 10 | inside: 11 | kind: function_definition 12 | pattern: $CONTEXT 13 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/cpp/type.yml: -------------------------------------------------------------------------------- 1 | id: type 2 | language: cpp 3 | rule: 4 | kind: type_identifier 5 | pattern: $NAME 6 | inside: 7 | any: 8 | - kind: type_definition 9 | - kind: enum_specifier 10 | pattern: $CONTEXT 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: class_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/enum.yml: -------------------------------------------------------------------------------- 1 | id: enum 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: enum_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/field.yml: -------------------------------------------------------------------------------- 1 | id: field 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | any: 7 | - inside: 8 | kind: variable_declarator 9 | inside: 10 | kind: variable_declaration 11 | inside: 12 | kind: field_declaration 13 | pattern: $CONTEXT 14 | - inside: 15 | kind: variable_declaration 16 | inside: 17 | kind: event_field_declaration 18 | pattern: $CONTEXT 19 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/interface.yml: -------------------------------------------------------------------------------- 1 | id: interface 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: interface_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/local-variable.yml: -------------------------------------------------------------------------------- 1 | id: local-variable 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | any: 7 | - inside: 8 | any: 9 | - kind: enum_member_declaration 10 | - inside: 11 | kind: assignment_expression 12 | field: left 13 | - inside: 14 | any: 15 | - kind: variable_declarator 16 | not: 17 | inside: 18 | kind: variable_declaration 19 | inside: 20 | kind: field_declaration 21 | - kind: parameter 22 | field: name 23 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: method_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/csharp/property.yml: -------------------------------------------------------------------------------- 1 | id: property 2 | language: csharp 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: property_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/go/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: go 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: function_declaration 8 | field: name 9 | pattern: $CONTEXT 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/go/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: go 3 | rule: 4 | kind: field_identifier 5 | pattern: $NAME 6 | inside: 7 | kind: method_declaration 8 | field: name 9 | pattern: $CONTEXT -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/go/type.yml: -------------------------------------------------------------------------------- 1 | id: type 2 | language: go 3 | rule: 4 | kind: type_identifier 5 | pattern: $NAME 6 | inside: 7 | kind: type_spec 8 | field: name 9 | pattern: $CONTEXT 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/java/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: java 3 | 4 | rule: 5 | kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: class_declaration 9 | pattern: $CONTEXT 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/java/interface.yml: -------------------------------------------------------------------------------- 1 | id: interface 2 | language: java 3 | 4 | rule: 5 | kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: interface_declaration 9 | pattern: $CONTEXT 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/java/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: java 3 | 4 | rule: 5 | kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: method_declaration 9 | pattern: $CONTEXT 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/javascript/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: javascript 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | any: 8 | - kind: class_declaration 9 | - kind: class 10 | pattern: $CONTEXT 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/javascript/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: javascript 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | inside: 8 | any: 9 | - kind: function_declaration 10 | - kind: function_expression 11 | - kind: generator_function 12 | - kind: generator_function_declaration 13 | - kind: variable_declarator 14 | inside: 15 | kind: lexical_declaration 16 | has: 17 | kind: arrow_function 18 | - kind: assignment_expression 19 | has: 20 | any: 21 | - kind: arrow_function 22 | pattern: $CONTEXT 23 | - kind: property_identifier 24 | inside: 25 | kind: pair 26 | has: 27 | any: 28 | - kind: function_expression 29 | - kind: arrow_function 30 | pattern: $CONTEXT 31 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/javascript/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: javascript 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | inside: 8 | kind: method_definition 9 | pattern: $CONTEXT 10 | - kind: property_identifier 11 | inside: 12 | kind: method_definition 13 | pattern: $CONTEXT 14 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/javascript/variable.yml: -------------------------------------------------------------------------------- 1 | id: variable 2 | language: javascript 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | pattern: $CONTEXT 8 | kind: variable_declarator 9 | not: 10 | has: 11 | any: 12 | - kind: function_declaration 13 | - kind: arrow_function 14 | inside: 15 | kind: lexical_declaration 16 | inside: 17 | any: 18 | - kind: export_statement 19 | - kind: program 20 | field: name 21 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: class_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/constant.yml: -------------------------------------------------------------------------------- 1 | id: constant 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: const_element 8 | inside: 9 | kind: const_declaration 10 | pattern: $CONTEXT 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/enum.yml: -------------------------------------------------------------------------------- 1 | id: enum 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: enum_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: function_definition 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/global.yml: -------------------------------------------------------------------------------- 1 | id: global 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: variable_name 8 | inside: 9 | kind: global_declaration 10 | pattern: $CONTEXT 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/interface.yml: -------------------------------------------------------------------------------- 1 | id: interface 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: interface_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: method_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/property.yml: -------------------------------------------------------------------------------- 1 | id: property 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: variable_name 8 | inside: 9 | kind: property_element 10 | inside: 11 | kind: property_declaration 12 | pattern: $CONTEXT 13 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/php/trait.yml: -------------------------------------------------------------------------------- 1 | id: trait 2 | language: php 3 | rule: 4 | kind: name 5 | pattern: $NAME 6 | inside: 7 | kind: trait_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/python/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: class_definition 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/python/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | any: 7 | - inside: 8 | kind: function_definition 9 | inside: 10 | kind: decorated_definition 11 | pattern: $CONTEXT 12 | - inside: 13 | kind: function_definition 14 | pattern: $CONTEXT 15 | not: 16 | inside: 17 | kind: decorated_definition 18 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/python/local-variable.yml: -------------------------------------------------------------------------------- 1 | id: local-variable 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | kind: assignment 8 | field: left 9 | pattern: $CONTEXT 10 | not: 11 | inside: 12 | kind: expression_statement 13 | inside: 14 | any: 15 | - kind: block 16 | inside: 17 | kind: class_definition 18 | - kind: module 19 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/python/variable.yml: -------------------------------------------------------------------------------- 1 | id: variable 2 | language: python 3 | utils: 4 | variable_def: 5 | kind: assignment 6 | inside: 7 | kind: expression_statement 8 | pattern: $CONTEXT 9 | any: 10 | - inside: 11 | kind: module 12 | - inside: 13 | kind: block 14 | inside: 15 | kind: class_definition 16 | rule: 17 | kind: identifier 18 | pattern: $NAME 19 | any: 20 | - inside: 21 | matches: variable_def 22 | - inside: 23 | kind: pattern_list 24 | inside: 25 | matches: variable_def 26 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/ruby/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: ruby 3 | rule: 4 | any: 5 | - kind: constant 6 | pattern: $NAME 7 | inside: 8 | any: 9 | - kind: class 10 | - kind: singleton_class 11 | pattern: $CONTEXT 12 | field: name 13 | - kind: identifier 14 | pattern: $NAME 15 | inside: 16 | kind: scope_resolution 17 | field: name 18 | inside: 19 | any: 20 | - kind: class 21 | - kind: singleton_class 22 | pattern: $CONTEXT 23 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/ruby/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: ruby 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | inside: 7 | any: 8 | - kind: method 9 | - kind: singleton_method 10 | - kind: alias 11 | pattern: $CONTEXT 12 | field: name -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/ruby/module.yml: -------------------------------------------------------------------------------- 1 | id: module 2 | language: ruby 3 | rule: 4 | any: 5 | - kind: constant 6 | pattern: $NAME 7 | inside: 8 | kind: module 9 | pattern: $CONTEXT 10 | field: name 11 | - kind: identifier 12 | pattern: $NAME 13 | inside: 14 | kind: scope_resolution 15 | field: name 16 | inside: 17 | kind: module 18 | pattern: $CONTEXT 19 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/enum.yml: -------------------------------------------------------------------------------- 1 | id: enum 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | kind: type_identifier 6 | inside: 7 | kind: enum_item 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | kind: identifier 6 | inside: 7 | pattern: $CONTEXT 8 | kind: function_item 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/implementation.yml: -------------------------------------------------------------------------------- 1 | id: implementation 2 | language: rust 3 | rule: 4 | any: 5 | - kind: type_identifier 6 | - kind: scoped_type_identifier 7 | inside: 8 | kind: impl_item 9 | pattern: $CONTEXT 10 | nthChild: 1 11 | pattern: $NAME 12 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/struct.yml: -------------------------------------------------------------------------------- 1 | id: struct 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | kind: type_identifier 6 | inside: 7 | kind: struct_item 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/trait.yml: -------------------------------------------------------------------------------- 1 | id: trait 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | kind: type_identifier 6 | inside: 7 | kind: trait_item 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/rust/type.yml: -------------------------------------------------------------------------------- 1 | id: type 2 | language: rust 3 | rule: 4 | pattern: $NAME 5 | kind: type_identifier 6 | inside: 7 | kind: type_item 8 | pattern: $CONTEXT 9 | nthChild: 1 10 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/class.yml: -------------------------------------------------------------------------------- 1 | id: class 2 | language: tsx 3 | rule: 4 | kind: type_identifier 5 | pattern: $NAME 6 | inside: 7 | any: 8 | - kind: class_declaration 9 | - kind: class 10 | pattern: $CONTEXT 11 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: tsx 3 | rule: 4 | pattern: $NAME 5 | any: 6 | - kind: identifier 7 | inside: 8 | any: 9 | - kind: function_declaration 10 | - kind: function_expression 11 | - kind: generator_function 12 | - kind: generator_function_declaration 13 | - kind: variable_declarator 14 | inside: 15 | kind: lexical_declaration 16 | has: 17 | kind: arrow_function 18 | field: value 19 | 20 | - kind: assignment_expression 21 | has: 22 | kind: arrow_function 23 | pattern: $CONTEXT 24 | - kind: property_identifier 25 | inside: 26 | kind: pair 27 | has: 28 | kind: arrow_function 29 | pattern: $CONTEXT 30 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/interface.yml: -------------------------------------------------------------------------------- 1 | id: interface 2 | language: tsx 3 | rule: 4 | pattern: $NAME 5 | kind: type_identifier 6 | inside: 7 | kind: interface_declaration 8 | pattern: $CONTEXT 9 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/local-variable.yml: -------------------------------------------------------------------------------- 1 | id: local-variable 2 | language: tsx 3 | utils: 4 | global_variable: 5 | kind: variable_declarator 6 | not: 7 | has: 8 | any: 9 | - kind: function_declaration 10 | - kind: arrow_function 11 | inside: 12 | any: 13 | - kind: lexical_declaration 14 | inside: 15 | kind: export_statement 16 | pattern: $CONTEXT 17 | - kind: lexical_declaration 18 | pattern: $CONTEXT 19 | inside: 20 | kind: program 21 | rule: 22 | pattern: $NAME 23 | any: 24 | - kind: identifier 25 | inside: 26 | kind: variable_declarator 27 | field: name 28 | not: 29 | any: 30 | - matches: global_variable 31 | - has: 32 | kind: arrow_function 33 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/method.yml: -------------------------------------------------------------------------------- 1 | id: method 2 | language: tsx 3 | rule: 4 | pattern: $NAME 5 | kind: property_identifier 6 | inside: 7 | any: 8 | - kind: method_definition 9 | - kind: public_field_definition 10 | has: 11 | any: 12 | - kind: arrow_function 13 | - kind: function_expression 14 | pattern: $CONTEXT 15 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/rules/tsx/variable.yml: -------------------------------------------------------------------------------- 1 | id: variable 2 | language: tsx 3 | utils: 4 | global_variable: 5 | kind: variable_declarator 6 | not: 7 | has: 8 | any: 9 | - kind: function_declaration 10 | - kind: arrow_function 11 | inside: 12 | any: 13 | - kind: lexical_declaration 14 | inside: 15 | kind: export_statement 16 | pattern: $CONTEXT 17 | - kind: lexical_declaration 18 | pattern: $CONTEXT 19 | inside: 20 | kind: program 21 | rule: 22 | pattern: $NAME 23 | any: 24 | - kind: identifier 25 | inside: 26 | matches: global_variable 27 | - kind: shorthand_property_identifier_pattern 28 | inside: 29 | kind: object_pattern 30 | inside: 31 | matches: global_variable 32 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/utils/python/function-call.yml: -------------------------------------------------------------------------------- 1 | id: function-call 2 | language: python 3 | rule: 4 | any: 5 | - kind: identifier 6 | pattern: $NAME 7 | inside: 8 | kind: call 9 | - kind: identifier 10 | pattern: $NAME 11 | inside: 12 | kind: attribute 13 | field: attribute 14 | inside: 15 | kind: call 16 | -------------------------------------------------------------------------------- /lsproxy/src/ast_grep/symbol/utils/python/function.yml: -------------------------------------------------------------------------------- 1 | id: function 2 | language: python 3 | rule: 4 | kind: identifier 5 | pattern: $NAME 6 | any: 7 | - inside: 8 | kind: function_definition 9 | inside: 10 | kind: decorated_definition 11 | pattern: $CONTEXT 12 | - inside: 13 | kind: function_definition 14 | pattern: $CONTEXT 15 | not: 16 | inside: 17 | kind: decorated_definition 18 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/definitions_in_file.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::{Data, Query}; 2 | use actix_web::HttpResponse; 3 | use log::info; 4 | 5 | use crate::api_types::{ErrorResponse, FileSymbolsRequest, Symbol}; 6 | use crate::AppState; 7 | 8 | /// Get symbols in a specific file (uses ast-grep) 9 | /// 10 | /// Returns a list of symbols (functions, classes, variables, etc.) defined in the specified file. 11 | /// 12 | /// Only the variabels defined at the file level are included. 13 | /// 14 | /// The returned positions point to the start of the symbol's identifier. 15 | /// 16 | /// e.g. for `User` on line 0 of `src/main.py`: 17 | /// ``` 18 | /// 0: class User: 19 | /// _________^ 20 | /// 1: def __init__(self, name, age): 21 | /// 2: self.name = name 22 | /// 3: self.age = age 23 | /// ``` 24 | #[utoipa::path( 25 | get, 26 | path = "/symbol/definitions-in-file", 27 | tag = "symbol", 28 | params(FileSymbolsRequest), 29 | responses( 30 | (status = 200, description = "Symbols retrieved successfully", body = Vec), 31 | (status = 400, description = "Bad request"), 32 | (status = 500, description = "Internal server error") 33 | ) 34 | )] 35 | pub async fn definitions_in_file( 36 | data: Data, 37 | info: Query, 38 | ) -> HttpResponse { 39 | info!( 40 | "Received definitions in file request for file: {}", 41 | info.file_path 42 | ); 43 | 44 | match data 45 | .manager 46 | .definitions_in_file_ast_grep(&info.file_path) 47 | .await 48 | { 49 | Ok(symbols) => { 50 | let symbol_response: Vec = symbols 51 | .into_iter() 52 | .filter(|s| s.rule_id != "local-variable") 53 | .map(Symbol::from) 54 | .collect(); 55 | HttpResponse::Ok().json(symbol_response) 56 | } 57 | Err(e) => HttpResponse::BadRequest().json(ErrorResponse { 58 | error: format!("Couldn't get symbols: {}", e), 59 | }), 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use super::*; 66 | 67 | use actix_web::http::StatusCode; 68 | 69 | use crate::api_types::{FilePosition, FileRange, Position, Range, Symbol}; 70 | use crate::initialize_app_state; 71 | use crate::test_utils::{python_sample_path, TestContext}; 72 | 73 | #[tokio::test] 74 | async fn test_python_file_symbols() -> Result<(), Box> { 75 | let _context = TestContext::setup(&python_sample_path(), false).await?; 76 | let state = initialize_app_state().await?; 77 | 78 | let mock_request = Query(FileSymbolsRequest { 79 | file_path: String::from("main.py"), 80 | }); 81 | 82 | let response = definitions_in_file(state, mock_request).await; 83 | 84 | assert_eq!(response.status(), StatusCode::OK); 85 | assert_eq!( 86 | response.headers().get("content-type").unwrap(), 87 | "application/json" 88 | ); 89 | 90 | // Check the body 91 | let body = response.into_body(); 92 | let bytes = actix_web::body::to_bytes(body).await.unwrap(); 93 | let file_symbols_response: Vec = serde_json::from_slice(&bytes).unwrap(); 94 | 95 | let expected = vec![ 96 | Symbol { 97 | name: String::from("plot_path"), 98 | kind: String::from("function"), 99 | identifier_position: FilePosition { 100 | path: String::from("main.py"), 101 | position: Position { 102 | line: 6, 103 | character: 4, 104 | }, 105 | }, 106 | file_range: FileRange { 107 | path: String::from("main.py"), 108 | range: Range { 109 | start: Position { 110 | line: 5, 111 | character: 0, 112 | }, 113 | end: Position { 114 | line: 12, 115 | character: 14, 116 | }, 117 | }, 118 | }, 119 | }, 120 | Symbol { 121 | name: String::from("main"), 122 | kind: String::from("function"), 123 | identifier_position: FilePosition { 124 | path: String::from("main.py"), 125 | position: Position { 126 | line: 14, 127 | character: 4, 128 | }, 129 | }, 130 | file_range: FileRange { 131 | path: String::from("main.py"), 132 | range: Range { 133 | start: Position { 134 | line: 14, 135 | character: 0, 136 | }, 137 | end: Position { 138 | line: 19, 139 | character: 28, 140 | }, 141 | }, 142 | }, 143 | }, 144 | ]; 145 | 146 | assert_eq!(expected, file_symbols_response); 147 | Ok(()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/error.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types::ErrorResponse; 2 | use crate::lsp::manager::LspManagerError; 3 | use actix_web::HttpResponse; 4 | 5 | pub trait IntoHttpResponse { 6 | fn into_http_response(self) -> HttpResponse; 7 | } 8 | 9 | impl IntoHttpResponse for LspManagerError { 10 | fn into_http_response(self) -> HttpResponse { 11 | log::error!("LSP error: {}", self); 12 | match self { 13 | Self::FileNotFound(path) => HttpResponse::BadRequest().json(ErrorResponse { 14 | error: format!("File not found: {}", path), 15 | }), 16 | Self::LspClientNotFound(lang) => { 17 | HttpResponse::InternalServerError().json(ErrorResponse { 18 | error: format!("LSP client not found for {:?}", lang), 19 | }) 20 | } 21 | Self::InternalError(msg) => HttpResponse::InternalServerError().json(ErrorResponse { 22 | error: format!("Internal error: {}", msg), 23 | }), 24 | Self::UnsupportedFileType(path) => HttpResponse::BadRequest().json(ErrorResponse { 25 | error: format!("Unsupported file type: {}", path), 26 | }), 27 | Self::NotImplemented(msg) => HttpResponse::NotImplemented().json(ErrorResponse { 28 | error: format!("Not implemented: {}", msg), 29 | }), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/health.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types::{HealthResponse, SupportedLanguages}; 2 | use crate::AppState; 3 | use actix_web::web::Data; 4 | use actix_web::HttpResponse; 5 | use std::collections::HashMap; 6 | 7 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 8 | 9 | /// Get health status of the LSP proxy service 10 | /// 11 | /// Returns the service status, version and language server availability 12 | #[utoipa::path( 13 | get, 14 | path = "/system/health", 15 | tag = "system", 16 | responses( 17 | (status = 200, description = "Health check successful", body = HealthResponse), 18 | (status = 500, description = "Internal server error") 19 | ) 20 | )] 21 | pub async fn health_check(data: Data) -> HttpResponse { 22 | let mut languages = HashMap::new(); 23 | for lang in [ 24 | SupportedLanguages::Python, 25 | SupportedLanguages::TypeScriptJavaScript, 26 | SupportedLanguages::Rust, 27 | SupportedLanguages::CPP, 28 | SupportedLanguages::CSharp, 29 | SupportedLanguages::Java, 30 | SupportedLanguages::Golang, 31 | SupportedLanguages::PHP, 32 | ] { 33 | languages.insert(lang, data.manager.get_client(lang).is_some()); 34 | } 35 | 36 | HttpResponse::Ok().json(HealthResponse { 37 | status: "ok".to_string(), 38 | version: VERSION.to_string(), 39 | languages, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/list_files.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::Data; 2 | use actix_web::HttpResponse; 3 | use log::error; 4 | 5 | use crate::handlers::error::IntoHttpResponse; 6 | use crate::AppState; 7 | 8 | /// Get a list of all files in the workspace 9 | /// 10 | /// Returns an array of file paths for all files in the current workspace. 11 | /// 12 | /// This is a convenience endpoint that does not use the underlying Language Servers directly, but it does apply the same filtering. 13 | #[utoipa::path( 14 | get, 15 | path = "/workspace/list-files", 16 | tag = "workspace", 17 | responses( 18 | (status = 200, description = "Workspace files retrieved successfully", body = Vec), 19 | (status = 400, description = "Bad request"), 20 | (status = 500, description = "Internal server error") 21 | ) 22 | )] 23 | pub async fn list_files(data: Data) -> HttpResponse { 24 | let files = data.manager.list_files().await; 25 | match files { 26 | Ok(files) => HttpResponse::Ok().json(files), 27 | Err(e) => { 28 | error!("Failed to get workspace files: {}", e); 29 | e.into_http_response() 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod test { 36 | use super::*; 37 | 38 | use actix_web::http::StatusCode; 39 | 40 | use crate::initialize_app_state; 41 | use crate::test_utils::{python_sample_path, TestContext}; 42 | 43 | #[tokio::test] 44 | async fn test_python_workspace_files() -> Result<(), Box> { 45 | let _context = TestContext::setup(&python_sample_path(), false).await?; 46 | let state = initialize_app_state().await?; 47 | 48 | let response = list_files(state).await; 49 | 50 | assert_eq!(response.status(), StatusCode::OK); 51 | assert_eq!( 52 | response.headers().get("content-type").unwrap(), 53 | "application/json" 54 | ); 55 | 56 | // Check the body 57 | let body = response.into_body(); 58 | let bytes = actix_web::body::to_bytes(body).await.unwrap(); 59 | let mut workspace_files_response: Vec = serde_json::from_slice(&bytes).unwrap(); 60 | 61 | let mut expected = [ 62 | "__init__.py", 63 | "decorators.py", 64 | "graph.py", 65 | "main.py", 66 | "search.py", 67 | ]; 68 | expected.sort(); 69 | workspace_files_response.sort(); 70 | assert_eq!(workspace_files_response, expected); 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | mod definitions_in_file; 2 | mod error; 3 | mod find_definition; 4 | mod find_identifier; 5 | mod find_referenced_symbols; 6 | mod find_references; 7 | mod health; 8 | mod list_files; 9 | mod read_source_code; 10 | 11 | mod utils; 12 | pub use self::{ 13 | definitions_in_file::*, find_definition::*, find_identifier::*, find_referenced_symbols::*, 14 | find_references::*, health::*, list_files::*, read_source_code::*, 15 | }; 16 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/read_source_code.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types::{ErrorResponse, ReadSourceCodeRequest}; 2 | use actix_web::web::{Data, Json}; 3 | use actix_web::HttpResponse; 4 | use log::{error, info}; 5 | use lsp_types::{Position as LspPosition, Range as LspRange}; 6 | use serde::Serialize; 7 | use utoipa::ToSchema; 8 | 9 | use crate::AppState; 10 | 11 | #[derive(Debug, Serialize, ToSchema)] 12 | pub struct ReadSourceCodeResponse { 13 | pub source_code: String, 14 | } 15 | 16 | /// Read source code from a file in the workspace 17 | /// 18 | /// Returns the contents of the specified file. 19 | #[utoipa::path( 20 | post, 21 | path = "/workspace/read-source-code", 22 | tag = "workspace", 23 | request_body = ReadSourceCodeRequest, 24 | responses( 25 | (status = 200, description = "Source code retrieved successfully", body = ReadSourceCodeResponse), 26 | (status = 400, description = "Bad request"), 27 | (status = 500, description = "Internal server error") 28 | ) 29 | )] 30 | pub async fn read_source_code( 31 | data: Data, 32 | info: Json, 33 | ) -> HttpResponse { 34 | info!("Reading source code from file: {}", info.path); 35 | 36 | let lsp_range = info.range.as_ref().map(|range| { 37 | LspRange::new( 38 | LspPosition { 39 | line: range.start.line, 40 | character: range.start.character, 41 | }, 42 | LspPosition { 43 | line: range.end.line, 44 | character: range.end.character, 45 | }, 46 | ) 47 | }); 48 | 49 | match data.manager.read_source_code(&info.path, lsp_range).await { 50 | Ok(source_code) => HttpResponse::Ok().json(ReadSourceCodeResponse { source_code }), 51 | Err(e) => { 52 | error!("Failed to read source code: {:?}", e); 53 | HttpResponse::InternalServerError().json(ErrorResponse { 54 | error: format!("Failed to read source code: {}", e), 55 | }) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lsproxy/src/handlers/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types::{FilePosition, Identifier}; 2 | 3 | #[derive(Debug)] 4 | pub enum PositionError { 5 | IdentifierNotFound { closest: Vec }, 6 | } 7 | 8 | impl std::fmt::Display for PositionError { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | match self { 11 | PositionError::IdentifierNotFound { closest } => { 12 | write!( 13 | f, 14 | "No identifier found at position. Closest matches: {:?}", 15 | closest 16 | ) 17 | } 18 | } 19 | } 20 | } 21 | 22 | impl std::error::Error for PositionError {} 23 | 24 | pub(crate) async fn find_identifier_at_position<'a>( 25 | identifiers: Vec, 26 | position: &FilePosition, 27 | ) -> Result { 28 | if let Some(exact_match) = identifiers 29 | .iter() 30 | .find(|i| i.file_range.contains(position.clone())) 31 | { 32 | return Ok(exact_match.clone()); 33 | } 34 | 35 | // Find closest matches by calculating distances 36 | let mut with_distances: Vec<_> = identifiers 37 | .iter() 38 | .map(|id| { 39 | let start_line_diff = 40 | (id.file_range.range.start.line as i32 - position.position.line as i32).abs(); 41 | let start_char_diff = (id.file_range.range.start.character as i32 42 | - position.position.character as i32) 43 | .abs(); 44 | let start_distance = start_line_diff * 100 + start_char_diff; 45 | 46 | let end_line_diff = 47 | (id.file_range.range.end.line as i32 - position.position.line as i32).abs(); 48 | let end_char_diff = (id.file_range.range.end.character as i32 49 | - position.position.character as i32) 50 | .abs(); 51 | let end_distance = end_line_diff * 100 + end_char_diff; 52 | 53 | (id.clone(), (start_distance.min(end_distance)) as f64) 54 | }) 55 | .collect(); 56 | 57 | with_distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); 58 | 59 | let closest = with_distances 60 | .into_iter() 61 | .take(3) 62 | .map(|(id, _)| id) 63 | .collect(); 64 | 65 | Err(PositionError::IdentifierNotFound { closest }) 66 | } 67 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/json_rpc.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use std::collections::HashMap; 4 | use std::error::Error; 5 | use std::fmt; 6 | use std::sync::atomic::{AtomicU64, Ordering}; 7 | use std::sync::Arc; 8 | use tokio::sync::broadcast::{channel, Receiver, Sender}; 9 | use tokio::sync::Mutex; 10 | 11 | pub trait JsonRpc: Send + Sync { 12 | fn create_success_response(&self, id: u64) -> String; 13 | fn create_request(&self, method: &str, params: Option) -> (u64, String); 14 | fn create_notification(&self, method: &str, params: Value) -> String; 15 | fn parse_message(&self, data: &str) -> Result; 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize, Clone)] 19 | pub struct JsonRpcMessage { 20 | pub jsonrpc: String, 21 | pub id: Option, 22 | pub method: Option, 23 | pub params: Option, 24 | pub result: Option, 25 | pub error: Option, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize, Clone)] 29 | pub struct InnerMessage { 30 | pub message: String, 31 | pub r#type: String, 32 | } 33 | #[derive(Debug, Serialize, Deserialize, Clone)] 34 | pub struct JsonRpcError { 35 | pub code: i32, 36 | pub message: String, 37 | pub data: Option, 38 | } 39 | 40 | impl fmt::Display for JsonRpcError { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!(f, "Error {}: {}", self.code, self.message) 43 | } 44 | } 45 | 46 | impl std::error::Error for JsonRpcError {} 47 | 48 | #[derive(Clone)] 49 | pub struct JsonRpcHandler { 50 | id_counter: Arc, 51 | } 52 | 53 | impl JsonRpcHandler { 54 | pub fn new() -> Self { 55 | Self { 56 | id_counter: Arc::new(AtomicU64::new(0)), 57 | } 58 | } 59 | } 60 | 61 | impl JsonRpc for JsonRpcHandler { 62 | fn create_success_response(&self, id: u64) -> String { 63 | serde_json::json!({ 64 | "jsonrpc": "2.0", 65 | "id": id, 66 | "result": null 67 | }) 68 | .to_string() 69 | } 70 | 71 | fn create_request(&self, method: &str, params: Option) -> (u64, String) { 72 | let id = self.id_counter.fetch_add(1, Ordering::Relaxed); 73 | let request = serde_json::json!({ 74 | "jsonrpc": "2.0", 75 | "id": id, 76 | "method": method, 77 | "params": params.unwrap_or(serde_json::Value::Null) 78 | }) 79 | .to_string(); 80 | (id, request) 81 | } 82 | 83 | fn create_notification(&self, method: &str, params: Value) -> String { 84 | serde_json::json!({ 85 | "jsonrpc": "2.0", 86 | "method": method, 87 | "params": params 88 | }) 89 | .to_string() 90 | } 91 | 92 | fn parse_message(&self, data: &str) -> Result { 93 | serde_json::from_str(data).map_err(|e| JsonRpcError { 94 | code: -32700, 95 | message: e.to_string(), 96 | data: None, 97 | }) 98 | } 99 | } 100 | 101 | #[derive(Clone, Eq, Hash, PartialEq)] 102 | pub struct ExpectedMessageKey { 103 | pub method: String, 104 | pub params: Value, 105 | } 106 | 107 | #[derive(Clone)] 108 | pub struct PendingRequests { 109 | request_channels: Arc>>>, 110 | notification_channels: Arc>>>, 111 | } 112 | 113 | impl PendingRequests { 114 | pub fn new() -> Self { 115 | Self { 116 | request_channels: Arc::new(Mutex::new(HashMap::new())), 117 | notification_channels: Arc::new(Mutex::new(HashMap::new())), 118 | } 119 | } 120 | 121 | pub async fn add_request( 122 | &self, 123 | id: u64, 124 | ) -> Result, Box> { 125 | let (tx, rx) = channel::(16); 126 | self.request_channels.lock().await.insert(id, tx); 127 | Ok(rx) 128 | } 129 | 130 | pub async fn remove_request( 131 | &self, 132 | id: u64, 133 | ) -> Result>, Box> { 134 | Ok(self.request_channels.lock().await.remove(&id)) 135 | } 136 | 137 | pub async fn add_notification( 138 | &self, 139 | expected_message: ExpectedMessageKey, 140 | ) -> Result, Box> { 141 | let (tx, rx) = channel::(16); 142 | self.notification_channels 143 | .lock() 144 | .await 145 | .insert(expected_message, tx); 146 | Ok(rx) 147 | } 148 | 149 | pub async fn remove_notification( 150 | &self, 151 | pattern: ExpectedMessageKey, 152 | ) -> Option> { 153 | self.notification_channels.lock().await.remove(&pattern) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/csharp.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}, 3 | utils::workspace_documents::{ 4 | DidOpenConfiguration, WorkspaceDocumentsHandler, CSHARP_FILE_PATTERNS, CSHARP_ROOT_FILES, 5 | DEFAULT_EXCLUDE_PATTERNS, 6 | }, 7 | }; 8 | use async_trait::async_trait; 9 | use log::error; 10 | use lsp_types::{InitializeParams, Url}; 11 | use notify_debouncer_mini::DebouncedEvent; 12 | use std::{error::Error, path::Path, process::Stdio}; 13 | use tokio::{process::Command, sync::broadcast::Receiver}; 14 | pub struct CSharpClient { 15 | process: ProcessHandler, 16 | json_rpc: JsonRpcHandler, 17 | workspace_documents: WorkspaceDocumentsHandler, 18 | pending_requests: PendingRequests, 19 | } 20 | #[async_trait] 21 | impl LspClient for CSharpClient { 22 | fn get_process(&mut self) -> &mut ProcessHandler { 23 | &mut self.process 24 | } 25 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 26 | &mut self.json_rpc 27 | } 28 | fn get_root_files(&mut self) -> Vec { 29 | CSHARP_ROOT_FILES.iter().map(|&s| s.to_owned()).collect() 30 | } 31 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 32 | &mut self.workspace_documents 33 | } 34 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 35 | &mut self.pending_requests 36 | } 37 | 38 | async fn get_initialize_params( 39 | &mut self, 40 | root_path: String, 41 | ) -> Result> { 42 | let workspace_folders = self.find_workspace_folders(root_path.clone()).await?; 43 | Ok(InitializeParams { 44 | capabilities: self.get_capabilities(), 45 | workspace_folders: Some(workspace_folders.clone()), 46 | root_uri: Some(Url::from_file_path(root_path).unwrap()), 47 | ..Default::default() 48 | }) 49 | } 50 | } 51 | impl CSharpClient { 52 | pub async fn new( 53 | root_path: &str, 54 | watch_events_rx: Receiver, 55 | ) -> Result> { 56 | let debug_file = std::fs::File::create("/tmp/csharp.log")?; 57 | let process = Command::new("csharp-ls") 58 | .current_dir(root_path) 59 | .stdin(Stdio::piped()) 60 | .stdout(Stdio::piped()) 61 | .stderr(debug_file) 62 | .spawn() 63 | .map_err(|e| { 64 | error!("Failed to start csharp-ls process: {}", e); 65 | Box::new(e) as Box 66 | })?; 67 | let process_handler = ProcessHandler::new(process) 68 | .await 69 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 70 | let json_rpc_handler = JsonRpcHandler::new(); 71 | let workspace_documents = WorkspaceDocumentsHandler::new( 72 | Path::new(root_path), 73 | CSHARP_FILE_PATTERNS 74 | .iter() 75 | .map(|&s| s.to_string()) 76 | .collect(), 77 | DEFAULT_EXCLUDE_PATTERNS 78 | .iter() 79 | .map(|&s| s.to_string()) 80 | .collect(), 81 | watch_events_rx, 82 | DidOpenConfiguration::Lazy, 83 | ); 84 | let pending_requests = PendingRequests::new(); 85 | Ok(Self { 86 | process: process_handler, 87 | json_rpc: json_rpc_handler, 88 | workspace_documents, 89 | pending_requests, 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/golang.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}, 3 | utils::workspace_documents::{ 4 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, 5 | GOLANG_FILE_PATTERNS, GOLANG_ROOT_FILES, 6 | }, 7 | }; 8 | use async_trait::async_trait; 9 | use log::error; 10 | use lsp_types::InitializeParams; 11 | use notify_debouncer_mini::DebouncedEvent; 12 | use std::{error::Error, path::Path, process::Stdio}; 13 | use tokio::{process::Command, sync::broadcast::Receiver}; 14 | pub struct GoplsClient { 15 | process: ProcessHandler, 16 | json_rpc: JsonRpcHandler, 17 | workspace_documents: WorkspaceDocumentsHandler, 18 | pending_requests: PendingRequests, 19 | } 20 | #[async_trait] 21 | impl LspClient for GoplsClient { 22 | fn get_process(&mut self) -> &mut ProcessHandler { 23 | &mut self.process 24 | } 25 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 26 | &mut self.json_rpc 27 | } 28 | fn get_root_files(&mut self) -> Vec { 29 | GOLANG_ROOT_FILES.iter().map(|&s| s.to_owned()).collect() 30 | } 31 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 32 | &mut self.workspace_documents 33 | } 34 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 35 | &mut self.pending_requests 36 | } 37 | 38 | async fn get_initialize_params( 39 | &mut self, 40 | root_path: String, 41 | ) -> Result> { 42 | let workspace_folders = self.find_workspace_folders(root_path.clone()).await?; 43 | Ok(InitializeParams { 44 | capabilities: self.get_capabilities(), 45 | workspace_folders: Some(workspace_folders.clone()), 46 | root_uri: workspace_folders.first().map(|f| f.uri.clone()), // <--------- Not default behavior 47 | ..Default::default() 48 | }) 49 | } 50 | } 51 | impl GoplsClient { 52 | pub async fn new( 53 | root_path: &str, 54 | watch_events_rx: Receiver, 55 | ) -> Result> { 56 | let process = Command::new("gopls") 57 | .arg("-mode=stdio") 58 | .arg("-vv") 59 | .arg("-logfile=/tmp/gopls.log") 60 | .arg("-rpc.trace") 61 | .current_dir(root_path) 62 | .stdin(Stdio::piped()) 63 | .stdout(Stdio::piped()) 64 | .stderr(Stdio::piped()) 65 | .spawn() 66 | .map_err(|e| { 67 | error!("Failed to start gopls process: {}", e); 68 | Box::new(e) as Box 69 | })?; 70 | let process_handler = ProcessHandler::new(process) 71 | .await 72 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 73 | let json_rpc_handler = JsonRpcHandler::new(); 74 | let workspace_documents = WorkspaceDocumentsHandler::new( 75 | Path::new(root_path), 76 | GOLANG_FILE_PATTERNS 77 | .iter() 78 | .map(|&s| s.to_string()) 79 | .collect(), 80 | DEFAULT_EXCLUDE_PATTERNS 81 | .iter() 82 | .map(|&s| s.to_string()) 83 | .collect(), 84 | watch_events_rx, 85 | DidOpenConfiguration::Lazy, 86 | ); 87 | let pending_requests = PendingRequests::new(); 88 | Ok(Self { 89 | process: process_handler, 90 | json_rpc: json_rpc_handler, 91 | workspace_documents, 92 | pending_requests, 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/mod.rs: -------------------------------------------------------------------------------- 1 | mod clang; 2 | mod csharp; 3 | mod golang; 4 | mod java; 5 | mod php; 6 | mod python; 7 | mod ruby; 8 | mod rust; 9 | mod typescript; 10 | 11 | pub use self::{ 12 | clang::*, csharp::*, golang::*, java::*, php::*, python::*, ruby::*, rust::*, typescript::*, 13 | }; 14 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/php.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}, 3 | utils::workspace_documents::{ 4 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, 5 | PHP_FILE_PATTERNS, PHP_ROOT_FILES, 6 | }, 7 | }; 8 | use async_trait::async_trait; 9 | use log::warn; 10 | use lsp_types::InitializeParams; 11 | use notify_debouncer_mini::DebouncedEvent; 12 | use std::{error::Error, path::Path, process::Stdio}; 13 | use tokio::{process::Command, sync::broadcast::Receiver}; 14 | use url::Url; 15 | 16 | pub struct PhpactorClient { 17 | process: ProcessHandler, 18 | json_rpc: JsonRpcHandler, 19 | workspace_documents: WorkspaceDocumentsHandler, 20 | pending_requests: PendingRequests, 21 | } 22 | 23 | #[async_trait] 24 | impl LspClient for PhpactorClient { 25 | fn get_process(&mut self) -> &mut ProcessHandler { 26 | &mut self.process 27 | } 28 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 29 | &mut self.json_rpc 30 | } 31 | fn get_root_files(&mut self) -> Vec { 32 | PHP_ROOT_FILES.iter().map(|&s| s.to_owned()).collect() 33 | } 34 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 35 | &mut self.workspace_documents 36 | } 37 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 38 | &mut self.pending_requests 39 | } 40 | 41 | async fn get_initialize_params( 42 | &mut self, 43 | root_path: String, 44 | ) -> Result> { 45 | let workspace_folders = self 46 | .find_workspace_folders(root_path.clone()) 47 | .await 48 | .unwrap(); 49 | Ok(InitializeParams { 50 | capabilities: self.get_capabilities(), 51 | workspace_folders: Some(workspace_folders.clone()), 52 | root_uri: Some(Url::from_file_path(&root_path).map_err(|_| "Invalid root path")?), 53 | ..Default::default() 54 | }) 55 | } 56 | } 57 | 58 | impl PhpactorClient { 59 | pub async fn new( 60 | root_path: &str, 61 | watch_events_rx: Receiver, 62 | ) -> Result> { 63 | // Create a Phpactor configuration file 64 | let config_path = Path::new(root_path).join(".phpactor.json"); 65 | let config_content = serde_json::json!({ 66 | "logging.enabled": true, 67 | "logging.level": "info", 68 | "logging.path": "/tmp/phpactor.log", 69 | "logging.formatter": "json", 70 | "language_server.trace": false, 71 | }); 72 | 73 | std::fs::write(&config_path, serde_json::to_string_pretty(&config_content)?) 74 | .map_err(|e| Box::new(e) as Box)?; 75 | 76 | // Dump autoload if it exists for better performance 77 | let mut child = Command::new("composer") 78 | .arg("dump-autoload") 79 | .arg("--no-scripts") 80 | .current_dir(root_path) // Set the working directory 81 | .stdout(Stdio::piped()) // Capture stdout 82 | .stderr(Stdio::piped()) // Capture stderr 83 | .spawn() 84 | .map_err(|e| format!("Failed to spawn `composer dump-autoload`: {}", e))?; 85 | 86 | // Wait for the child process to complete 87 | if let Ok(status) = child.wait().await { 88 | if !status.success() { 89 | if let Some(code) = status.code() { 90 | warn!( 91 | "`composer dump-autoload` exited with non-zero status code: {}", 92 | code 93 | ); 94 | } else { 95 | warn!("`composer dump-autoload` was terminated by a signal."); 96 | } 97 | } 98 | } 99 | 100 | let process = Command::new("phpactor") 101 | .arg("language-server") 102 | .current_dir(root_path) 103 | .stdin(Stdio::piped()) 104 | .stdout(Stdio::piped()) 105 | .stderr(Stdio::piped()) 106 | .spawn() 107 | .map_err(|e| Box::new(e) as Box)?; 108 | 109 | let process_handler = ProcessHandler::new(process) 110 | .await 111 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 112 | 113 | let workspace_documents = WorkspaceDocumentsHandler::new( 114 | Path::new(root_path), 115 | PHP_FILE_PATTERNS.iter().map(|&s| s.to_string()).collect(), 116 | DEFAULT_EXCLUDE_PATTERNS 117 | .iter() 118 | .map(|&s| s.to_string()) 119 | .collect(), 120 | watch_events_rx, 121 | DidOpenConfiguration::Lazy, 122 | ); 123 | 124 | let json_rpc_handler = JsonRpcHandler::new(); 125 | 126 | Ok(Self { 127 | process: process_handler, 128 | json_rpc: json_rpc_handler, 129 | workspace_documents, 130 | pending_requests: PendingRequests::new(), 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/python.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, process::Stdio}; 2 | 3 | use async_trait::async_trait; 4 | use notify_debouncer_mini::DebouncedEvent; 5 | use tokio::process::Command; 6 | use tokio::sync::broadcast::Receiver; 7 | 8 | use crate::lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}; 9 | 10 | use crate::utils::workspace_documents::{ 11 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, 12 | PYTHON_FILE_PATTERNS, PYTHON_ROOT_FILES, 13 | }; 14 | 15 | pub struct JediClient { 16 | process: ProcessHandler, 17 | json_rpc: JsonRpcHandler, 18 | workspace_documents: WorkspaceDocumentsHandler, 19 | pending_requests: PendingRequests, 20 | } 21 | 22 | #[async_trait] 23 | impl LspClient for JediClient { 24 | fn get_process(&mut self) -> &mut ProcessHandler { 25 | &mut self.process 26 | } 27 | 28 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 29 | &mut self.json_rpc 30 | } 31 | 32 | fn get_root_files(&mut self) -> Vec { 33 | PYTHON_ROOT_FILES.iter().map(|&s| s.to_string()).collect() 34 | } 35 | 36 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 37 | &mut self.workspace_documents 38 | } 39 | 40 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 41 | &mut self.pending_requests 42 | } 43 | } 44 | 45 | impl JediClient { 46 | pub async fn new( 47 | root_path: &str, 48 | watch_events_rx: Receiver, 49 | ) -> Result> { 50 | let process = Command::new("jedi-language-server") 51 | .current_dir(root_path) 52 | .stdin(Stdio::piped()) 53 | .stdout(Stdio::piped()) 54 | .stderr(Stdio::piped()) 55 | .spawn() 56 | .map_err(|e| Box::new(e) as Box)?; 57 | 58 | let process_handler = ProcessHandler::new(process) 59 | .await 60 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 61 | 62 | let workspace_documents = WorkspaceDocumentsHandler::new( 63 | Path::new(root_path), 64 | PYTHON_FILE_PATTERNS 65 | .iter() 66 | .map(|&s| s.to_string()) 67 | .collect(), 68 | DEFAULT_EXCLUDE_PATTERNS 69 | .iter() 70 | .map(|&s| s.to_string()) 71 | .collect(), 72 | watch_events_rx, 73 | DidOpenConfiguration::None, 74 | ); 75 | 76 | let json_rpc_handler = JsonRpcHandler::new(); 77 | 78 | Ok(Self { 79 | process: process_handler, 80 | json_rpc: json_rpc_handler, 81 | workspace_documents, 82 | pending_requests: PendingRequests::new(), 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/ruby.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}, 3 | utils::workspace_documents::{ 4 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, 5 | RUBY_FILE_PATTERNS, RUBY_ROOT_FILES, 6 | }, 7 | }; 8 | use async_trait::async_trait; 9 | use log::error; 10 | use lsp_types::InitializeParams; 11 | use notify_debouncer_mini::DebouncedEvent; 12 | use std::{error::Error, path::Path, process::Stdio}; 13 | use tokio::{process::Command, sync::broadcast::Receiver}; 14 | pub struct RubyClient { 15 | process: ProcessHandler, 16 | json_rpc: JsonRpcHandler, 17 | workspace_documents: WorkspaceDocumentsHandler, 18 | pending_requests: PendingRequests, 19 | } 20 | #[async_trait] 21 | impl LspClient for RubyClient { 22 | fn get_process(&mut self) -> &mut ProcessHandler { 23 | &mut self.process 24 | } 25 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 26 | &mut self.json_rpc 27 | } 28 | fn get_root_files(&mut self) -> Vec { 29 | RUBY_ROOT_FILES.iter().map(|&s| s.to_owned()).collect() 30 | } 31 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 32 | &mut self.workspace_documents 33 | } 34 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 35 | &mut self.pending_requests 36 | } 37 | 38 | async fn get_initialize_params( 39 | &mut self, 40 | root_path: String, 41 | ) -> Result> { 42 | let workspace_folders = self.find_workspace_folders(root_path.clone()).await?; 43 | Ok(InitializeParams { 44 | capabilities: self.get_capabilities(), 45 | workspace_folders: Some(workspace_folders.clone()), 46 | root_uri: workspace_folders.first().map(|f| f.uri.clone()), // <--------- Not default behavior 47 | ..Default::default() 48 | }) 49 | } 50 | } 51 | impl RubyClient { 52 | pub async fn new( 53 | root_path: &str, 54 | watch_events_rx: Receiver, 55 | ) -> Result> { 56 | let debug_file = std::fs::File::create("/tmp/ruby-lsp.log")?; 57 | let process = Command::new("ruby-lsp") 58 | .arg("--use-launcher") 59 | .current_dir(root_path) 60 | .stdin(Stdio::piped()) 61 | .stdout(Stdio::piped()) 62 | .stderr(debug_file) 63 | .spawn() 64 | .map_err(|e| { 65 | error!("Failed to start ruby-lsp process: {}", e); 66 | Box::new(e) as Box 67 | })?; 68 | let process_handler = ProcessHandler::new(process) 69 | .await 70 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 71 | let json_rpc_handler = JsonRpcHandler::new(); 72 | let workspace_documents = WorkspaceDocumentsHandler::new( 73 | Path::new(root_path), 74 | RUBY_FILE_PATTERNS.iter().map(|&s| s.to_string()).collect(), 75 | DEFAULT_EXCLUDE_PATTERNS 76 | .iter() 77 | .map(|&s| s.to_string()) 78 | .collect(), 79 | watch_events_rx, 80 | DidOpenConfiguration::Lazy, 81 | ); 82 | let pending_requests = PendingRequests::new(); 83 | Ok(Self { 84 | process: process_handler, 85 | json_rpc: json_rpc_handler, 86 | workspace_documents, 87 | pending_requests, 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/rust.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::Path, process::Stdio}; 2 | 3 | use async_trait::async_trait; 4 | use lsp_types::{ 5 | ClientCapabilities, DocumentSymbolClientCapabilities, InitializeParams, 6 | TextDocumentClientCapabilities, 7 | }; 8 | use notify_debouncer_mini::DebouncedEvent; 9 | use tokio::process::Command; 10 | use tokio::sync::broadcast::Receiver; 11 | use url::Url; 12 | 13 | use crate::lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}; 14 | 15 | use crate::utils::workspace_documents::{ 16 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, RUST_FILE_PATTERNS, 17 | RUST_ROOT_FILES, 18 | }; 19 | 20 | pub struct RustAnalyzerClient { 21 | process: ProcessHandler, 22 | json_rpc: JsonRpcHandler, 23 | workspace_documents: WorkspaceDocumentsHandler, 24 | pending_requests: PendingRequests, 25 | } 26 | 27 | #[async_trait] 28 | impl LspClient for RustAnalyzerClient { 29 | fn get_capabilities(&mut self) -> ClientCapabilities { 30 | let mut capabilities = ClientCapabilities::default(); 31 | capabilities.text_document = Some(TextDocumentClientCapabilities { 32 | document_symbol: Some(DocumentSymbolClientCapabilities { 33 | hierarchical_document_symbol_support: Some(true), 34 | ..Default::default() 35 | }), 36 | ..Default::default() 37 | }); 38 | 39 | capabilities.experimental = Some(serde_json::json!({ 40 | "serverStatusNotification": true 41 | })); 42 | capabilities 43 | } 44 | 45 | async fn get_initialize_params( 46 | &mut self, 47 | root_path: String, 48 | ) -> Result> { 49 | Ok(InitializeParams { 50 | capabilities: self.get_capabilities(), 51 | workspace_folders: Some( 52 | self.find_workspace_folders(root_path.clone()) 53 | .await 54 | .unwrap(), 55 | ), 56 | root_uri: Some(Url::from_file_path(&root_path).map_err(|_| "Invalid root path")?), 57 | initialization_options: Some(serde_json::json!({ 58 | "cargo": { 59 | "sysroot": serde_json::Value::Null 60 | } 61 | })), 62 | ..Default::default() 63 | }) 64 | } 65 | 66 | fn get_process(&mut self) -> &mut ProcessHandler { 67 | &mut self.process 68 | } 69 | 70 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 71 | &mut self.json_rpc 72 | } 73 | 74 | fn get_root_files(&mut self) -> Vec { 75 | RUST_ROOT_FILES.iter().map(|&s| s.to_owned()).collect() 76 | } 77 | 78 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 79 | &mut self.workspace_documents 80 | } 81 | 82 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 83 | &mut self.pending_requests 84 | } 85 | 86 | async fn setup_workspace( 87 | &mut self, 88 | _root_path: &str, 89 | ) -> Result<(), Box> { 90 | // This is required for workspace features like go to definition to work 91 | self.send_request("rust-analyzer/reloadWorkspace", None) 92 | .await?; 93 | Ok(()) 94 | } 95 | } 96 | 97 | impl RustAnalyzerClient { 98 | pub async fn new( 99 | root_path: &str, 100 | watch_events_rx: Receiver, 101 | ) -> Result> { 102 | let process = Command::new("rust-analyzer") 103 | .current_dir(root_path) 104 | .stdin(Stdio::piped()) 105 | .stdout(Stdio::piped()) 106 | .stderr(Stdio::piped()) 107 | .spawn() 108 | .map_err(|e| Box::new(e) as Box)?; 109 | 110 | let process_handler = ProcessHandler::new(process) 111 | .await 112 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 113 | let json_rpc_handler = JsonRpcHandler::new(); 114 | 115 | let workspace_documents = WorkspaceDocumentsHandler::new( 116 | Path::new(root_path), 117 | RUST_FILE_PATTERNS.iter().map(|&s| s.to_string()).collect(), 118 | DEFAULT_EXCLUDE_PATTERNS 119 | .iter() 120 | .map(|&s| s.to_string()) 121 | .collect(), 122 | watch_events_rx, 123 | DidOpenConfiguration::None, 124 | ); 125 | 126 | Ok(Self { 127 | process: process_handler, 128 | json_rpc: json_rpc_handler, 129 | workspace_documents, 130 | pending_requests: PendingRequests::new(), 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/languages/typescript.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::path::Path; 3 | use std::process::Stdio; 4 | 5 | use async_trait::async_trait; 6 | use lsp_types::InitializeParams; 7 | use notify_debouncer_mini::DebouncedEvent; 8 | use tokio::process::Command; 9 | use tokio::sync::broadcast::Receiver; 10 | use url::Url; 11 | 12 | use crate::lsp::{JsonRpcHandler, LspClient, PendingRequests, ProcessHandler}; 13 | 14 | use crate::utils::workspace_documents::{ 15 | DidOpenConfiguration, WorkspaceDocumentsHandler, DEFAULT_EXCLUDE_PATTERNS, 16 | TYPESCRIPT_AND_JAVASCRIPT_FILE_PATTERNS, TYPESCRIPT_AND_JAVASCRIPT_ROOT_FILES, 17 | }; 18 | 19 | pub struct TypeScriptLanguageClient { 20 | process: ProcessHandler, 21 | json_rpc: JsonRpcHandler, 22 | workspace_documents: WorkspaceDocumentsHandler, 23 | pending_requests: PendingRequests, 24 | } 25 | 26 | #[async_trait] 27 | impl LspClient for TypeScriptLanguageClient { 28 | fn get_process(&mut self) -> &mut ProcessHandler { 29 | &mut self.process 30 | } 31 | 32 | fn get_json_rpc(&mut self) -> &mut JsonRpcHandler { 33 | &mut self.json_rpc 34 | } 35 | 36 | fn get_root_files(&mut self) -> Vec { 37 | TYPESCRIPT_AND_JAVASCRIPT_ROOT_FILES 38 | .iter() 39 | .map(|&s| s.to_owned()) 40 | .collect() 41 | } 42 | 43 | fn get_pending_requests(&mut self) -> &mut PendingRequests { 44 | &mut self.pending_requests 45 | } 46 | 47 | fn get_workspace_documents(&mut self) -> &mut WorkspaceDocumentsHandler { 48 | &mut self.workspace_documents 49 | } 50 | 51 | async fn get_initialize_params( 52 | &mut self, 53 | root_path: String, 54 | ) -> Result> { 55 | let capabilities = self.get_capabilities(); 56 | Ok(InitializeParams { 57 | capabilities, 58 | root_uri: Some(Url::from_file_path(root_path).map_err(|_| "Invalid root path")?), 59 | initialization_options: Some(serde_json::json!({ 60 | "tsserver": { 61 | "useSyntaxServer": "never" 62 | } 63 | })), 64 | ..Default::default() 65 | }) 66 | } 67 | } 68 | 69 | impl TypeScriptLanguageClient { 70 | pub async fn new( 71 | root_path: &str, 72 | watch_events_rx: Receiver, 73 | ) -> Result> { 74 | let process = Command::new("typescript-language-server") 75 | .arg("--stdio") 76 | .current_dir(root_path) 77 | .stdin(Stdio::piped()) 78 | .stdout(Stdio::piped()) 79 | .stderr(Stdio::piped()) 80 | .spawn() 81 | .map_err(|e| Box::new(e) as Box)?; 82 | 83 | let process_handler = ProcessHandler::new(process) 84 | .await 85 | .map_err(|e| format!("Failed to create ProcessHandler: {}", e))?; 86 | let json_rpc_handler = JsonRpcHandler::new(); 87 | let workspace_documents = WorkspaceDocumentsHandler::new( 88 | Path::new(root_path), 89 | TYPESCRIPT_AND_JAVASCRIPT_FILE_PATTERNS 90 | .iter() 91 | .map(|&s| s.to_string()) 92 | .collect(), 93 | DEFAULT_EXCLUDE_PATTERNS 94 | .iter() 95 | .map(|&s| s.to_string()) 96 | .collect(), 97 | watch_events_rx, 98 | DidOpenConfiguration::Lazy, 99 | ); 100 | Ok(Self { 101 | process: process_handler, 102 | json_rpc: json_rpc_handler, 103 | workspace_documents, 104 | pending_requests: PendingRequests::new(), 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/manager/language_tests/c_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | #[ignore] // TODO: https://github.com/agentic-labs/lsproxy/issues/98 5 | async fn test_references() -> Result<(), Box> { 6 | let context = TestContext::setup(&c_sample_path(), true).await?; 7 | let manager = context 8 | .manager 9 | .as_ref() 10 | .ok_or("Manager is not initialized")?; 11 | tokio::time::sleep(Duration::from_millis(200)).await; 12 | let references = manager 13 | .find_references( 14 | "map.c", 15 | lsp_types::Position { 16 | line: 30, 17 | character: 5, 18 | }, 19 | ) 20 | .await?; 21 | 22 | let expected = vec![ 23 | Location { 24 | uri: Url::parse("file:///mnt/lsproxy_root/sample_project/c/map.c").unwrap(), 25 | range: lsp_types::Range { 26 | start: lsp_types::Position { 27 | line: 30, 28 | character: 5, 29 | }, 30 | end: lsp_types::Position { 31 | line: 30, 32 | character: 14, 33 | }, 34 | }, 35 | }, 36 | Location { 37 | uri: Url::parse("file:///mnt/lsproxy_root/sample_project/c/main.c").unwrap(), 38 | range: Range { 39 | start: lsp_types::Position { 40 | line: 15, 41 | character: 8, 42 | }, 43 | end: lsp_types::Position { 44 | line: 15, 45 | character: 17, 46 | }, 47 | }, 48 | }, 49 | Location { 50 | uri: Url::parse("file:///mnt/lsproxy_root/sample_project/c/map.h").unwrap(), 51 | range: Range { 52 | start: lsp_types::Position { 53 | line: 11, 54 | character: 5, 55 | }, 56 | end: lsp_types::Position { 57 | line: 11, 58 | character: 14, 59 | }, 60 | }, 61 | }, 62 | ]; 63 | 64 | // Sort locations before comparing 65 | let mut actual_locations = references; 66 | let mut expected_locations = expected; 67 | 68 | actual_locations.sort_by(|a, b| a.uri.path().cmp(b.uri.path())); 69 | expected_locations.sort_by(|a, b| a.uri.path().cmp(b.uri.path())); 70 | 71 | assert_eq!(actual_locations, expected_locations); 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/manager/language_tests/mod.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{GotoDefinitionResponse, Location, Range, Url}; 2 | use tokio::time::{sleep, Duration}; 3 | 4 | use crate::test_utils::{ 5 | c_sample_path, cpp_sample_path, csharp_sample_path, go_sample_path, java_sample_path, 6 | js_sample_path, php_sample_path, python_sample_path, rust_sample_path, typescript_sample_path, 7 | TestContext, 8 | }; 9 | 10 | use crate::api_types::{FilePosition, FileRange, Position, Symbol, SymbolResponse}; 11 | 12 | mod c_tests; 13 | mod cpp_tests; 14 | mod csharp_tests; 15 | mod golang_tests; 16 | mod java_tests; 17 | mod js_tests; 18 | mod php_tests; 19 | mod python_tests; 20 | mod rust_tests; 21 | mod tsx_tests; 22 | mod typescript_tests; 23 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/manager/language_tests/typescript_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types; 2 | 3 | use super::*; 4 | 5 | #[tokio::test] 6 | async fn test_file_symbols() -> Result<(), Box> { 7 | let context = TestContext::setup(&typescript_sample_path(), true).await?; 8 | let manager = context 9 | .manager 10 | .as_ref() 11 | .ok_or("Manager is not initialized")?; 12 | let file_path = "src/node.ts"; 13 | let file_symbols = manager.definitions_in_file_ast_grep(file_path).await?; 14 | let mut symbol_response: SymbolResponse = file_symbols.into_iter().map(Symbol::from).collect(); 15 | 16 | let mut expected = vec![ 17 | Symbol { 18 | name: String::from("Node"), 19 | kind: String::from("class"), 20 | identifier_position: FilePosition { 21 | path: String::from("src/node.ts"), 22 | position: Position { 23 | line: 0, 24 | character: 13, 25 | }, 26 | }, 27 | file_range: FileRange { 28 | path: String::from("src/node.ts"), 29 | range: api_types::Range { 30 | start: Position { 31 | line: 0, 32 | character: 0, 33 | }, 34 | end: Position { 35 | line: 14, 36 | character: 1, 37 | }, 38 | }, 39 | }, 40 | }, 41 | Symbol { 42 | name: String::from("constructor"), 43 | kind: String::from("method"), 44 | identifier_position: FilePosition { 45 | path: String::from("src/node.ts"), 46 | position: Position { 47 | line: 1, 48 | character: 4, 49 | }, 50 | }, 51 | file_range: FileRange { 52 | path: String::from("src/node.ts"), 53 | range: api_types::Range { 54 | start: Position { 55 | line: 1, 56 | character: 0, 57 | }, 58 | end: Position { 59 | line: 7, 60 | character: 8, 61 | }, 62 | }, 63 | }, 64 | }, 65 | Symbol { 66 | name: String::from("f"), 67 | kind: String::from("method"), 68 | identifier_position: FilePosition { 69 | path: String::from("src/node.ts"), 70 | position: Position { 71 | line: 10, 72 | character: 4, 73 | }, 74 | }, 75 | file_range: FileRange { 76 | path: String::from("src/node.ts"), 77 | range: api_types::Range { 78 | start: Position { 79 | line: 10, 80 | character: 0, 81 | }, 82 | end: Position { 83 | line: 10, 84 | character: 37, 85 | }, 86 | }, 87 | }, 88 | }, 89 | Symbol { 90 | name: String::from("toString"), 91 | kind: String::from("method"), 92 | identifier_position: FilePosition { 93 | path: String::from("src/node.ts"), 94 | position: Position { 95 | line: 13, 96 | character: 4, 97 | }, 98 | }, 99 | file_range: FileRange { 100 | path: String::from("src/node.ts"), 101 | range: api_types::Range { 102 | start: Position { 103 | line: 13, 104 | character: 0, 105 | }, 106 | end: Position { 107 | line: 13, 108 | character: 57, 109 | }, 110 | }, 111 | }, 112 | }, 113 | ]; 114 | // sort symbols by name 115 | symbol_response.sort_by_key(|s| s.name.clone()); 116 | expected.sort_by_key(|s| s.name.clone()); 117 | assert_eq!(symbol_response, expected); 118 | Ok(()) 119 | } 120 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/manager/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod manager; 2 | 3 | pub use manager::*; 4 | 5 | #[cfg(test)] 6 | mod language_tests; 7 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | pub(crate) mod json_rpc; 3 | pub(crate) mod languages; 4 | pub(crate) mod manager; 5 | pub(crate) mod process; 6 | pub use self::{client::*, json_rpc::*, process::*}; 7 | -------------------------------------------------------------------------------- /lsproxy/src/lsp/process.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::sync::Arc; 3 | use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; 4 | use tokio::process::{Child, ChildStdin, ChildStdout}; 5 | use tokio::sync::Mutex; 6 | 7 | #[async_trait::async_trait] 8 | pub trait Process: Send + Sync { 9 | async fn send(&mut self, data: &str) -> Result<(), Box>; 10 | async fn receive(&self) -> Result>; 11 | } 12 | 13 | #[derive(Clone)] 14 | pub struct ProcessHandler { 15 | pub stdin: Arc>, 16 | pub stdout: Arc>>, 17 | } 18 | 19 | impl ProcessHandler { 20 | pub async fn new(mut child: Child) -> Result> { 21 | let stdin = child.stdin.take().ok_or("Failed to open stdin")?; 22 | let stdout = child.stdout.take().ok_or("Failed to open stdout")?; 23 | Ok(Self { 24 | stdin: Arc::new(Mutex::new(stdin)), 25 | stdout: Arc::new(Mutex::new(BufReader::new(stdout))), 26 | }) 27 | } 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl Process for ProcessHandler { 32 | async fn send(&mut self, data: &str) -> Result<(), Box> { 33 | let mut stdin = self.stdin.lock().await; 34 | stdin.write_all(data.as_bytes()).await?; 35 | stdin.flush().await?; 36 | Ok(()) 37 | } 38 | 39 | async fn receive(&self) -> Result> { 40 | let mut content_length: Option = None; 41 | let mut buffer = Vec::new(); 42 | 43 | loop { 44 | let mut stdout = self.stdout.lock().await; 45 | let n = stdout.read_until(b'\n', &mut buffer).await?; 46 | if n == 0 { 47 | continue; 48 | } 49 | 50 | let line = String::from_utf8_lossy(&buffer[buffer.len() - n..]); 51 | if line.trim().is_empty() && content_length.is_some() { 52 | let length = 53 | content_length.ok_or("Missing Content-Length header in LSP message")?; 54 | let mut content = vec![0; length]; 55 | stdout.read_exact(&mut content).await?; 56 | return Ok(String::from_utf8(content)?); 57 | } else if line.starts_with("Content-Length: ") { 58 | content_length = Some(line.trim_start_matches("Content-Length: ").trim().parse()?); 59 | } 60 | buffer.clear(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lsproxy/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use log::{error, info}; 4 | use lsproxy::{ 5 | initialize_app_state_with_mount_dir, run_server_with_port_and_host, write_openapi_to_file, 6 | }; 7 | use std::path::PathBuf; 8 | 9 | /// Command line interface for LSProxy server 10 | #[derive(Parser, Debug)] 11 | #[command(author, version, about, long_about = None)] 12 | struct Cli { 13 | /// Write OpenAPI specification to openapi.json file 14 | #[arg(short, long)] 15 | write_openapi: bool, 16 | 17 | /// Host address to bind the server to 18 | #[arg(long, default_value = "0.0.0.0")] 19 | host: String, 20 | 21 | /// Override the default mount directory path where your workspace files are located 22 | #[arg(long)] 23 | mount_dir: Option, 24 | 25 | /// Port number to bind the server to 26 | #[arg(long, default_value_t = 4444)] 27 | port: u16, 28 | } 29 | 30 | #[actix_web::main] 31 | async fn main() -> std::io::Result<()> { 32 | // Set up panic handler for better error reporting 33 | std::panic::set_hook(Box::new(|panic_info| { 34 | error!("Server panicked: {:?}", panic_info); 35 | })); 36 | 37 | // Initialize tracing subscriber for better logging 38 | tracing_subscriber::fmt() 39 | .with_env_filter( 40 | tracing_subscriber::EnvFilter::try_from_default_env() 41 | .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), 42 | ) 43 | .init(); 44 | 45 | // Parse command line arguments 46 | let cli = Cli::parse(); 47 | 48 | // Handle OpenAPI spec generation if requested 49 | if cli.write_openapi { 50 | if let Err(e) = write_openapi_to_file(&PathBuf::from("openapi.json")) { 51 | error!("Error: Failed to write the openapi.json to a file. Please see error for more details."); 52 | return Err(e); 53 | } 54 | return Ok(()); 55 | } 56 | 57 | // Initialize application state with optional mount directory override 58 | let app_state = initialize_app_state_with_mount_dir(cli.mount_dir.as_deref()) 59 | .await 60 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; 61 | 62 | // Run the server with specified host 63 | info!("Starting on port {}", cli.port); 64 | 65 | run_server_with_port_and_host(app_state, cli.port, &cli.host).await 66 | } 67 | -------------------------------------------------------------------------------- /lsproxy/src/middleware/jwt.rs: -------------------------------------------------------------------------------- 1 | use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}; 2 | use actix_web::Error; 3 | use futures_util::future::LocalBoxFuture; 4 | use futures_util::future::{ready, Ready}; 5 | use jsonwebtoken::{decode, DecodingKey, Validation}; 6 | use serde::{Deserialize, Serialize}; 7 | use std::env; 8 | 9 | pub fn is_auth_enabled() -> bool { 10 | env::var("USE_AUTH").map(|v| v == "true").unwrap_or(true) 11 | } 12 | 13 | pub fn validate_jwt_config() -> Result { 14 | if !is_auth_enabled() { 15 | return Ok("Authentication disabled".to_string()); 16 | } 17 | 18 | match env::var("JWT_SECRET") { 19 | Ok(secret) => Ok(secret), 20 | Err(_) => Err("JWT_SECRET environment variable not set. To use authentication you need to set this variable. If you want to turn off authentication you can set USE_AUTH=false.".to_string()) 21 | } 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize)] 25 | pub struct Claims { 26 | pub exp: usize, 27 | } 28 | 29 | pub struct JwtMiddleware; 30 | 31 | impl Transform for JwtMiddleware 32 | where 33 | S: Service, Error = Error>, 34 | S::Future: 'static, 35 | B: 'static, 36 | { 37 | type Response = ServiceResponse; 38 | type Error = Error; 39 | type InitError = (); 40 | type Transform = JwtMiddlewareService; 41 | type Future = Ready>; 42 | 43 | fn new_transform(&self, service: S) -> Self::Future { 44 | ready(Ok(JwtMiddlewareService { service })) 45 | } 46 | } 47 | 48 | pub struct JwtMiddlewareService { 49 | service: S, 50 | } 51 | 52 | impl Service for JwtMiddlewareService 53 | where 54 | S: Service, Error = Error>, 55 | S::Future: 'static, 56 | B: 'static, 57 | { 58 | type Response = ServiceResponse; 59 | type Error = Error; 60 | type Future = LocalBoxFuture<'static, Result>; 61 | 62 | forward_ready!(service); 63 | 64 | fn call(&self, req: ServiceRequest) -> Self::Future { 65 | let auth_header = req.headers().get("Authorization"); 66 | 67 | if let Some(auth_header) = auth_header { 68 | if let Ok(auth_str) = auth_header.to_str() { 69 | if auth_str.starts_with("Bearer ") { 70 | let token = auth_str.trim_start_matches("Bearer "); 71 | let secret = match std::env::var("JWT_SECRET") { 72 | Ok(secret) => secret, 73 | Err(_) => { 74 | return Box::pin(async move { 75 | Err(actix_web::error::ErrorInternalServerError( 76 | "JWT_SECRET environment variable not set", 77 | )) 78 | }); 79 | } 80 | }; 81 | 82 | match decode::( 83 | token, 84 | &DecodingKey::from_secret(secret.as_bytes()), 85 | &Validation::default(), 86 | ) { 87 | Ok(_) => { 88 | let fut = self.service.call(req); 89 | return Box::pin(async move { 90 | let res = fut.await?; 91 | Ok(res) 92 | }); 93 | } 94 | Err(_) => { 95 | return Box::pin(async move { 96 | Err(actix_web::error::ErrorUnauthorized("Invalid token")) 97 | }); 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | Box::pin(async move { 105 | Err(actix_web::error::ErrorUnauthorized( 106 | "Missing or invalid authorization header", 107 | )) 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lsproxy/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jwt; 2 | #[cfg(test)] 3 | mod tests; 4 | 5 | pub use jwt::{is_auth_enabled, validate_jwt_config, JwtMiddleware}; 6 | -------------------------------------------------------------------------------- /lsproxy/src/middleware/tests.rs: -------------------------------------------------------------------------------- 1 | use super::jwt::{Claims, JwtMiddleware}; 2 | use actix_web::test::{self, TestRequest}; 3 | use actix_web::{web, App, HttpResponse}; 4 | use jsonwebtoken::{encode, EncodingKey, Header}; 5 | use std::time::{SystemTime, UNIX_EPOCH}; 6 | 7 | async fn test_handler() -> HttpResponse { 8 | HttpResponse::Ok().finish() 9 | } 10 | 11 | #[actix_web::test] 12 | async fn test_valid_token() { 13 | std::env::set_var("JWT_SECRET", "test_secret"); 14 | 15 | let claims = Claims { 16 | exp: SystemTime::now() 17 | .duration_since(UNIX_EPOCH) 18 | .unwrap() 19 | .as_secs() as usize 20 | + 3600, 21 | }; 22 | 23 | let token = encode( 24 | &Header::default(), 25 | &claims, 26 | &EncodingKey::from_secret("test_secret".as_bytes()), 27 | ) 28 | .unwrap(); 29 | 30 | let app = test::init_service( 31 | App::new() 32 | .wrap(JwtMiddleware) 33 | .route("/", web::get().to(test_handler)), 34 | ) 35 | .await; 36 | 37 | let req = TestRequest::get() 38 | .uri("/") 39 | .insert_header(("Authorization", format!("Bearer {}", token))) 40 | .to_request(); 41 | 42 | let resp = test::call_service(&app, req).await; 43 | assert!(resp.status().is_success()); 44 | } 45 | 46 | #[actix_web::test] 47 | async fn test_invalid_token() { 48 | std::env::set_var("JWT_SECRET", "test_secret"); 49 | 50 | let app = test::init_service( 51 | App::new() 52 | .wrap(JwtMiddleware) 53 | .route("/", web::get().to(test_handler)), 54 | ) 55 | .await; 56 | 57 | let req = TestRequest::get() 58 | .uri("/") 59 | .insert_header(("Authorization", "Bearer invalid_token")) 60 | .to_request(); 61 | 62 | let err = test::try_call_service(&app, req).await.unwrap_err(); 63 | let resp = err.error_response(); 64 | assert_eq!(resp.status().as_u16(), 401); 65 | } 66 | 67 | #[actix_web::test] 68 | async fn test_missing_auth_header() { 69 | let app = test::init_service( 70 | App::new() 71 | .wrap(JwtMiddleware) 72 | .route("/", web::get().to(test_handler)), 73 | ) 74 | .await; 75 | 76 | let req = TestRequest::get().uri("/").to_request(); 77 | let err = test::try_call_service(&app, req).await.unwrap_err(); 78 | let resp = err.error_response(); 79 | assert_eq!(resp.status().as_u16(), 401); 80 | } 81 | 82 | #[actix_web::test] 83 | async fn test_missing_jwt_secret() { 84 | std::env::remove_var("JWT_SECRET"); 85 | 86 | let app = test::init_service( 87 | App::new() 88 | .wrap(JwtMiddleware) 89 | .route("/", web::get().to(test_handler)), 90 | ) 91 | .await; 92 | 93 | let req = TestRequest::get() 94 | .uri("/") 95 | .insert_header(("Authorization", "Bearer some_token")) 96 | .to_request(); 97 | 98 | let err = test::try_call_service(&app, req).await.unwrap_err(); 99 | let resp = err.error_response(); 100 | assert_eq!(resp.status().as_u16(), 500); 101 | } 102 | -------------------------------------------------------------------------------- /lsproxy/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::api_types::{set_thread_local_mount_dir, unset_thread_local_mount_dir}; 2 | use crate::lsp::manager::Manager; 3 | 4 | pub fn python_sample_path() -> String { 5 | "/mnt/lsproxy_root/sample_project/python".to_string() 6 | } 7 | 8 | pub fn js_sample_path() -> String { 9 | "/mnt/lsproxy_root/sample_project/js".to_string() 10 | } 11 | 12 | pub fn java_sample_path() -> String { 13 | "/mnt/lsproxy_root/sample_project/java".to_string() 14 | } 15 | 16 | pub fn rust_sample_path() -> String { 17 | "/mnt/lsproxy_root/sample_project/rust".to_string() 18 | } 19 | 20 | pub fn go_sample_path() -> String { 21 | "/mnt/lsproxy_root/sample_project/go".to_string() 22 | } 23 | 24 | pub fn typescript_sample_path() -> String { 25 | "/mnt/lsproxy_root/sample_project/typescript".to_string() 26 | } 27 | 28 | pub fn csharp_sample_path() -> String { 29 | "/mnt/lsproxy_root/sample_project/csharp".to_string() 30 | } 31 | 32 | pub fn cpp_sample_path() -> String { 33 | "/mnt/lsproxy_root/sample_project/cpp".to_string() 34 | } 35 | 36 | pub fn c_sample_path() -> String { 37 | "/mnt/lsproxy_root/sample_project/c".to_string() 38 | } 39 | 40 | pub fn php_sample_path() -> String { 41 | "/mnt/lsproxy_root/sample_project/php".to_string() 42 | } 43 | 44 | pub fn ruby_sample_path() -> String { 45 | "/mnt/lsproxy_root/sample_project/ruby".to_string() 46 | } 47 | 48 | pub struct TestContext { 49 | pub manager: Option, 50 | } 51 | 52 | impl TestContext { 53 | pub async fn setup(file_path: &str, manager: bool) -> Result> { 54 | set_thread_local_mount_dir(file_path); 55 | if manager { 56 | let mut manager = Manager::new(file_path).await?; 57 | if let Err(e) = manager.start_langservers(file_path).await { 58 | unset_thread_local_mount_dir(); 59 | return Err(e); 60 | } 61 | return Ok(Self { 62 | manager: Some(manager), 63 | }); 64 | } 65 | Ok(Self { manager: None }) 66 | } 67 | } 68 | 69 | impl Drop for TestContext { 70 | fn drop(&mut self) { 71 | unset_thread_local_mount_dir(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lsproxy/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod file_utils; 2 | pub(crate) mod workspace_documents; 3 | -------------------------------------------------------------------------------- /performance/process_resource_csv.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | import argparse 5 | 6 | def plot_performance_data(csv_file): 7 | # Read the CSV file 8 | df = pd.read_csv(csv_file) 9 | 10 | # Convert timestamp to datetime with ms precision 11 | df['timestamp'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d %H:%M:%S.%f') 12 | 13 | # Calculate the relative time in seconds from start with ms precision 14 | start_time = df['timestamp'].min() 15 | df['relative_time'] = (df['timestamp'] - start_time).dt.total_seconds() 16 | 17 | # Get unique processes (excluding rows where pid is 'TOTAL') 18 | processes = df[df['pid'] != 'TOTAL']['command'].unique() 19 | 20 | # Create figure with two subplots sharing x axis 21 | fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 22 | fig.suptitle('Process Performance Metrics', fontsize=14) 23 | 24 | # Color map for consistent colors across plots 25 | colors = plt.cm.tab10(np.linspace(0, 1, len(processes) + 1)) 26 | 27 | # Plot CPU cores usage 28 | for i, process in enumerate(processes): 29 | process_data = df[df['command'] == process] 30 | ax1.plot(process_data['relative_time'], process_data['cores'], 31 | label=process, color=colors[i], linewidth=1) 32 | 33 | # Plot total CPU usage 34 | total_data = df[df['pid'] == 'TOTAL'] 35 | ax1.plot(total_data['relative_time'], total_data['cores'], 36 | label='Total', color=colors[-1], linewidth=2, linestyle='--') 37 | 38 | ax1.set_ylabel('CPU Cores') 39 | ax1.grid(True, alpha=0.3) 40 | ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left') 41 | 42 | # Plot Memory usage 43 | for i, process in enumerate(processes): 44 | process_data = df[df['command'] == process] 45 | ax2.plot(process_data['relative_time'], process_data['memory_mb'], 46 | label=process, color=colors[i], linewidth=1) 47 | 48 | # Plot total memory usage 49 | ax2.plot(total_data['relative_time'], total_data['memory_mb'], 50 | label='Total', color=colors[-1], linewidth=2, linestyle='--') 51 | 52 | ax2.set_xlabel('Time (seconds)') 53 | ax2.set_ylabel('Memory (MB)') 54 | ax2.grid(True, alpha=0.3) 55 | ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left') 56 | 57 | # Calculate and display actual sample rate 58 | time_diffs = np.diff(df[df['pid'] == 'TOTAL']['relative_time']) 59 | actual_rate = np.mean(time_diffs) 60 | plt.figtext(0.02, 0.02, f'Avg sample rate: {actual_rate:.3f}s', 61 | fontsize=8, ha='left') 62 | 63 | # Adjust layout to prevent label cutoff 64 | plt.tight_layout() 65 | 66 | return fig 67 | 68 | def main(): 69 | parser = argparse.ArgumentParser(description='Plot process performance metrics from CSV') 70 | parser.add_argument('csv_file', help='Path to the CSV file containing performance data') 71 | parser.add_argument('-o', '--output', help='Output file path (optional)') 72 | args = parser.parse_args() 73 | 74 | # Create the plot 75 | fig = plot_performance_data(args.csv_file) 76 | 77 | # Save or show the plot 78 | if args.output: 79 | fig.savefig(args.output, bbox_inches='tight', dpi=300) 80 | print(f"Plot saved to {args.output}") 81 | else: 82 | plt.show() 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /release/build-and-extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Build the Docker image 5 | echo "Building Docker image..." 6 | docker build -t lsproxy-builder -f release/Dockerfile lsproxy 7 | 8 | # Create a temporary container 9 | CONTAINER_ID=$(docker create lsproxy-builder) 10 | 11 | # Copy the binary from the container to /tmp 12 | echo "Extracting binary to /tmp..." 13 | docker cp $CONTAINER_ID:/usr/local/bin/lsproxy-bin /tmp/lsproxy 14 | 15 | # Cleanup 16 | echo "Cleaning up..." 17 | docker rm $CONTAINER_ID 18 | 19 | echo "Binary extracted to /tmp/lsproxy" 20 | -------------------------------------------------------------------------------- /sample_project/c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "map.h" 4 | #include "pathfinding.h" 5 | 6 | int main() { 7 | struct stop *stops = NULL; 8 | struct route *routes = NULL; 9 | int s_len = 0, r_len = 0, p_len = 0; 10 | int *path = NULL; 11 | 12 | init_stops(&stops, &s_len); 13 | init_routes(&routes, &r_len, stops, s_len); 14 | 15 | if (find_path(stops, routes, s_len, path, &p_len)) { 16 | print_map(path, p_len, stops); 17 | printf("path cost is %d:\n", p_len); 18 | for (int i = p_len - 1; i >= 0; i--) { 19 | printf("(%1.0f, %1.0f)\n", stops[path[i]].col, stops[path[i]].row); 20 | } 21 | } else { 22 | puts("IMPOSSIBLE"); 23 | } 24 | 25 | // Cleanup 26 | for (int i = 0; i < s_len; ++i) { 27 | free(stops[i].n); 28 | } 29 | free(stops); 30 | free(routes); 31 | free(path); 32 | 33 | return 0; 34 | } -------------------------------------------------------------------------------- /sample_project/c/map.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "map.h" 3 | #include "pathfinding.h" 4 | 5 | char map[MAP_SIZE_ROWS][MAP_SIZE_COLS] = { 6 | {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 7 | {1, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 8 | {1, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 9 | {1, 0, 0, 0, 0, 1, 1, 1, 0, 1}, 10 | {1, 0, 0, 1, 0, 0, 0, 1, 0, 1}, 11 | {1, 0, 0, 1, 0, 0, 0, 1, 0, 1}, 12 | {1, 0, 0, 1, 1, 1, 1, 1, 0, 1}, 13 | {1, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 14 | {1, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 15 | {1, 1, 1, 1, 1, 1, 1, 1, 1, 1} 16 | }; 17 | 18 | int ind[MAP_SIZE_ROWS][MAP_SIZE_COLS] = { 19 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 20 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 21 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 22 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 23 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 24 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 25 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 26 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 27 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 28 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1} 29 | }; 30 | 31 | void print_map(int *path, int p_len, struct stop *stops) { 32 | int i, j, k, b; 33 | 34 | for (i = 0; i < MAP_SIZE_ROWS; i++) { 35 | for (j = 0; j < MAP_SIZE_COLS; j++) { 36 | if (map[i][j]) { 37 | putchar(0xdb); 38 | } else { 39 | b = 0; 40 | for (k = 0; k < p_len; k++) { 41 | if (ind[i][j] == path[k]) { 42 | ++b; 43 | } 44 | } 45 | putchar(b ? 'x' : '.'); 46 | } 47 | } 48 | putchar('\n'); 49 | } 50 | } -------------------------------------------------------------------------------- /sample_project/c/map.h: -------------------------------------------------------------------------------- 1 | #ifndef MAP_H 2 | #define MAP_H 3 | 4 | #include "pathfinding.h" 5 | 6 | #define MAP_SIZE_ROWS 10 7 | #define MAP_SIZE_COLS 10 8 | 9 | extern char map[MAP_SIZE_ROWS][MAP_SIZE_COLS]; 10 | extern int ind[MAP_SIZE_ROWS][MAP_SIZE_COLS]; 11 | 12 | void print_map(int *path, int p_len, struct stop *stops); 13 | 14 | #endif -------------------------------------------------------------------------------- /sample_project/c/pathfinding.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "pathfinding.h" 6 | #include "map.h" 7 | 8 | void init_stops(struct stop **stops, int *s_len) { 9 | int i, j; 10 | *s_len = 0; 11 | 12 | for (i = 1; i < MAP_SIZE_ROWS - 1; i++) { 13 | for (j = 1; j < MAP_SIZE_COLS - 1; j++) { 14 | if (!map[i][j]) { 15 | ++(*s_len); 16 | *stops = realloc(*stops, (*s_len) * sizeof(struct stop)); 17 | int t = *s_len - 1; 18 | (*stops)[t].col = j; 19 | (*stops)[t].row = i; 20 | (*stops)[t].from = -1; 21 | (*stops)[t].g = DBL_MAX; 22 | (*stops)[t].n_len = 0; 23 | (*stops)[t].n = NULL; 24 | ind[i][j] = t; 25 | } 26 | } 27 | } 28 | 29 | // Calculate heuristics 30 | int e = *s_len - 1; 31 | for (i = 0; i < *s_len; i++) { 32 | (*stops)[i].h = sqrt(pow((*stops)[e].row - (*stops)[i].row, 2) + 33 | pow((*stops)[e].col - (*stops)[i].col, 2)); 34 | } 35 | } 36 | 37 | void init_routes(struct route **routes, int *r_len, struct stop *stops, int s_len) { 38 | int i, j, k, l; 39 | *r_len = 0; 40 | 41 | for (i = 1; i < MAP_SIZE_ROWS - 1; i++) { 42 | for (j = 1; j < MAP_SIZE_COLS - 1; j++) { 43 | if (ind[i][j] >= 0) { 44 | for (k = i - 1; k <= i + 1; k++) { 45 | for (l = j - 1; l <= j + 1; l++) { 46 | if ((k == i) and (l == j)) continue; 47 | if (ind[k][l] >= 0) { 48 | ++(*r_len); 49 | *routes = realloc(*routes, (*r_len) * sizeof(struct route)); 50 | int t = *r_len - 1; 51 | (*routes)[t].x = ind[i][j]; 52 | (*routes)[t].y = ind[k][l]; 53 | (*routes)[t].d = sqrt(pow(stops[(*routes)[t].y].row - 54 | stops[(*routes)[t].x].row, 2) + 55 | pow(stops[(*routes)[t].y].col - 56 | stops[(*routes)[t].x].col, 2)); 57 | 58 | ++stops[(*routes)[t].x].n_len; 59 | stops[(*routes)[t].x].n = realloc(stops[(*routes)[t].x].n, 60 | stops[(*routes)[t].x].n_len * sizeof(int)); 61 | stops[(*routes)[t].x].n[stops[(*routes)[t].x].n_len - 1] = t; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /sample_project/c/pathfinding.h: -------------------------------------------------------------------------------- 1 | #ifndef PATHFINDING_H 2 | #define PATHFINDING_H 3 | 4 | struct stop { 5 | double col, row; 6 | int *n; 7 | int n_len; 8 | double f, g, h; 9 | int from; 10 | }; 11 | 12 | struct route { 13 | int x; 14 | int y; 15 | double d; 16 | }; 17 | 18 | int find_path(struct stop *stops, struct route *routes, int s_len, int *path, int *p_len); 19 | void init_stops(struct stop **stops, int *s_len); 20 | void init_routes(struct route **routes, int *r_len, struct stop *stops, int s_len); 21 | 22 | #endif -------------------------------------------------------------------------------- /sample_project/cpp/astar_search.cpp: -------------------------------------------------------------------------------- 1 | #include "cpp_classes/astar.cpp" 2 | 3 | int main( int argc, char* argv[] ) { 4 | map m; 5 | point s, e( 7, 7 ); 6 | aStar as; 7 | 8 | if( as.search( s, e, m ) ) { 9 | std::list path; 10 | int c = as.path( path ); 11 | for( int y = -1; y < 9; y++ ) { 12 | for( int x = -1; x < 9; x++ ) { 13 | if( x < 0 || y < 0 || x > 7 || y > 7 || m( x, y ) == 1 ) 14 | std::cout << char(0xdb); 15 | else { 16 | if( std::find( path.begin(), path.end(), point( x, y ) )!= path.end() ) 17 | std::cout << "x"; 18 | else std::cout << "."; 19 | } 20 | } 21 | std::cout << "\n"; 22 | } 23 | 24 | std::cout << "\nPath cost " << c << ": "; 25 | for( std::list::iterator i = path.begin(); i != path.end(); i++ ) { 26 | std::cout<< "(" << ( *i ).x << ", " << ( *i ).y << ") "; 27 | } 28 | } 29 | std::cout << "\n\n"; 30 | return 0; 31 | } -------------------------------------------------------------------------------- /sample_project/cpp/cpp_classes/astar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "point.cpp" 6 | #include "node.cpp" 7 | #include "map.cpp" 8 | 9 | class aStar { 10 | public: 11 | aStar() { 12 | neighbours[0] = point( -1, -1 ); neighbours[1] = point( 1, -1 ); 13 | neighbours[2] = point( -1, 1 ); neighbours[3] = point( 1, 1 ); 14 | neighbours[4] = point( 0, -1 ); neighbours[5] = point( -1, 0 ); 15 | neighbours[6] = point( 0, 1 ); neighbours[7] = point( 1, 0 ); 16 | } 17 | 18 | int calcDist( point& p ){ 19 | // need a better heuristic 20 | int x = end.x - p.x, y = end.y - p.y; 21 | return( x * x + y * y ); 22 | } 23 | 24 | bool isValid( point& p ) { 25 | return ( p.x >-1 && p.y > -1 && p.x < m.w && p.y < m.h ); 26 | } 27 | 28 | bool existPoint( point& p, int cost ) { 29 | std::list::iterator i; 30 | i = std::find( closed.begin(), closed.end(), p ); 31 | if( i != closed.end() ) { 32 | if( ( *i ).cost + ( *i ).dist < cost ) return true; 33 | else { closed.erase( i ); return false; } 34 | } 35 | i = std::find( open.begin(), open.end(), p ); 36 | if( i != open.end() ) { 37 | if( ( *i ).cost + ( *i ).dist < cost ) return true; 38 | else { open.erase( i ); return false; } 39 | } 40 | return false; 41 | } 42 | 43 | bool fillOpen( node& n ) { 44 | int stepCost, nc, dist; 45 | point neighbour; 46 | 47 | for( int x = 0; x < 8; x++ ) { 48 | // one can make diagonals have different cost 49 | stepCost = x < 4 ? 1 : 1; 50 | neighbour = n.pos + neighbours[x]; 51 | if( neighbour == end ) return true; 52 | 53 | if( isValid( neighbour ) && m( neighbour.x, neighbour.y ) != 1 ) { 54 | nc = stepCost + n.cost; 55 | dist = calcDist( neighbour ); 56 | if( !existPoint( neighbour, nc + dist ) ) { 57 | node m; 58 | m.cost = nc; m.dist = dist; 59 | m.pos = neighbour; 60 | m.parent = n.pos; 61 | open.push_back( m ); 62 | } 63 | } 64 | } 65 | return false; 66 | } 67 | 68 | bool search( point& s, point& e, map& mp ) { 69 | node n; end = e; start = s; m = mp; 70 | n.cost = 0; n.pos = s; n.parent = 0; n.dist = calcDist( s ); 71 | open.push_back( n ); 72 | while( !open.empty() ) { 73 | //open.sort(); 74 | node n = open.front(); 75 | open.pop_front(); 76 | closed.push_back( n ); 77 | if( fillOpen( n ) ) return true; 78 | } 79 | return false; 80 | } 81 | 82 | int path( std::list& path ) { 83 | path.push_front( end ); 84 | int cost = 1 + closed.back().cost; 85 | path.push_front( closed.back().pos ); 86 | point parent = closed.back().parent; 87 | 88 | for( std::list::reverse_iterator i = closed.rbegin(); i != closed.rend(); i++ ) { 89 | if( ( *i ).pos == parent && !( ( *i ).pos == start ) ) { 90 | path.push_front( ( *i ).pos ); 91 | parent = ( *i ).parent; 92 | } 93 | } 94 | path.push_front( start ); 95 | return cost; 96 | } 97 | 98 | map m; point end, start; 99 | point neighbours[8]; 100 | std::list open; 101 | std::list closed; 102 | }; -------------------------------------------------------------------------------- /sample_project/cpp/cpp_classes/map.cpp: -------------------------------------------------------------------------------- 1 | class map { 2 | public: 3 | map() { 4 | char t[8][8] = { 5 | {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}, 6 | {0, 0, 0, 0, 1, 1, 1, 0}, {0, 0, 1, 0, 0, 0, 1, 0}, 7 | {0, 0, 1, 0, 0, 0, 1, 0}, {0, 0, 1, 1, 1, 1, 1, 0}, 8 | {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0} 9 | }; 10 | w = h = 8; 11 | for( int r = 0; r < h; r++ ) 12 | for( int s = 0; s < w; s++ ) 13 | m[s][r] = t[r][s]; 14 | } 15 | int operator() ( int x, int y ) { return m[x][y]; } 16 | char m[8][8]; 17 | int w, h; 18 | }; -------------------------------------------------------------------------------- /sample_project/cpp/cpp_classes/node.cpp: -------------------------------------------------------------------------------- 1 | class node { 2 | public: 3 | bool operator == (const node& o ) { return pos == o.pos; } 4 | bool operator == (const point& o ) { return pos == o; } 5 | bool operator < (const node& o ) { return dist + cost < o.dist + o.cost; } 6 | point pos, parent; 7 | int dist, cost; 8 | }; -------------------------------------------------------------------------------- /sample_project/cpp/cpp_classes/point.cpp: -------------------------------------------------------------------------------- 1 | class point { 2 | public: 3 | point( int a = 0, int b = 0 ) { x = a; y = b; } 4 | bool operator ==( const point& o ) { return o.x == x && o.y == y; } 5 | point operator +( const point& o ) { return point( o.x + x, o.y + y ); } 6 | int x, y; 7 | }; -------------------------------------------------------------------------------- /sample_project/csharp/.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample_project/csharp/AStar.cs: -------------------------------------------------------------------------------- 1 | namespace AStarPathfinding 2 | { 3 | public class AStar 4 | { 5 | private readonly List _open = new(); 6 | private readonly List _closed = new(); 7 | private readonly List _path = new(); 8 | private readonly int[][] _maze; 9 | private Node _current; 10 | private readonly int _xStart; 11 | private readonly int _yStart; 12 | private int _xEnd, _yEnd; 13 | private readonly bool _diag; 14 | 15 | public AStar(int[][] maze, int xStart, int yStart, bool diag) 16 | { 17 | _maze = maze; 18 | _current = new Node(null, xStart, yStart, 0, 0); 19 | _xStart = xStart; 20 | _yStart = yStart; 21 | _diag = diag; 22 | } 23 | 24 | public List? FindPathTo(int xEnd, int yEnd) 25 | { 26 | _xEnd = xEnd; 27 | _yEnd = yEnd; 28 | _closed.Add(_current); 29 | AddNeighborsToOpenList(); 30 | 31 | while (_current.X != _xEnd || _current.Y != _yEnd) 32 | { 33 | if (!_open.Any()) 34 | return null; 35 | 36 | _current = _open[0]; 37 | _open.RemoveAt(0); 38 | _closed.Add(_current); 39 | AddNeighborsToOpenList(); 40 | } 41 | 42 | _path.Insert(0, _current); 43 | while (_current.X != _xStart || _current.Y != _yStart) 44 | { 45 | _current = _current.Parent!; 46 | _path.Insert(0, _current); 47 | } 48 | 49 | return _path; 50 | } 51 | 52 | private void AddNeighborsToOpenList() 53 | { 54 | for (int x = -1; x <= 1; x++) 55 | { 56 | for (int y = -1; y <= 1; y++) 57 | { 58 | if (!_diag && x != 0 && y != 0) 59 | continue; 60 | 61 | var node = new Node(_current, _current.X + x, _current.Y + y, _current.G, Distance(x, y)); 62 | 63 | if ((x != 0 || y != 0) && 64 | _current.X + x >= 0 && _current.X + x < _maze[0].Length && 65 | _current.Y + y >= 0 && _current.Y + y < _maze.Length && 66 | _maze[_current.Y + y][_current.X + x] != -1 && 67 | !FindNeighborInList(_open, node) && 68 | !FindNeighborInList(_closed, node)) 69 | { 70 | node.G = node.Parent!.G + 1.0; 71 | node.G += _maze[_current.Y + y][_current.X + x]; 72 | _open.Add(node); 73 | } 74 | } 75 | } 76 | _open.Sort(); 77 | } 78 | 79 | private double Distance(int x, int y) 80 | { 81 | return Math.Sqrt(Math.Pow(_xEnd - (_current.X + x), 2) + Math.Pow(_yEnd - (_current.Y + y), 2)); 82 | } 83 | 84 | private bool FindNeighborInList(List list, Node node) 85 | { 86 | return list.Any(n => n.X == node.X && n.Y == node.Y); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sample_project/csharp/Node.cs: -------------------------------------------------------------------------------- 1 | namespace AStarPathfinding 2 | { 3 | public class Node : IComparable 4 | { 5 | public Node? Parent { get; set; } 6 | public int X { get; set; } 7 | public int Y { get; set; } 8 | public double G { get; set; } 9 | public double H { get; set; } 10 | 11 | public Node(Node? parent, int x, int y, double g, double h) 12 | { 13 | Parent = parent; 14 | X = x; 15 | Y = y; 16 | G = g; 17 | H = h; 18 | } 19 | 20 | public int CompareTo(Node? other) 21 | { 22 | if (other == null) return 1; 23 | return (G + H).CompareTo(other.G + other.H); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample_project/csharp/Program.cs: -------------------------------------------------------------------------------- 1 | namespace AStarPathfinding 2 | { 3 | class Program 4 | { 5 | static void Main(string[] args) 6 | { 7 | // -1 = blocked 8 | // 0+ = additional movement cost 9 | int[][] maze = new int[][] 10 | { 11 | new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, 12 | new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, 13 | new int[] { 0, 0, 0, 100, 100, 100, 0, 0 }, 14 | new int[] { 0, 0, 0, 0, 0, 100, 0, 0 }, 15 | new int[] { 0, 0, 100, 0, 0, 100, 0, 0 }, 16 | new int[] { 0, 0, 100, 0, 0, 100, 0, 0 }, 17 | new int[] { 0, 0, 100, 100, 100, 100, 0, 0 }, 18 | new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } 19 | }; 20 | 21 | var aStar = new AStar(maze, 0, 0, true); 22 | var path = aStar.FindPathTo(7, 7); 23 | 24 | if (path != null) 25 | { 26 | foreach (var node in path) 27 | { 28 | Console.Write($"[{node.X}, {node.Y}] "); 29 | maze[node.Y][node.X] = -1; 30 | } 31 | Console.WriteLine($"\nTotal cost: {path[^1].G:F2}"); 32 | 33 | foreach (var row in maze) 34 | { 35 | foreach (var entry in row) 36 | { 37 | Console.Write(entry switch 38 | { 39 | 0 => "_", 40 | -1 => "*", 41 | _ => "#" 42 | }); 43 | } 44 | Console.WriteLine(); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sample_project/go/go.mod: -------------------------------------------------------------------------------- 1 | module astar_test 2 | 3 | go 1.22.0 4 | -------------------------------------------------------------------------------- /sample_project/go/golang_astar/astar.go: -------------------------------------------------------------------------------- 1 | // Package astar implements the A* search algorithm with minimal constraints 2 | // on the graph representation. 3 | package golang_astar 4 | 5 | import "container/heap" 6 | 7 | type rNode struct { 8 | n Node 9 | from Node 10 | l int // route len 11 | g Cost // route cost 12 | f Cost // "g+h", route cost + heuristic estimate 13 | fx int // heap.Fix index 14 | } 15 | 16 | type openHeap []*rNode // priority queue 17 | 18 | // Route computes a route from start to end nodes using the A* algorithm. 19 | // 20 | // The algorithm is general A*, where the heuristic is not required to be 21 | // monotonic. If a route exists, the function will find a route regardless 22 | // of the quality of the Heuristic. For an admissiable heuristic, the route 23 | // will be optimal. 24 | func Route(start, end Node) (route []Node, cost int) { 25 | // start node initialized with heuristic 26 | cr := &rNode{n: start, l: 1, f: Cost(end.Heuristic(start))} 27 | // maintain a set of reached nodes. start is reached initially 28 | r := map[Node]*rNode{start: cr} 29 | // oh is a heap of nodes "open" for exploration. nodes go on the heap 30 | // when they get an initial or new "g" route distance, and therefore a 31 | // new "f" which serves as priority for exploration. 32 | oh := openHeap{cr} 33 | for len(oh) > 0 { 34 | bestRoute := heap.Pop(&oh).(*rNode) 35 | bestNode := bestRoute.n 36 | if bestNode == end { 37 | // done. prepare return values 38 | cost = int(bestRoute.g) 39 | route = make([]Node, bestRoute.l) 40 | for i := len(route) - 1; i >= 0; i-- { 41 | route[i] = bestRoute.n 42 | bestRoute = r[bestRoute.from] 43 | } 44 | return 45 | } 46 | l := bestRoute.l + 1 47 | for _, to := range bestNode.To() { 48 | // "g" route distance from start 49 | g := bestRoute.g + to.Cost 50 | if alt, ok := r[to.To]; !ok { 51 | // alt being reached for the first time 52 | alt = &rNode{n: to.To, from: bestNode, l: l, 53 | g: g, f: g + Cost(end.Heuristic(to.To))} 54 | r[to.To] = alt 55 | heap.Push(&oh, alt) 56 | } else { 57 | if g >= alt.g { 58 | continue // candidate route no better than existing route 59 | } 60 | // it's a better route 61 | // update data and make sure it's on the heap 62 | alt.from = bestNode 63 | alt.l = l 64 | alt.g = g 65 | alt.f = g + Cost(end.Heuristic(alt.n)) 66 | if alt.fx < 0 { 67 | heap.Push(&oh, alt) 68 | } else { 69 | heap.Fix(&oh, alt.fx) 70 | } 71 | } 72 | } 73 | } 74 | return nil, 0 75 | } 76 | 77 | // implement container/heap 78 | func (h openHeap) Len() int { return len(h) } 79 | func (h openHeap) Less(i, j int) bool { return h[i].f < h[j].f } 80 | func (h openHeap) Swap(i, j int) { 81 | h[i], h[j] = h[j], h[i] 82 | h[i].fx = i 83 | h[j].fx = j 84 | } 85 | 86 | func (p *openHeap) Push(x interface{}) { 87 | h := *p 88 | fx := len(h) 89 | h = append(h, x.(*rNode)) 90 | h[fx].fx = fx 91 | *p = h 92 | } 93 | 94 | func (p *openHeap) Pop() interface{} { 95 | h := *p 96 | last := len(h) - 1 97 | *p = h[:last] 98 | h[last].fx = -1 99 | return h[last] 100 | } 101 | -------------------------------------------------------------------------------- /sample_project/go/golang_astar/grid.go: -------------------------------------------------------------------------------- 1 | package golang_astar 2 | 3 | // Grid represents the search space with barriers 4 | type Grid struct { 5 | Width int 6 | Height int 7 | Barriers map[Node]bool 8 | } 9 | 10 | // NewGrid creates a new grid with the given dimensions 11 | func NewGrid(width, height int) *Grid { 12 | return &Grid{ 13 | Width: width, 14 | Height: height, 15 | Barriers: make(map[Node]bool), 16 | } 17 | } 18 | 19 | // IsValidPosition checks if a position is within grid bounds 20 | func (g *Grid) IsValidPosition(n Node) bool { 21 | return n.X >= 0 && n.X < g.Width && n.Y >= 0 && n.Y < g.Height 22 | } 23 | 24 | // GetNeighbors returns valid neighboring nodes 25 | func (g *Grid) GetNeighbors(n Node) []Arc { 26 | neighbors := make([]Arc, 0, 8) 27 | 28 | // Check all 8 adjacent positions 29 | for dx := -1; dx <= 1; dx++ { 30 | for dy := -1; dy <= 1; dy++ { 31 | if dx == 0 && dy == 0 { 32 | continue 33 | } 34 | 35 | next := Node{n.X + dx, n.Y + dy} 36 | if !g.IsValidPosition(next) { 37 | continue 38 | } 39 | 40 | cost := Cost(1) 41 | if g.Barriers[next] { 42 | cost = 100 43 | } 44 | neighbors = append(neighbors, Arc{next, cost}) 45 | } 46 | } 47 | return neighbors 48 | } 49 | -------------------------------------------------------------------------------- /sample_project/go/golang_astar/node.go: -------------------------------------------------------------------------------- 1 | package golang_astar 2 | 3 | import "fmt" 4 | 5 | // Node represents a position in the grid 6 | type Node struct { 7 | X, Y int 8 | } 9 | 10 | // String provides a string representation of Node 11 | func (n Node) String() string { 12 | return fmt.Sprintf("(%d,%d)", n.X, n.Y) 13 | } 14 | 15 | // Equal checks if two nodes have the same coordinates 16 | func (n Node) Equal(other Node) bool { 17 | return n.X == other.X && n.Y == other.Y 18 | } 19 | 20 | // Cost represents the cost to move between nodes 21 | type Cost int 22 | 23 | // Arc represents a connection between nodes with an associated cost 24 | type Arc struct { 25 | To Node 26 | Cost Cost 27 | } 28 | 29 | // Heuristic calculates estimated cost to reach another node 30 | func (n Node) Heuristic(from Node) int { 31 | dx := n.X - from.X 32 | if dx < 0 { 33 | dx = -dx 34 | } 35 | dy := n.Y - from.Y 36 | if dy < 0 { 37 | dy = -dy 38 | } 39 | if dx > dy { 40 | return dx 41 | } 42 | return dy 43 | } 44 | 45 | // To returns list of arcs from this node to neighbors 46 | func (n Node) To() []Arc { 47 | neighbors := make([]Arc, 0, 8) 48 | for dx := -1; dx <= 1; dx++ { 49 | for dy := -1; dy <= 1; dy++ { 50 | if dx == 0 && dy == 0 { 51 | continue 52 | } 53 | next := Node{n.X + dx, n.Y + dy} 54 | neighbors = append(neighbors, Arc{To: next, Cost: 1}) 55 | } 56 | } 57 | return neighbors 58 | } 59 | -------------------------------------------------------------------------------- /sample_project/go/golang_astar/search.go: -------------------------------------------------------------------------------- 1 | package golang_astar 2 | 3 | import ( 4 | "container/heap" 5 | ) 6 | 7 | // node represents a node in the search path 8 | type searchNode struct { 9 | pos Node 10 | parent *searchNode 11 | g, h, f Cost 12 | index int // for heap.Interface 13 | } 14 | 15 | // nodeHeap implements heap.Interface 16 | type nodeHeap []*searchNode 17 | 18 | func (h nodeHeap) Len() int { return len(h) } 19 | func (h nodeHeap) Less(i, j int) bool { return h[i].f < h[j].f } 20 | func (h nodeHeap) Swap(i, j int) { 21 | h[i], h[j] = h[j], h[i] 22 | h[i].index = i 23 | h[j].index = j 24 | } 25 | func (h *nodeHeap) Push(x interface{}) { 26 | n := len(*h) 27 | item := x.(*searchNode) 28 | item.index = n 29 | *h = append(*h, item) 30 | } 31 | func (h *nodeHeap) Pop() interface{} { 32 | old := *h 33 | n := len(old) 34 | item := old[n-1] 35 | old[n-1] = nil 36 | item.index = -1 37 | *h = old[0 : n-1] 38 | return item 39 | } 40 | 41 | // Heuristic estimates remaining cost to goal 42 | func Heuristic(current, goal Node) Cost { 43 | dx := current.X - goal.X 44 | if dx < 0 { 45 | dx = -dx 46 | } 47 | dy := current.Y - goal.Y 48 | if dy < 0 { 49 | dy = -dy 50 | } 51 | if dx > dy { 52 | return Cost(dx) 53 | } 54 | return Cost(dy) 55 | } 56 | 57 | // FindPath finds the shortest path between start and goal 58 | func FindPath(grid *Grid, start, goal Node) ([]Node, Cost) { 59 | openSet := &nodeHeap{} 60 | heap.Init(openSet) 61 | 62 | startNode := &searchNode{ 63 | pos: start, 64 | g: 0, 65 | h: Heuristic(start, goal), 66 | parent: nil, 67 | } 68 | startNode.f = startNode.g + startNode.h 69 | heap.Push(openSet, startNode) 70 | 71 | closedSet := make(map[Node]*searchNode) 72 | 73 | for openSet.Len() > 0 { 74 | current := heap.Pop(openSet).(*searchNode) 75 | 76 | if current.pos.Equal(goal) { 77 | // Reconstruct path 78 | path := []Node{} 79 | cost := current.g 80 | for current != nil { 81 | path = append([]Node{current.pos}, path...) 82 | current = current.parent 83 | } 84 | return path, cost 85 | } 86 | 87 | closedSet[current.pos] = current 88 | 89 | for _, arc := range grid.GetNeighbors(current.pos) { 90 | if _, exists := closedSet[arc.To]; exists { 91 | continue 92 | } 93 | 94 | g := current.g + arc.Cost 95 | 96 | var neighbor *searchNode 97 | for _, node := range *openSet { 98 | if node.pos.Equal(arc.To) { 99 | neighbor = node 100 | break 101 | } 102 | } 103 | 104 | if neighbor == nil { 105 | neighbor = &searchNode{ 106 | pos: arc.To, 107 | parent: current, 108 | g: g, 109 | h: Heuristic(arc.To, goal), 110 | } 111 | neighbor.f = neighbor.g + neighbor.h 112 | heap.Push(openSet, neighbor) 113 | } else if g < neighbor.g { 114 | neighbor.parent = current 115 | neighbor.g = g 116 | neighbor.f = g + neighbor.h 117 | heap.Fix(openSet, neighbor.index) 118 | } 119 | } 120 | } 121 | 122 | return nil, 0 // No path found 123 | } 124 | -------------------------------------------------------------------------------- /sample_project/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "astar_test/golang_astar" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | // Create 8x8 grid 10 | grid := golang_astar.NewGrid(8, 8) 11 | 12 | // Add barriers 13 | barriers := []golang_astar.Node{ 14 | {2, 4}, {2, 5}, {2, 6}, {3, 6}, {4, 6}, {5, 6}, 15 | {5, 5}, {5, 4}, {5, 3}, {5, 2}, {4, 2}, {3, 2}, 16 | } 17 | 18 | for _, b := range barriers { 19 | grid.Barriers[b] = true 20 | } 21 | 22 | start := golang_astar.Node{0, 0} 23 | goal := golang_astar.Node{7, 7} 24 | 25 | fmt.Printf("Finding path from %v to %v\n", start, goal) 26 | 27 | path, cost := golang_astar.FindPath(grid, start, goal) 28 | if path == nil { 29 | fmt.Println("No path found!") 30 | return 31 | } 32 | 33 | fmt.Printf("Path found with cost %d:\n", cost) 34 | for _, node := range path { 35 | fmt.Printf("%v ", node) 36 | } 37 | fmt.Println() 38 | } 39 | -------------------------------------------------------------------------------- /sample_project/java/Main.java: -------------------------------------------------------------------------------- 1 | package sample_project.java; 2 | 3 | import java.util.List; 4 | 5 | public class Main { 6 | public static void main(String[] args) { 7 | // -1 = blocked 8 | // 0+ = additional movement cost 9 | int[][] maze = { 10 | { 0, 0, 0, 0, 0, 0, 0, 0}, 11 | { 0, 0, 0, 0, 0, 0, 0, 0}, 12 | { 0, 0, 0,100,100,100, 0, 0}, 13 | { 0, 0, 0, 0, 0,100, 0, 0}, 14 | { 0, 0,100, 0, 0,100, 0, 0}, 15 | { 0, 0,100, 0, 0,100, 0, 0}, 16 | { 0, 0,100,100,100,100, 0, 0}, 17 | { 0, 0, 0, 0, 0, 0, 0, 0}, 18 | }; 19 | AStar as = new AStar(maze, 0, 0, true); 20 | List path = as.findPathTo(7, 7); 21 | if (path != null) { 22 | path.forEach((n) -> { 23 | System.out.print("[" + n.x + ", " + n.y + "] "); 24 | maze[n.y][n.x] = -1; 25 | }); 26 | System.out.printf("\nTotal cost: %.02f\n", path.get(path.size() - 1).g); 27 | 28 | for (int[] maze_row : maze) { 29 | for (int maze_entry : maze_row) { 30 | switch (maze_entry) { 31 | case 0: 32 | System.out.print("_"); 33 | break; 34 | case -1: 35 | System.out.print("*"); 36 | break; 37 | default: 38 | System.out.print("#"); 39 | } 40 | } 41 | System.out.println(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sample_project/java/Node.java: -------------------------------------------------------------------------------- 1 | package sample_project.java; 2 | 3 | public class Node implements Comparable { 4 | public Node parent; 5 | public int x, y; 6 | public double g; 7 | public double h; 8 | 9 | Node(Node parent, int xpos, int ypos, double g, double h) { 10 | this.parent = parent; 11 | this.x = xpos; 12 | this.y = ypos; 13 | this.g = g; 14 | this.h = h; 15 | } 16 | 17 | @Override 18 | public int compareTo(Node o) { 19 | return Double.compare(this.g + this.h, o.g + o.h); 20 | } 21 | } -------------------------------------------------------------------------------- /sample_project/js/astar_search.js: -------------------------------------------------------------------------------- 1 | function manhattan(x1, y1, x2, y2) { 2 | return Math.abs(x1 - x2) + Math.abs(y1 - y2); 3 | } 4 | 5 | function aStar (board, startx, starty, goalx, goaly, 6 | open = Array(8 * 8).fill(null), 7 | closed = Array(8 * 8).fill(null), 8 | current = { 9 | "coord": [startx, starty], 10 | "distance": 0, 11 | "heuristic": manhattan(startx, starty, goalx, goaly), 12 | "previous": null 13 | }) { 14 | const [x, y] = [...current.coord]; 15 | 16 | if (x === goalx && y === goaly) { 17 | closed[x + y * 8] = current; 18 | return (lambda = (closed, x, y, startx, starty) => { 19 | if (x === startx && y === starty) { 20 | return [[x, y]]; 21 | } 22 | const [px, py] = closed.filter(e => e !== null) 23 | .find(({coord: [nx, ny]}) => { 24 | return nx === x && ny === y 25 | }).previous; 26 | return lambda(closed, px, py, startx, starty).concat([[x,y]]); 27 | })(closed, x, y, startx, starty); 28 | } 29 | 30 | let newOpen = open.slice(); 31 | [ 32 | [x + 1, y + 1], [x - 1, y - 1], [x + 1, y], [x, y + 1], 33 | [x - 1, y + 1], [x + 1, y - 1], [x - 1, y], [x, y - 1] 34 | ].filter(([x,y]) => x >= 0 && x < 8 && 35 | y >= 0 && y < 8 && 36 | closed[x + y * 8] === null 37 | ).forEach(([x,y]) => { 38 | newOpen[x + y * 8] = { 39 | "coord": [x,y], 40 | "distance": current.distance + (board[x + y * 8] === 1 ? 100 : 1), 41 | "heuristic": manhattan(x, y, goalx, goaly), 42 | "previous": [...current.coord] 43 | }; 44 | }); 45 | 46 | let newClosed = closed.slice(); 47 | newClosed[x + y * 8] = current; 48 | 49 | const [newCurrent,] = newOpen.filter(e => e !== null) 50 | .sort((a, b) => { 51 | return (a.distance + a.heuristic) - (b.distance + b.heuristic); 52 | }); 53 | 54 | const [newx, newy] = [...newCurrent.coord]; 55 | newOpen[newx + newy * 8] = null; 56 | 57 | return aStar(board, startx, starty, goalx, goaly, 58 | newOpen, newClosed, newCurrent); 59 | } 60 | 61 | const board = [ 62 | 0,0,0,0,0,0,0,0, 63 | 0,0,0,0,0,0,0,0, 64 | 0,0,0,0,1,1,1,0, 65 | 0,0,1,0,0,0,1,0, 66 | 0,0,1,0,0,0,1,0, 67 | 0,0,1,1,1,1,1,0, 68 | 0,0,0,0,0,0,0,0, 69 | 0,0,0,0,0,0,0,0 70 | ]; 71 | 72 | console.log(aStar(board, 0,0, 7,7)); -------------------------------------------------------------------------------- /sample_project/js/functions.js: -------------------------------------------------------------------------------- 1 | // Property identifier in a 'pair' with a 'function_expression' 2 | const objWithFuncExpr = { 3 | propFuncExpr: function () { 4 | console.log("Hello, world!"); 5 | }, 6 | }; 7 | 8 | // Property identifier in a 'pair' with an 'arrow_function' 9 | const objWithArrowFunc = { 10 | propArrowFunc: () => {}, 11 | }; 12 | 13 | // Top-level function declaration 14 | function topLevelStandardFunction() {} 15 | 16 | // Variable declarator with an arrow function 17 | const topLevelArrowConst = () => {}; 18 | 19 | // Variable declarator with a function expression 20 | const topLevelFuncExprConst = function namedInnerFuncExpr() {}; 21 | 22 | // Assignment expression with an arrow function 23 | let assignedArrowLet; 24 | assignedArrowLet = () => {}; 25 | -------------------------------------------------------------------------------- /sample_project/js/methods.js: -------------------------------------------------------------------------------- 1 | // Property identifier in a 'method_definition' (class method) 2 | class MyClassExample { 3 | classMethodRegular() {} 4 | 5 | static staticClassMethod() {} 6 | 7 | get getterMethod() { 8 | return this._x; 9 | } 10 | 11 | set setterMethod(value) { 12 | this._x = value; 13 | } 14 | } 15 | 16 | // Property identifier in a 'method_definition' (object shorthand method) 17 | const objWithShorthand = { 18 | shorthandObjMethod() {}, 19 | 20 | *generatorShorthandMethod() {}, 21 | 22 | async asyncShorthandMethod() {}, 23 | }; 24 | -------------------------------------------------------------------------------- /sample_project/perl/astar_search.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; # https://rosettacode.org/wiki/A*_search_algorithm 4 | use warnings; 5 | use List::AllUtils qw( nsort_by ); 6 | 7 | sub distance 8 | { 9 | my ($r1, $c1, $r2, $c2) = split /[, ]/, "@_"; 10 | sqrt( ($r1-$r2)**2 + ($c1-$c2)**2 ); 11 | } 12 | 13 | my $start = '0,0'; 14 | my $finish = '7,7'; 15 | my %barrier = map {$_, 100} 16 | split ' ', '2,4 2,5 2,6 3,6 4,6 5,6 5,5 5,4 5,3 5,2 4,2 3,2'; 17 | my %values = ( $start, 0 ); 18 | my @new = [ $start, 0 ]; 19 | my %from; 20 | my $mid; 21 | while( ! exists $values{$finish} and @new ) 22 | { 23 | my $pick = (shift @new)->[0]; 24 | for my $n ( nsort_by { distance($_, $finish) } # heuristic 25 | grep !/-|8/ && ! exists $values{$_}, 26 | glob $pick =~ s/\d+/{@{[$&-1]},$&,@{[$&+1]}}/gr 27 | ) 28 | { 29 | $from{$n} = $pick; 30 | $values{$n} = $values{$pick} + ( $barrier{$n} // 1 ); 31 | my $new = [ $n, my $dist = $values{$n} ]; 32 | my $low = 0; # binary insertion into @new (the priority queue) 33 | my $high = @new; 34 | $new[$mid = $low + $high >> 1][1] <= $dist 35 | ? ($low = $mid + 1) : ($high = $mid) while $low < $high; 36 | splice @new, $low, 0, $new; # insert in order 37 | } 38 | } 39 | 40 | my $grid = "s.......\n" . ('.' x 8 . "\n") x 7; 41 | substr $grid, /,/ * $` * 9 + $', 1, 'b' for keys %barrier; 42 | my @path = my $pos = $finish; # the walkback to get path 43 | while( $pos ne $start ) 44 | { 45 | substr $grid, $pos =~ /,/ ? $` * 9 + $' : die, 1, 'x'; 46 | unshift @path, $pos = $from{$pos}; 47 | } 48 | print "$grid\nvalue $values{$finish} path @path\n"; -------------------------------------------------------------------------------- /sample_project/php/AStar.php: -------------------------------------------------------------------------------- 1 | maze = $maze; 21 | $this->xstart = $xstart; 22 | $this->ystart = $ystart; 23 | $this->diag = $diag; 24 | $this->now = new Node(null, $xstart, $ystart, 0, 0); 25 | } 26 | 27 | public function findPathTo(int $xend, int $yend): ?array { 28 | $this->xend = $xend; 29 | $this->yend = $yend; 30 | $this->closed[] = $this->now; 31 | $this->addNeighborsToOpenList(); 32 | 33 | while ($this->now->x !== $this->xend || $this->now->y !== $this->yend) { 34 | if (empty($this->open)) { 35 | return null; 36 | } 37 | $this->now = array_shift($this->open); 38 | $this->closed[] = $this->now; 39 | $this->addNeighborsToOpenList(); 40 | } 41 | 42 | array_unshift($this->path, $this->now); 43 | while ($this->now->x !== $this->xstart || $this->now->y !== $this->ystart) { 44 | $this->now = $this->now->parent; 45 | array_unshift($this->path, $this->now); 46 | } 47 | 48 | return $this->path; 49 | } 50 | 51 | private function addNeighborsToOpenList(): void { 52 | for ($x = -1; $x <= 1; $x++) { 53 | for ($y = -1; $y <= 1; $y++) { 54 | if (!$this->diag && $x !== 0 && $y !== 0) { 55 | continue; 56 | } 57 | 58 | $node = new Node( 59 | $this->now, 60 | $this->now->x + $x, 61 | $this->now->y + $y, 62 | $this->now->g, 63 | $this->distance($this->now->x + $x, $this->now->y + $y) 64 | ); 65 | 66 | if (($x !== 0 || $y !== 0) && 67 | $this->now->x + $x >= 0 && $this->now->x + $x < count($this->maze[0]) && 68 | $this->now->y + $y >= 0 && $this->now->y + $y < count($this->maze) && 69 | $this->maze[$this->now->y + $y][$this->now->x + $x] !== -1 && 70 | !$this->findNeighborInList($this->open, $node) && 71 | !$this->findNeighborInList($this->closed, $node)) { 72 | 73 | $node->g = $node->parent->g + 1.0; 74 | $node->g += $this->maze[$this->now->y + $y][$this->now->x + $x]; 75 | $this->open[] = $node; 76 | } 77 | } 78 | } 79 | 80 | usort($this->open, fn($a, $b) => $a->compareTo($b)); 81 | } 82 | 83 | private function distance(int $x, int $y): float { 84 | return sqrt(pow($x - $this->xend, 2) + pow($y - $this->yend, 2)); 85 | } 86 | 87 | private function findNeighborInList(array $list, Node $node): bool { 88 | foreach ($list as $n) { 89 | if ($n->x === $node->x && $n->y === $node->y) { 90 | return true; 91 | } 92 | } 93 | return false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sample_project/php/Comparable.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 13 | $this->x = $x; 14 | $this->y = $y; 15 | $this->g = $g; 16 | $this->h = $h; 17 | } 18 | 19 | public function compareTo($other): int { 20 | $thisF = $this->g + $this->h; 21 | $otherF = $other->g + $other->h; 22 | return $thisF <=> $otherF; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample_project/php/main.php: -------------------------------------------------------------------------------- 1 | findPathTo(7, 7); 23 | 24 | if ($path !== null) { 25 | foreach ($path as $node) { 26 | echo "[{$node->x}, {$node->y}] "; 27 | $maze[$node->y][$node->x] = -1; 28 | } 29 | echo "\nTotal cost: " . number_format(end($path)->g, 2) . "\n"; 30 | 31 | foreach ($maze as $row) { 32 | foreach ($row as $cell) { 33 | switch ($cell) { 34 | case 0: 35 | echo "_"; 36 | break; 37 | case -1: 38 | echo "*"; 39 | break; 40 | default: 41 | echo "#"; 42 | } 43 | } 44 | echo "\n"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample_project/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentic-labs/lsproxy/6042f6087589be8113275546b8158bb7fb5203a4/sample_project/python/__init__.py -------------------------------------------------------------------------------- /sample_project/python/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import time 3 | 4 | def log_execution_time(func): 5 | @wraps(func) 6 | def wrapper(*args, **kwargs): 7 | start_time = time.time() 8 | result = func(*args, **kwargs) 9 | end_time = time.time() 10 | print(f"{func.__name__} took {end_time - start_time:.2f} seconds to execute") 11 | return result 12 | return wrapper 13 | -------------------------------------------------------------------------------- /sample_project/python/graph.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | from decorators import log_execution_time 3 | from enum import Enum 4 | 5 | class GraphBase: 6 | pass 7 | 8 | class CostStrategy(Enum): 9 | BARRIER = "barrier" 10 | DISTANCE = "distance" 11 | COMBINED = "combined" 12 | 13 | class AStarGraph(GraphBase): 14 | def __init__(self): 15 | self._barriers: List[List[Tuple[int, int]]] = [] 16 | self._barriers.append([ 17 | (2, 4), (2, 5), (2, 6), 18 | (3, 6), (4, 6), (5, 6), 19 | (5, 5), (5, 4), (5, 3), 20 | (5, 2), (4, 2), (3, 2), 21 | ]) 22 | 23 | @property 24 | def barriers(self): 25 | return self._barriers 26 | 27 | def _barrier_cost(self, a: Tuple[int, int], b: Tuple[int, int]) -> float: 28 | """Original barrier-based cost calculation""" 29 | for barrier in self.barriers: 30 | if b in barrier: 31 | return 100 32 | return 1 33 | 34 | def _distance_cost(self, a: Tuple[int, int], b: Tuple[int, int]) -> float: 35 | """Cost based on Manhattan distance between points""" 36 | return abs(b[0] - a[0]) + abs(b[1] - a[1]) 37 | 38 | def _combined_cost(self, a: Tuple[int, int], b: Tuple[int, int]) -> float: 39 | """Combines barrier and distance costs""" 40 | barrier_cost = self._barrier_cost(a, b) 41 | distance_cost = self._distance_cost(a, b) 42 | return barrier_cost * distance_cost 43 | 44 | def move_cost(self, a: Tuple[int, int], b: Tuple[int, int], 45 | strategy: CostStrategy = CostStrategy.BARRIER) -> float: 46 | """ 47 | Calculate movement cost between two points using specified strategy. 48 | 49 | Args: 50 | a: Starting position 51 | b: Ending position 52 | strategy: Cost calculation strategy to use 53 | 54 | Returns: 55 | float: Cost of movement 56 | """ 57 | if strategy == CostStrategy.BARRIER: 58 | cost_function = self._barrier_cost 59 | elif strategy == CostStrategy.DISTANCE: 60 | cost_function = self._distance_cost 61 | elif strategy == CostStrategy.COMBINED: 62 | cost_function = self._combined_cost 63 | else: 64 | raise ValueError(f"Unknown cost strategy: {strategy}") 65 | 66 | return cost_function(a, b) 67 | 68 | @log_execution_time 69 | def heuristic(self, start, goal): 70 | D = 1 71 | D2 = 1 72 | dx = abs(start[0] - goal[0]) 73 | dy = abs(start[1] - goal[1]) 74 | return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy) 75 | 76 | @log_execution_time 77 | def get_vertex_neighbours(self, pos, cost_strategy: CostStrategy = CostStrategy.BARRIER): 78 | n = [] 79 | for dx, dy in [ 80 | (1, 0), (-1, 0), (0, 1), (0, -1), 81 | (1, 1), (-1, 1), (1, -1), (-1, -1), 82 | ]: 83 | x2 = pos[0] + dx 84 | y2 = pos[1] + dy 85 | if x2 < 0 or x2 > 7 or y2 < 0 or y2 > 7: 86 | continue 87 | if self.move_cost(pos, (x2, y2), strategy=cost_strategy) < 100: 88 | n.append((x2, y2)) 89 | return n 90 | -------------------------------------------------------------------------------- /sample_project/python/main.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from graph import AStarGraph 3 | from search import a_star_search 4 | from decorators import log_execution_time 5 | 6 | @log_execution_time 7 | def plot_path(path, graph: AStarGraph) -> None: 8 | plt.plot([v[0] for v in path], [v[1] for v in path]) 9 | for barrier in graph.barriers: 10 | plt.plot([v[0] for v in barrier], [v[1] for v in barrier]) 11 | plt.xlim(-1, 8) 12 | plt.ylim(-1, 8) 13 | plt.show() 14 | 15 | def main(): 16 | graph = AStarGraph() 17 | result, cost = a_star_search((0, 0), (7, 7), graph) 18 | print("route", result) 19 | print("cost", cost) 20 | plot_path(result, graph) 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /sample_project/python/search.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Set, Tuple, List 2 | from graph import AStarGraph 3 | from decorators import log_execution_time 4 | 5 | @log_execution_time 6 | def initialize_search(start, end, graph: AStarGraph): 7 | G = {start: 0} # Actual movement cost to each position from the start position 8 | F = { 9 | start: graph.heuristic(start, end) 10 | } # Estimated movement cost of start to end going via this position 11 | closed_vertices = set() 12 | open_vertices = set([start]) 13 | came_from = {} 14 | return G, F, closed_vertices, open_vertices, came_from 15 | 16 | @log_execution_time 17 | def a_star_search(start, end, graph: AStarGraph): 18 | def reconstruct_path(current: any, came_from: Dict) -> List: 19 | """ 20 | Reconstructs the path from end to start using the came_from dictionary. 21 | Returns the path in correct order (start to end). 22 | """ 23 | path = [] 24 | while current in came_from: 25 | path.append(current) 26 | current = came_from[current] 27 | path.append(start) 28 | return path[::-1] 29 | 30 | G, F, closed_vertices, open_vertices, came_from = initialize_search( 31 | start, end, graph 32 | ) 33 | 34 | while open_vertices: 35 | current = min(open_vertices, key=lambda pos: F[pos]) 36 | if current == end: 37 | return reconstruct_path(current, came_from), F[end] 38 | 39 | open_vertices.remove(current) 40 | closed_vertices.add(current) 41 | 42 | for neighbour in graph.get_vertex_neighbours(current): 43 | if neighbour in closed_vertices: 44 | continue 45 | 46 | candidate_g = G[current] + graph.move_cost(current, neighbour) 47 | 48 | if neighbour not in open_vertices: 49 | open_vertices.add(neighbour) 50 | elif candidate_g >= G.get(neighbour, float("inf")): 51 | continue 52 | 53 | came_from[neighbour] = current 54 | G[neighbour] = candidate_g 55 | F[neighbour] = G[neighbour] + graph.heuristic(neighbour, end) 56 | 57 | raise RuntimeError("A* failed to find a solution") 58 | -------------------------------------------------------------------------------- /sample_project/readme.md: -------------------------------------------------------------------------------- 1 | ### Overview 2 | This sample project is designed to replicate some of the challenges you might face with a semicomplex project using multiple languages 3 | 4 | We take code from: 5 | https://rosettacode.org/wiki/A*_search_algorithm 6 | 7 | This code is spread out over multiple files unrealistically, to demonstrate the ability to find, search, and modify with disparate and mixed files. -------------------------------------------------------------------------------- /sample_project/ruby/decorators.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module LogExecutionTime 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | end 7 | 8 | module ClassMethods 9 | def log_time(method_name) 10 | original_method = instance_method(method_name) 11 | define_method(method_name) do |*args| 12 | start_time = Time.now 13 | result = original_method.bind(self).call(*args) 14 | end_time = Time.now 15 | puts "#{method_name} took #{(end_time - start_time).round(2)} seconds to execute" 16 | result 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /sample_project/ruby/graph.rb: -------------------------------------------------------------------------------- 1 | require_relative 'decorators' 2 | 3 | module CostStrategy 4 | BARRIER = :barrier 5 | DISTANCE = :distance 6 | COMBINED = :combined 7 | end 8 | 9 | class GraphBase 10 | end 11 | 12 | class AStarGraph < GraphBase 13 | include LogExecutionTime 14 | 15 | attr_reader :barriers 16 | 17 | def initialize 18 | @barriers = [] 19 | @barriers << [ 20 | [2, 4], [2, 5], [2, 6], 21 | [3, 6], [4, 6], [5, 6], 22 | [5, 5], [5, 4], [5, 3], 23 | [5, 2], [4, 2], [3, 2] 24 | ] 25 | end 26 | 27 | private def barrier_cost(a, b) 28 | @barriers.any? { |barrier| barrier.include?(b) } ? 100 : 1 29 | end 30 | 31 | private def distance_cost(a, b) 32 | (b[0] - a[0]).abs + (b[1] - a[1]).abs 33 | end 34 | 35 | private def combined_cost(a, b) 36 | barrier_cost(a, b) * distance_cost(a, b) 37 | end 38 | 39 | def move_cost(a, b, strategy: CostStrategy::BARRIER) 40 | case strategy 41 | when CostStrategy::BARRIER 42 | barrier_cost(a, b) 43 | when CostStrategy::DISTANCE 44 | distance_cost(a, b) 45 | when CostStrategy::COMBINED 46 | combined_cost(a, b) 47 | else 48 | raise ArgumentError, "Unknown cost strategy: #{strategy}" 49 | end 50 | end 51 | 52 | log_time def heuristic(start, goal) 53 | d = 1 54 | d2 = 1 55 | dx = (start[0] - goal[0]).abs 56 | dy = (start[1] - goal[1]).abs 57 | d * (dx + dy) + (d2 - 2 * d) * [dx, dy].min 58 | end 59 | 60 | log_time def get_vertex_neighbours(pos, cost_strategy: CostStrategy::BARRIER) 61 | neighbors = [] 62 | [ 63 | [1, 0], [-1, 0], [0, 1], [0, -1], 64 | [1, 1], [-1, 1], [1, -1], [-1, -1] 65 | ].each do |dx, dy| 66 | x2 = pos[0] + dx 67 | y2 = pos[1] + dy 68 | next if x2 < 0 || x2 > 7 || y2 < 0 || y2 > 7 69 | 70 | neighbor = [x2, y2] 71 | neighbors << neighbor if move_cost(pos, neighbor, strategy: cost_strategy) < 100 72 | end 73 | neighbors 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /sample_project/ruby/main.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require_relative 'graph' 3 | require_relative 'search' 4 | 5 | # Note: For visualization, we'll need to install the 'matplotlib' gem 6 | begin 7 | require 'matplotlib/pyplot' 8 | rescue LoadError 9 | puts "For visualization, please install matplotlib: gem install matplotlib" 10 | end 11 | 12 | class PathPlotter 13 | include LogExecutionTime 14 | 15 | def initialize(graph) 16 | @graph = graph 17 | end 18 | 19 | log_time def plot_path(path) 20 | plt = Matplotlib::Pyplot 21 | 22 | # Plot the path 23 | plt.plot(path.map { |v| v[0] }, path.map { |v| v[1] }) 24 | 25 | # Plot the barriers 26 | @graph.barriers.each do |barrier| 27 | plt.plot(barrier.map { |v| v[0] }, barrier.map { |v| v[1] }) 28 | end 29 | 30 | plt.xlim(-1, 8) 31 | plt.ylim(-1, 8) 32 | plt.show 33 | end 34 | end 35 | 36 | def main 37 | graph = AStarGraph.new 38 | search = AStarSearch.new(graph) 39 | 40 | start_pos = [0, 0] 41 | goal_pos = [7, 7] 42 | 43 | result, cost = search.search(start_pos, goal_pos) 44 | puts "Route: #{result}" 45 | puts "Cost: #{cost}" 46 | 47 | # Only attempt to plot if matplotlib is available 48 | if defined?(Matplotlib) 49 | plotter = PathPlotter.new(graph) 50 | plotter.plot_path(result) 51 | end 52 | end 53 | 54 | if __FILE__ == $PROGRAM_NAME 55 | main 56 | end 57 | -------------------------------------------------------------------------------- /sample_project/ruby/search.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require_relative 'decorators' 3 | require_relative 'graph' 4 | 5 | class AStarSearch 6 | include LogExecutionTime 7 | 8 | def initialize(graph) 9 | @graph = graph 10 | end 11 | 12 | log_time def initialize_search(start, goal) 13 | { 14 | g_score: { start => 0 }, 15 | f_score: { start => @graph.heuristic(start, goal) }, 16 | closed_vertices: Set.new, 17 | open_vertices: Set.new([start]), 18 | came_from: {} 19 | } 20 | end 21 | 22 | private def reconstruct_path(current, came_from, start) 23 | path = [] 24 | while came_from.key?(current) 25 | path << current 26 | current = came_from[current] 27 | end 28 | path << start 29 | path.reverse 30 | end 31 | 32 | log_time def search(start, goal) 33 | search_state = initialize_search(start, goal) 34 | g_score = search_state[:g_score] 35 | f_score = search_state[:f_score] 36 | closed_vertices = search_state[:closed_vertices] 37 | open_vertices = search_state[:open_vertices] 38 | came_from = search_state[:came_from] 39 | 40 | while open_vertices.any? 41 | current = open_vertices.min_by { |pos| f_score[pos] } 42 | return [reconstruct_path(current, came_from, start), f_score[current]] if current == goal 43 | 44 | open_vertices.delete(current) 45 | closed_vertices.add(current) 46 | 47 | @graph.get_vertex_neighbours(current).each do |neighbor| 48 | next if closed_vertices.include?(neighbor) 49 | 50 | candidate_g = g_score[current] + @graph.move_cost(current, neighbor) 51 | 52 | if !open_vertices.include?(neighbor) 53 | open_vertices.add(neighbor) 54 | elsif candidate_g >= (g_score[neighbor] || Float::INFINITY) 55 | next 56 | end 57 | 58 | came_from[neighbor] = current 59 | g_score[neighbor] = candidate_g 60 | f_score[neighbor] = g_score[neighbor] + @graph.heuristic(neighbor, goal) 61 | end 62 | end 63 | 64 | raise RuntimeError, "A* failed to find a solution" 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /sample_project/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "astar" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /sample_project/rust/src/astar.rs: -------------------------------------------------------------------------------- 1 | use crate::point::Point; 2 | use crate::node::Node; 3 | use crate::map::Map; 4 | 5 | pub struct AStar { 6 | neighbours: [Point; 8], 7 | open: Vec, 8 | closed: Vec, 9 | pub m: Map, 10 | start: Point, 11 | end: Point, 12 | } 13 | 14 | impl AStar { 15 | pub fn new() -> Self { 16 | let neighbours = [ 17 | Point::new(-1, -1), Point::new(1, -1), 18 | Point::new(-1, 1), Point::new(1, 1), 19 | Point::new(0, -1), Point::new(-1, 0), 20 | Point::new(0, 1), Point::new(1, 0), 21 | ]; 22 | AStar { 23 | neighbours, 24 | open: Vec::new(), 25 | closed: Vec::new(), 26 | m: Map::new(), 27 | start: Point::new(0, 0), 28 | end: Point::new(0, 0), 29 | } 30 | } 31 | 32 | fn calc_dist(&self, p: &Point) -> i32 { 33 | let x = self.end.x - p.x; 34 | let y = self.end.y - p.y; 35 | x * x + y * y 36 | } 37 | 38 | fn is_valid(&self, p: &Point) -> bool { 39 | p.x >= 0 && p.y >= 0 && p.x < self.m.w && p.y < self.m.h 40 | } 41 | 42 | fn exist_point(&mut self, p: &Point, cost: i32) -> bool { 43 | if let Some(pos) = self.closed.iter().position(|n| n.pos == *p) { 44 | if self.closed[pos].cost + self.closed[pos].dist < cost { 45 | return true; 46 | } 47 | self.closed.remove(pos); 48 | return false; 49 | } 50 | if let Some(pos) = self.open.iter().position(|n| n.pos == *p) { 51 | if self.open[pos].cost + self.open[pos].dist < cost { 52 | return true; 53 | } 54 | self.open.remove(pos); 55 | return false; 56 | } 57 | false 58 | } 59 | 60 | fn fill_open(&mut self, n: &Node) -> bool { 61 | // Create a local copy of neighbours to avoid borrowing self 62 | let neighbours = self.neighbours; 63 | 64 | for (i, neighbour_offset) in neighbours.iter().enumerate() { 65 | let step_cost = if i < 4 { 1 } else { 1 }; 66 | let neighbour = n.pos + *neighbour_offset; 67 | 68 | if neighbour == self.end { 69 | return true; 70 | } 71 | 72 | if self.is_valid(&neighbour) && self.m.get(neighbour.x, neighbour.y) != 1 { 73 | let nc = step_cost + n.cost; 74 | let dist = self.calc_dist(&neighbour); 75 | 76 | if !self.exist_point(&neighbour, nc + dist) { 77 | self.open.push(Node { 78 | cost: nc, 79 | dist, 80 | pos: neighbour, 81 | parent: n.pos, 82 | }); 83 | } 84 | } 85 | } 86 | false 87 | } 88 | 89 | pub fn search(&mut self, s: Point, e: Point, mp: Map) -> bool { 90 | self.end = e; 91 | self.start = s; 92 | self.m = mp; 93 | 94 | self.open.push(Node { 95 | cost: 0, 96 | pos: s, 97 | parent: Point::new(0, 0), 98 | dist: self.calc_dist(&s), 99 | }); 100 | 101 | while !self.open.is_empty() { 102 | self.open.sort_by(|a, b| (a.cost + a.dist).cmp(&(b.cost + b.dist))); 103 | let n = self.open.remove(0); 104 | self.closed.push(n.clone()); 105 | if self.fill_open(&n) { 106 | return true; 107 | } 108 | } 109 | false 110 | } 111 | 112 | pub fn path(&self) -> (Vec, i32) { 113 | let mut path = vec![self.end]; 114 | let cost = 1 + self.closed.last().unwrap().cost; 115 | path.push(self.closed.last().unwrap().pos); 116 | let mut parent = self.closed.last().unwrap().parent; 117 | 118 | for node in self.closed.iter().rev() { 119 | if node.pos == parent && !(node.pos == self.start) { 120 | path.push(node.pos); 121 | parent = node.parent; 122 | } 123 | } 124 | path.push(self.start); 125 | path.reverse(); 126 | (path, cost) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /sample_project/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | mod point; 2 | mod node; 3 | mod map; 4 | mod astar; 5 | 6 | use std::io::{self, Write}; 7 | use point::Point; 8 | use map::Map; 9 | use astar::AStar; 10 | 11 | fn main() { 12 | let mut astar = AStar::new(); 13 | let map = Map::new(); 14 | let start = Point::new(0, 0); 15 | let end = Point::new(7, 7); 16 | 17 | if astar.search(start, end, map) { 18 | let (path, cost) = astar.path(); 19 | 20 | for y in -1..9 { 21 | for x in -1..9 { 22 | if x < 0 || y < 0 || x > 7 || y > 7 || astar.m.get(x, y) == 1 { 23 | print!("█"); 24 | } else { 25 | if path.contains(&Point::new(x, y)) { 26 | print!("x"); 27 | } else { 28 | print!("."); 29 | } 30 | } 31 | } 32 | println!(); 33 | } 34 | 35 | print!("\nPath cost {}: ", cost); 36 | io::stdout().flush().unwrap(); 37 | 38 | for p in path { 39 | print!("({}, {}) ", p.x, p.y); 40 | } 41 | println!("\n"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample_project/rust/src/map.rs: -------------------------------------------------------------------------------- 1 | pub struct Map { 2 | pub m: [[i8; 8]; 8], 3 | pub w: i32, 4 | pub h: i32, 5 | } 6 | 7 | impl Map { 8 | pub fn new() -> Self { 9 | let m = [ 10 | [0, 0, 0, 0, 0, 0, 0, 0], 11 | [0, 0, 0, 0, 0, 0, 0, 0], 12 | [0, 0, 0, 0, 1, 1, 1, 0], 13 | [0, 0, 1, 0, 0, 0, 1, 0], 14 | [0, 0, 1, 0, 0, 0, 1, 0], 15 | [0, 0, 1, 1, 1, 1, 1, 0], 16 | [0, 0, 0, 0, 0, 0, 0, 0], 17 | [0, 0, 0, 0, 0, 0, 0, 0], 18 | ]; 19 | Map { m, w: 8, h: 8 } 20 | } 21 | 22 | pub fn get(&self, x: i32, y: i32) -> i8 { 23 | self.m[y as usize][x as usize] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample_project/rust/src/node.rs: -------------------------------------------------------------------------------- 1 | use crate::point::Point; 2 | 3 | #[derive(Clone, PartialEq)] 4 | pub struct Node { 5 | pub pos: Point, 6 | pub parent: Point, 7 | pub dist: i32, 8 | pub cost: i32, 9 | } 10 | 11 | impl PartialOrd for Node { 12 | fn partial_cmp(&self, other: &Self) -> Option { 13 | (self.dist + self.cost).partial_cmp(&(other.dist + other.cost)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample_project/rust/src/point.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq)] 2 | pub struct Point { 3 | pub x: i32, 4 | pub y: i32, 5 | } 6 | 7 | impl Point { 8 | pub fn new(x: i32, y: i32) -> Self { 9 | Point { x, y } 10 | } 11 | } 12 | 13 | impl std::ops::Add for Point { 14 | type Output = Self; 15 | 16 | fn add(self, other: Self) -> Self { 17 | Point { 18 | x: self.x + other.x, 19 | y: self.y + other.y, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample_project/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astar-typescript", 3 | "version": "1.0.0", 4 | "description": "A* pathfinding implementation in TypeScript", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "start": "ts-node src/main.ts", 8 | "build": "tsc", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "dependencies": { 12 | "typescript": "^4.9.5", 13 | "ts-node": "^10.9.1", 14 | "@types/node": "^18.15.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample_project/typescript/src/Controls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ControlsProps { 4 | onFindPath: () => void; 5 | onReset: () => void; 6 | isAnimating: boolean; 7 | } 8 | 9 | export const Controls: React.FC = ({ 10 | onFindPath, 11 | onReset, 12 | isAnimating 13 | }) => { 14 | return ( 15 |
16 | 23 | 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /sample_project/typescript/src/GridDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Node } from './node'; 3 | import { Grid } from './types'; 4 | 5 | interface GridDisplayProps { 6 | maze: Grid; 7 | start: { x: number; y: number }; 8 | end: { x: number; y: number }; 9 | path: Node[] | null; 10 | currentStep: number; 11 | isAnimating: boolean; 12 | onCellClick: (x: number, y: number) => void; 13 | } 14 | 15 | export const GridDisplay: React.FC = ({ 16 | maze, 17 | start, 18 | end, 19 | path, 20 | currentStep, 21 | onCellClick 22 | }) => { 23 | const getCellColor = (x: number, y: number): string => { 24 | if (x === start.x && y === start.y) return 'bg-green-500'; 25 | if (x === end.x && y === end.y) return 'bg-red-500'; 26 | if (path?.slice(0, currentStep + 1).some(node => node.x === x && node.y === y)) { 27 | return 'bg-blue-500'; 28 | } 29 | if (maze[y][x] === 100) return 'bg-gray-500'; 30 | if (maze[y][x] === -1) return 'bg-black'; 31 | return 'bg-white'; 32 | }; 33 | 34 | return ( 35 |
36 | {maze.map((row, y) => ( 37 |
38 | {row.map((_, x) => ( 39 |
onCellClick(x, y)} 44 | /> 45 | ))} 46 |
47 | ))} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /sample_project/typescript/src/InfoPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Node } from './node'; 3 | 4 | interface InfoPanelProps { 5 | path: Node[] | null; 6 | } 7 | 8 | export const InfoPanel: React.FC = ({ path }) => { 9 | return ( 10 |
11 |
12 | Click cells to toggle walls. Green = Start, Red = End, Blue = Path 13 |
14 | {path && ( 15 |
16 | Path length: {path.length} steps 17 |
18 | Total cost: {path[path.length - 1].g.toFixed(2)} 19 |
20 | )} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /sample_project/typescript/src/PathfinderDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Node } from './node'; 3 | import { AStar } from './astar'; 4 | import { Grid } from './types'; 5 | import { GridDisplay } from './GridDisplay'; 6 | import { Controls } from './Controls'; 7 | import { InfoPanel } from './InfoPanel'; 8 | 9 | interface PathfinderDisplayProps { 10 | initialMaze?: Grid; 11 | start?: { x: number; y: number }; 12 | end?: { x: number; y: number }; 13 | allowDiagonal?: boolean; 14 | } 15 | 16 | export const PathfinderDisplay: React.FC = ({ 17 | initialMaze = [ 18 | [0, 0, 0, 0, 0, 0, 0, 0], 19 | [0, 0, 0, 0, 0, 0, 0, 0], 20 | [0, 0, 0, 100, 100, 100, 0, 0], 21 | [0, 0, 0, 0, 0, 100, 0, 0], 22 | [0, 0, 100, 0, 0, 100, 0, 0], 23 | [0, 0, 100, 0, 0, 100, 0, 0], 24 | [0, 0, 100, 100, 100, 100, 0, 0], 25 | [0, 0, 0, 0, 0, 0, 0, 0], 26 | ], 27 | start = { x: 0, y: 0 }, 28 | end = { x: 7, y: 7 }, 29 | allowDiagonal = true, 30 | }) => { 31 | const [maze, setMaze] = useState(initialMaze); 32 | const [path, setPath] = useState(null); 33 | const [isAnimating, setIsAnimating] = useState(false); 34 | const [currentStep, setCurrentStep] = useState(0); 35 | 36 | const findPath = () => { 37 | const astar = new AStar(maze, start.x, start.y, allowDiagonal); 38 | const newPath = astar.findPathTo(end.x, end.y); 39 | setPath(newPath); 40 | setCurrentStep(0); 41 | setIsAnimating(true); 42 | }; 43 | 44 | useEffect(() => { 45 | if (isAnimating && path && currentStep < path.length) { 46 | const timer = setTimeout(() => { 47 | setCurrentStep(prev => prev + 1); 48 | }, 500); 49 | return () => clearTimeout(timer); 50 | } 51 | if (currentStep >= (path?.length ?? 0)) { 52 | setIsAnimating(false); 53 | } 54 | }, [isAnimating, currentStep, path]); 55 | 56 | const toggleCell = (x: number, y: number) => { 57 | if ((x === start.x && y === start.y) || (x === end.x && y === end.y)) return; 58 | 59 | const newMaze = maze.map(row => [...row]); 60 | newMaze[y][x] = newMaze[y][x] === 100 ? 0 : 100; 61 | setMaze(newMaze); 62 | setPath(null); 63 | setCurrentStep(0); 64 | }; 65 | 66 | const handleReset = () => { 67 | setMaze(initialMaze); 68 | setPath(null); 69 | setCurrentStep(0); 70 | }; 71 | 72 | return ( 73 |
74 | 79 | 80 | 89 | 90 | 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /sample_project/typescript/src/astar.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './node'; 2 | import { Grid, Point } from './types'; 3 | 4 | // Interface definition 5 | interface IAStarPathfinder { 6 | findPathTo(xend: number, yend: number): Node[] | null; 7 | } 8 | 9 | export class AStar implements IAStarPathfinder { 10 | private open: Node[] = []; 11 | private closed: Node[] = []; 12 | private path: Node[] = []; 13 | private now: Node; 14 | 15 | constructor( 16 | private maze: Grid, 17 | private xstart: number, 18 | private ystart: number, 19 | private diag: boolean 20 | ) { 21 | this.now = new Node(null, xstart, ystart, 0, 0); 22 | } 23 | 24 | // Arrow function method implementing interface method 25 | findPathTo = (xend: number, yend: number): Node[] | null => { 26 | this.closed.push(this.now); 27 | this.addNeighborsToOpenList(xend, yend); 28 | while (this.now.x !== xend || this.now.y !== yend) { 29 | if (this.open.length === 0) { 30 | return null; 31 | } 32 | this.now = this.open[0]; 33 | this.open.splice(0, 1); 34 | this.closed.push(this.now); 35 | this.addNeighborsToOpenList(xend, yend); 36 | } 37 | this.path = [this.now]; 38 | while (this.now.x !== this.xstart || this.now.y !== this.ystart) { 39 | this.now = this.now.parent!; 40 | this.path.unshift(this.now); 41 | } 42 | return this.path; 43 | }; 44 | 45 | // Traditional method with function keyword 46 | private isInBounds(point: Point): boolean { 47 | return ( 48 | point.x >= 0 && 49 | point.x < this.maze[0].length && 50 | point.y >= 0 && 51 | point.y < this.maze.length 52 | ); 53 | } 54 | 55 | // Arrow function as property 56 | private isWalkable = (point: Point): boolean => { 57 | return this.maze[point.y][point.x] !== -1; 58 | }; 59 | 60 | // Method shorthand notation 61 | private addNeighborsToOpenList(xend: number, yend: number): void { 62 | for (let x = -1; x <= 1; x++) { 63 | for (let y = -1; y <= 1; y++) { 64 | if (!this.diag && x !== 0 && y !== 0) { 65 | continue; 66 | } 67 | const newPoint = { 68 | x: this.now.x + x, 69 | y: this.now.y + y 70 | }; 71 | if (x === 0 && y === 0) continue; 72 | if (!this.isInBounds(newPoint)) continue; 73 | if (!this.isWalkable(newPoint)) continue; 74 | const node = new Node( 75 | this.now, 76 | newPoint.x, 77 | newPoint.y, 78 | this.now.g, 79 | this.distance(newPoint.x, newPoint.y, xend, yend) 80 | ); 81 | if ( 82 | this.findNeighborInList(this.open, node) || 83 | this.findNeighborInList(this.closed, node) 84 | ) continue; 85 | node.g = node.parent!.g + 1; 86 | node.g += this.maze[newPoint.y][newPoint.x]; 87 | this.open.push(node); 88 | } 89 | } 90 | this.open.sort((a, b) => a.f() - b.f()); 91 | } 92 | 93 | // Arrow function with explicit parameter types 94 | private distance = (x1: number, y1: number, x2: number, y2: number): number => { 95 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 96 | }; 97 | 98 | // Traditional function expression assigned to property 99 | private findNeighborInList = function(list: Node[], node: Node): boolean { 100 | return list.some(n => n.x === node.x && n.y === node.y); 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /sample_project/typescript/src/main.ts: -------------------------------------------------------------------------------- 1 | import { AStar } from './astar'; 2 | import { PathVisualizer } from './visualization'; 3 | import { Grid } from './types'; 4 | 5 | // Arrow function for main 6 | const main = (): void => { 7 | const maze: Grid = [ 8 | [0, 0, 0, 0, 0, 0, 0, 0], 9 | [0, 0, 0, 0, 0, 0, 0, 0], 10 | [0, 0, 0, 100, 100, 100, 0, 0], 11 | [0, 0, 0, 0, 0, 100, 0, 0], 12 | [0, 0, 100, 0, 0, 100, 0, 0], 13 | [0, 0, 100, 0, 0, 100, 0, 0], 14 | [0, 0, 100, 100, 100, 100, 0, 0], 15 | [0, 0, 0, 0, 0, 0, 0, 0], 16 | ]; 17 | 18 | const astar = new AStar(maze, 0, 0, true); 19 | const path = astar.findPathTo(7, 7); 20 | 21 | if (path) { 22 | PathVisualizer.printPath(path); 23 | PathVisualizer.visualizePath(maze, path); 24 | } else { 25 | console.log('No path found!'); 26 | } 27 | }; 28 | 29 | // IIFE (Immediately Invoked Function Expression) with arrow function 30 | (() => { 31 | main(); 32 | })(); 33 | 34 | export { main }; 35 | -------------------------------------------------------------------------------- /sample_project/typescript/src/node.ts: -------------------------------------------------------------------------------- 1 | export class Node { 2 | constructor( 3 | public parent: Node | null, 4 | public x: number, 5 | public y: number, 6 | public g: number, 7 | public h: number 8 | ) {} 9 | 10 | // Arrow function for the getter 11 | f = (): number => this.g + this.h; 12 | 13 | // Alternative method using arrow function 14 | toString = (): string => `Node(${this.x}, ${this.y})`; 15 | } 16 | -------------------------------------------------------------------------------- /sample_project/typescript/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | export type Grid = number[][]; 7 | -------------------------------------------------------------------------------- /sample_project/typescript/src/visualization.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './node'; 2 | import { Grid } from './types'; 3 | 4 | export class PathVisualizer { 5 | // Traditional static method 6 | static visualizePath(maze: Grid, path: Node[]): void { 7 | const visualMaze = maze.map(row => [...row]); 8 | 9 | // Arrow function in forEach 10 | path.forEach((node): void => { 11 | visualMaze[node.y][node.x] = -1; 12 | }); 13 | 14 | console.log('\nPath visualization:'); 15 | visualMaze.forEach(row => { 16 | console.log(row.map(cell => { 17 | if (cell === 0) return '_'; 18 | if (cell === -1) return '*'; 19 | return '#'; 20 | }).join('')); 21 | }); 22 | 23 | console.log(`\nTotal cost: ${path[path.length - 1].g.toFixed(2)}`); 24 | } 25 | 26 | // Arrow function static method 27 | static printPath = (path: Node[]): void => { 28 | console.log('Path coordinates:'); 29 | path.forEach(node => { 30 | console.log(`[${node.x}, ${node.y}]`); 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /sample_project/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src" 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status 4 | 5 | # Build the application using the build Dockerfile 6 | docker build -t lsproxy-dev lsproxy 7 | 8 | # Run the builder container to create the binary 9 | if ! docker run --rm -v "$(pwd)/lsproxy":/usr/src/app lsproxy-dev; then 10 | echo "Build failed. Exiting." 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/coverage_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status 4 | 5 | # Build the application using the build Dockerfile 6 | docker build -t lsproxy-dev lsproxy 7 | 8 | if ! docker run --rm -v "$(pwd)/lsproxy":/usr/src/app -v "$(pwd)":/mnt/lsproxy_root lsproxy-dev cargo llvm-cov --html; then 9 | echo "Tests failed. Exiting." 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /scripts/fmt.sh: -------------------------------------------------------------------------------- 1 | docker build -t lsproxy-dev lsproxy 2 | docker run --rm -v "$(pwd)/lsproxy":/usr/src/app lsproxy-dev cargo fmt 3 | -------------------------------------------------------------------------------- /scripts/generate_jwt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import jwt 4 | import os 5 | import time 6 | import argparse 7 | from datetime import datetime 8 | 9 | def generate_token(secret=None): 10 | # Get JWT secret from argument or environment variable 11 | jwt_secret = secret or os.getenv('JWT_SECRET') 12 | if not jwt_secret: 13 | print("Error: No JWT secret provided") 14 | print("Please either:") 15 | print(" 1. Set environment variable: export JWT_SECRET=your_secret_here") 16 | print(" 2. Provide secret as argument: ./generate_jwt.py --secret your_secret_here") 17 | return 18 | 19 | # Set expiration to 24 hours from now 20 | exp = int(time.time()) + 86400 # Current time + 24 hours 21 | 22 | # Create claims 23 | claims = { 24 | 'exp': exp 25 | } 26 | 27 | try: 28 | # Generate token 29 | token = jwt.encode(claims, jwt_secret, algorithm='HS256') 30 | 31 | # Print results 32 | print("\nGenerated JWT Token. To use in Swagger UI, copy this token and then enter it in the authorize field of the UI:") 33 | print(f"{token}\n") 34 | print("To use in API requests, add the Bearer prefix:") 35 | print(f"Bearer {token}\n") 36 | 37 | print("Token will expire at:", 38 | datetime.fromtimestamp(exp).strftime('%Y-%m-%d %H:%M:%S'), "(in 24 hours)\n") 39 | 40 | except Exception as e: 41 | print(f"Error generating token: {e}") 42 | 43 | if __name__ == "__main__": 44 | parser = argparse.ArgumentParser(description='Generate JWT token') 45 | parser.add_argument('--secret', type=str, help='JWT secret key') 46 | args = parser.parse_args() 47 | 48 | generate_token(args.secret) 49 | -------------------------------------------------------------------------------- /scripts/generate_spec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status 4 | 5 | ./scripts/build.sh 6 | 7 | # Run the application to generate the OpenAPI spec 8 | docker run --name temp_lsp_box -v "$(pwd)/lsproxy/target/release":/usr/src/app lsproxy-dev ./lsproxy -w 9 | 10 | # Copy the generated OpenAPI spec from the container to the host 11 | docker cp temp_lsp_box:/usr/src/app/openapi.json ./openapi.json 12 | 13 | # Remove the temporary container 14 | docker rm temp_lsp_box 15 | 16 | echo "OpenAPI specification has been generated and saved as openapi.json" 17 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # Exit immediately if a command exits with a non-zero status 3 | 4 | # Color definitions 5 | GREEN='\033[0;32m' 6 | BLUE='\033[0;34m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | # Help function 11 | print_usage() { 12 | echo -e "${BLUE}Usage: $0 [--no-auth] ${NC}" 13 | echo -e " --no-auth : Disable authentication (sets USE_AUTH=false)" 14 | echo -e " workspace_path: Path to the workspace directory" 15 | } 16 | 17 | # Parse command line arguments 18 | WORKSPACE_PATH="" 19 | USE_AUTH=true 20 | 21 | while [[ $# -gt 0 ]]; do 22 | case $1 in 23 | --no-auth) 24 | USE_AUTH=false 25 | shift 26 | ;; 27 | -h|--help) 28 | print_usage 29 | exit 0 30 | ;; 31 | *) 32 | if [ -z "$WORKSPACE_PATH" ]; then 33 | WORKSPACE_PATH="$1" 34 | else 35 | echo -e "${YELLOW}Warning: Unexpected argument: $1${NC}" 36 | fi 37 | shift 38 | ;; 39 | esac 40 | done 41 | 42 | # Check if workspace path is provided 43 | if [ -z "$WORKSPACE_PATH" ]; then 44 | echo -e "${YELLOW}Error: Workspace path is required${NC}" 45 | print_usage 46 | exit 1 47 | fi 48 | 49 | if [ "$USE_AUTH" = true ]; then 50 | # Generate JWT token for Swagger login 51 | echo -e "${BLUE}Generating JWT token for Swagger UI login...${NC}" 52 | JWT_SECRET=test_secret ./scripts/generate_jwt.py 53 | echo -e "${YELLOW}Note: To disable authentication, you can use the --no-auth flag${NC}" 54 | 55 | # Ask for confirmation to continue - fixed syntax 56 | echo -e "${GREEN}Token has been generated. Press Enter to continue with application startup${NC}" 57 | read -p "(Ctrl+C to cancel)..." 58 | 59 | AUTH_ENV="-e JWT_SECRET=test_secret" 60 | else 61 | echo -e "${BLUE}Running in no-auth mode...${NC}" 62 | AUTH_ENV="-e USE_AUTH=false" 63 | fi 64 | 65 | echo -e "${BLUE}Starting application...${NC}" 66 | 67 | # Run the build 68 | ./scripts/build.sh 69 | 70 | # Run the application 71 | docker run --rm -p 4444:4444 \ 72 | -v "$WORKSPACE_PATH:/mnt/workspace" \ 73 | $AUTH_ENV \ 74 | -v "$(pwd)/lsproxy/target/release":/usr/src/app \ 75 | lsproxy-dev ./lsproxy 76 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status 4 | 5 | # Build the application using the build Dockerfile 6 | docker build -t lsproxy-dev lsproxy 7 | 8 | if ! docker run --rm -v "$(pwd)/lsproxy":/usr/src/app -v "$(pwd)":/mnt/lsproxy_root lsproxy-dev cargo test $@; then 9 | echo "Tests failed. Exiting." 10 | exit 1 11 | fi 12 | --------------------------------------------------------------------------------