diff --git a/.github/workflows/release-lsp.yml b/.github/workflows/release-lsp.yml index e087de1..d93cc42 100644 --- a/.github/workflows/release-lsp.yml +++ b/.github/workflows/release-lsp.yml @@ -1,14 +1,32 @@ name: Release LSP on: - push: - tags: - - '*' + workflow_dispatch: + workflow_run: + workflows: ["Release"] + types: + - completed jobs: + get-tag: + name: Get Release Tag + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + outputs: + tag: ${{ steps.get-tag.outputs.tag }} + steps: + - name: Get tag from latest release + id: get-tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name') + echo "tag=$TAG" >> $GITHUB_OUTPUT + release: name: Release LSP - ${{ matrix.os }} (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} + needs: get-tag strategy: matrix: include: @@ -52,13 +70,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@v4 - - - name: Set variables - id: vars - shell: bash - run: | - echo "package_name=$(sed -En 's/name[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1)" >> $GITHUB_OUTPUT - echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + with: + ref: ${{ needs.get-tag.outputs.tag }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -97,104 +110,47 @@ jobs: name: ${{ matrix.asset_name }} path: target/${{ matrix.target }}/release/${{ matrix.binary_name }} - create-release: - name: Create LSP Release + upload-lsp-binaries: + name: Upload LSP Binaries runs-on: ubuntu-latest - needs: release + needs: [get-tag, release] + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Checkout Source - uses: actions/checkout@v4 - - - name: Set variables - id: vars - run: | - echo "package_name=$(sed -En 's/name[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1)" >> $GITHUB_OUTPUT - echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - - name: Remove Same Release - uses: omarabid-forks/action-rollback@stable - continue-on-error: true - with: - tag: ${{ steps.vars.outputs.package_version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Release - id: create-release - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.vars.outputs.package_version }} - release_name: LSP Server ${{ steps.vars.outputs.package_version }} - body: ${{ steps.vars.outputs.package_name }} LSP Server - ${{ steps.vars.outputs.package_version }} - draft: false - prerelease: false - - name: Upload Linux x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-linux-x64/ci - asset_name: ci-lsp-linux-x64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-linux-x64/ci ci-lsp-linux-x64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-linux-x64 --repo ${{ github.repository }} - name: Upload Linux arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-linux-arm64/ci - asset_name: ci-lsp-linux-arm64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-linux-arm64/ci ci-lsp-linux-arm64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-linux-arm64 --repo ${{ github.repository }} - name: Upload Windows x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-windows-x64.exe/ci.exe - asset_name: ci-lsp-windows-x64.exe - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-windows-x64.exe/ci.exe ci-lsp-windows-x64.exe + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-windows-x64.exe --repo ${{ github.repository }} - name: Upload Windows arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-windows-arm64.exe/ci.exe - asset_name: ci-lsp-windows-arm64.exe - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-windows-arm64.exe/ci.exe ci-lsp-windows-arm64.exe + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-windows-arm64.exe --repo ${{ github.repository }} - name: Upload macOS x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-darwin-x64/ci - asset_name: ci-lsp-darwin-x64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-darwin-x64/ci ci-lsp-darwin-x64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-darwin-x64 --repo ${{ github.repository }} - name: Upload macOS arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-darwin-arm64/ci - asset_name: ci-lsp-darwin-arm64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-darwin-arm64/ci ci-lsp-darwin-arm64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-darwin-arm64 --repo ${{ github.repository }} - name: Purge artifacts uses: omarabid-forks/purge-artifacts@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d319644..f90cb36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,25 +116,85 @@ jobs: with: path: artifacts - - name: Create Release and Upload Assets + - name: Remove Same Release + uses: omarabid-forks/action-rollback@stable + continue-on-error: true + with: + tag: ${{ steps.vars.outputs.package_version }} env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.vars.outputs.package_version }} - PACKAGE_NAME: ${{ steps.vars.outputs.package_name }} - run: | - # Delete existing release if exists - gh release delete "$TAG" --yes || true - - # Create release - gh release create "$TAG" \ - --title "Version $TAG" \ - --notes "$PACKAGE_NAME - $TAG" \ - artifacts/ci-linux-x86_64/ci \ - artifacts/ci-linux-aarch64/ci \ - artifacts/ci-windows-x86_64.exe/ci.exe \ - artifacts/ci-windows-aarch64.exe/ci.exe \ - artifacts/ci-macos-x86_64/ci \ - artifacts/ci-macos-aarch64/ci + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Release + id: create-release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.vars.outputs.package_version }} + release_name: ${{ steps.vars.outputs.package_name }} ${{ steps.vars.outputs.package_version }} + body: ${{ steps.vars.outputs.package_name }} - ${{ steps.vars.outputs.package_version }} + draft: false + prerelease: false + + - name: Upload Linux x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-linux-x86_64/ci + asset_name: ci-linux-x86_64 + asset_content_type: application/octet-stream + + - name: Upload Linux aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-linux-aarch64/ci + asset_name: ci-linux-aarch64 + asset_content_type: application/octet-stream + + - name: Upload Windows x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-windows-x86_64.exe/ci.exe + asset_name: ci-windows-x86_64.exe + asset_content_type: application/octet-stream + + - name: Upload Windows aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-windows-aarch64.exe/ci.exe + asset_name: ci-windows-aarch64.exe + asset_content_type: application/octet-stream + + - name: Upload macOS x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-macos-x86_64/ci + asset_name: ci-macos-x86_64 + asset_content_type: application/octet-stream + + - name: Upload macOS aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-macos-aarch64/ci + asset_name: ci-macos-aarch64 + asset_content_type: application/octet-stream - name: Purge artifacts uses: omarabid-forks/purge-artifacts@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 120e748..fc98f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,28 @@ All notable changes to this project will be documented in this file. -## [0.0.5] - 2025-03-02 +## [0.1.0] - 2026-03-07 ### Added + +- **VS Code Extension**: Companion extension for Visual Studio Code with LSP integration +- **Neovim Plugin**: Full Neovim support with telescope integration for browsing files by ownership +- **Vim Plugin**: Vim plugin for CODEOWNERS LSP integration +- **LSP Enhancements**: + - Inlay hints support for CODEOWNERS patterns + - Custom commands: `listFiles`, `listOwners`, `listTags`, `getFileOwnership` + - TCP transport support (`ci lsp --port `) + +### Changed + +- Refactored LSP server into modular structure for better maintainability +- Simplified VS Code extension by removing unused features +- Improved telescope file pickers with proper entry makers + +## [0.0.5] - 2026-03-02 + +### Added + - **LSP Server**: Language Server Protocol implementation for IDE integration - `textDocument/hover`: Shows owners and tags when hovering over files - `textDocument/codeLens`: Displays ownership annotations above files @@ -14,20 +33,24 @@ All notable changes to this project will be documented in this file. - **VS Code Extension**: Companion extension for Visual Studio Code (in `vscode-extension/`) ### Changed + - Upgraded `utoipa` to 5.4.0 with schema fixes - LSP feature is now opt-in via `--features lsp` cargo flag ### Fixed + - Safe serialization of CodeLens arguments - Output redirected to stderr for LSP compatibility -## [0.0.4] - 2024-12-XX +## [0.0.4] - 2025-12-XX ### Added + - `infer-owners` command for intelligent owner inference - ARM64 runners for release builds (Linux, Windows, macOS) ### Changed + - Dependency updates and formatting improvements ## [0.0.3] - Previous Release diff --git a/README.md b/README.md index 26f6dc6..6a48d06 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,13 @@ ### Pre-built Binaries -**Latest Release: `0.0.4`** +**Latest Release: `0.1.0`** -- **Linux x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-linux-x86_64) -- **Linux ARM64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-linux-aarch64) -- **Windows x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-windows-x86_64.exe) -- **macOS Intel**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-macos-x86_64) -- **macOS Apple Silicon**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-macos-aarch64) +- **Linux x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.1.0/ci-linux-x86_64) +- **Linux ARM64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.1.0/ci-linux-aarch64) +- **Windows x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.1.0/ci-windows-x86_64.exe) +- **macOS Intel**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.1.0/ci-macos-x86_64) +- **macOS Apple Silicon**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.1.0/ci-macos-aarch64) #### Installation Instructions diff --git a/ci/Cargo.toml b/ci/Cargo.toml index 0e37b2e..a8346b2 100644 --- a/ci/Cargo.toml +++ b/ci/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ci" -version = "0.0.5" +version = "0.1.0" authors = ["Abid Omar "] edition = "2021" repository = "https://github.com/code-input/cli" @@ -24,7 +24,7 @@ syslog = ["codeinput/syslog"] lsp = ["codeinput/tower-lsp", "codeinput/tokio", "tokio"] [dependencies] -codeinput = { version = "0.0.5", path = "../codeinput" } +codeinput = { version = "0.1.0", path = "../codeinput" } human-panic = { workspace = true } better-panic = { workspace = true } log = { workspace = true } diff --git a/codeinput/Cargo.toml b/codeinput/Cargo.toml index ed7f8a8..e021398 100644 --- a/codeinput/Cargo.toml +++ b/codeinput/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codeinput" -version = "0.0.5" +version = "0.1.0" authors = ["Abid Omar "] edition = "2021" repository = "https://github.com/code-input/cli" diff --git a/codeinput/src/lsp/commands.rs b/codeinput/src/lsp/commands.rs index 2c35a89..ab541d1 100644 --- a/codeinput/src/lsp/commands.rs +++ b/codeinput/src/lsp/commands.rs @@ -7,6 +7,13 @@ use super::server::LspServer; use super::types::*; impl LspServer { + pub async fn get_file_ownership_command(&self, uri_str: String) -> LspResult> { + let Ok(uri) = Url::parse(&uri_str) else { + return Err(tower_lsp::jsonrpc::Error::invalid_params("Invalid URI")); + }; + Ok(self.get_file_ownership(&uri).await) + } + pub async fn list_files(&self, workspace_uri: Option) -> LspResult { let workspaces = self.workspaces.read().await; @@ -107,3 +114,4 @@ impl LspServer { } } + diff --git a/codeinput/src/lsp/handlers.rs b/codeinput/src/lsp/handlers.rs index 4b6d55f..9be0de3 100644 --- a/codeinput/src/lsp/handlers.rs +++ b/codeinput/src/lsp/handlers.rs @@ -1,11 +1,11 @@ use serde::Serialize; use serde_json::Value; +use tower_lsp::LanguageServer; use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types::*; -use tower_lsp::LanguageServer; use url::Url; -use super::server::{is_codeowners_file, uri_to_path, LspServer}; +use super::server::{LspServer, is_codeowners_file, uri_to_path}; #[tower_lsp::async_trait] impl LanguageServer for LspServer { @@ -51,6 +51,7 @@ impl LanguageServer for LspServer { "codeinput.listFiles".to_string(), "codeinput.listOwners".to_string(), "codeinput.listTags".to_string(), + "codeinput.getFileOwnership".to_string(), ], work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, @@ -259,25 +260,6 @@ impl LanguageServer for LspServer { } } - // Add unowned warning CodeLens - if info.is_unowned { - // Safely serialize arguments - if let Some(uri_val) = serde_json::to_value(file_uri.to_string()).ok() { - lenses.push(CodeLens { - range: Range { - start: Position::new(0, 0), - end: Position::new(0, 0), - }, - command: Some(Command { - title: "$(warning) Unowned file".to_string(), - command: "codeinput.addOwner".to_string(), - arguments: Some(vec![uri_val]), - }), - data: None, - }); - } - } - return Ok(Some(lenses)); } @@ -287,145 +269,90 @@ impl LanguageServer for LspServer { async fn inlay_hint(&self, params: InlayHintParams) -> LspResult>> { let file_uri = params.text_document.uri; - self.client - .log_message( - MessageType::INFO, - format!("inlay_hint called for: {}", file_uri), - ) - .await; - - // Only show inlay hints in CODEOWNERS files let path = uri_to_path(&file_uri); - if let Ok(path) = path { - let file_name = path - .file_name() - .map(|n| n.to_string_lossy()) - .unwrap_or_default(); - self.client - .log_message(MessageType::INFO, format!("file_name: {}", file_name)) - .await; - if file_name.to_lowercase() != "codeowners" { - self.client - .log_message(MessageType::INFO, "Not a CODEOWNERS file, skipping") - .await; - return Ok(None); - } - } else { - self.client - .log_message(MessageType::WARNING, "Failed to convert URI to path") - .await; + let file_path = match path { + Ok(p) => p, + Err(_) => return Ok(None), + }; + + if file_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_lowercase() + != "codeowners" + { return Ok(None); } let workspaces = self.workspaces.read().await; - self.client - .log_message( - MessageType::INFO, - format!("Workspace count: {}", workspaces.len()), - ) - .await; - // Find the workspace that contains this file - for (root_uri, state) in workspaces.iter() { - self.client - .log_message( - MessageType::INFO, - format!("Checking workspace: {}", root_uri), - ) - .await; - let mut hints = vec![]; + for (_root_uri, state) in workspaces.iter() { + let file_entries: Vec<_> = state + .cache + .entries + .iter() + .filter(|e| e.source_file == file_path) + .collect(); + + if file_entries.is_empty() { + continue; + } - let file_path = match uri_to_path(&file_uri) { - Ok(p) => p, + // Read the CODEOWNERS file content asynchronously to get line lengths + let content = match tokio::fs::read_to_string(&file_path).await { + Ok(c) => c, Err(_) => continue, }; - // Read the CODEOWNERS file content - if let Ok(content) = std::fs::read_to_string(&file_path) { - self.client - .log_message( - MessageType::INFO, - format!("Read CODEOWNERS file, {} lines", content.lines().count()), - ) - .await; - for (line_num, line) in content.lines().enumerate() { - let trimmed = line.trim(); - - // Skip empty lines and comments - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } + let lines: Vec<&str> = content.lines().collect(); + let mut hints = vec![]; - // Parse pattern from line - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - if !parts.is_empty() { - let pattern = parts[0]; - - // Count files matching this pattern using the cache - let match_count = state - .cache - .entries - .iter() - .filter(|entry| { - let rel_path = entry.source_file.to_string_lossy(); - // Simple pattern matching - if pattern == "*" { - return true; - } - if pattern.ends_with('/') { - let dir_pattern = &pattern[..pattern.len() - 1]; - rel_path.starts_with(dir_pattern) - } else if pattern.starts_with('*') { - let suffix = &pattern[1..]; - rel_path.ends_with(suffix) - } else if pattern.ends_with("/*") { - let prefix = &pattern[..pattern.len() - 2]; - rel_path.starts_with(prefix) - } else { - rel_path.contains(pattern.trim_start_matches('/')) - } - }) - .count(); - - if match_count > 0 { - let line_length = line.len() as u32; - hints.push(InlayHint { - position: Position::new(line_num as u32, line_length), - label: InlayHintLabel::String(format!( - " {} file{}", - match_count, - if match_count == 1 { "" } else { "s" } - )), - kind: Some(InlayHintKind::TYPE), - text_edits: None, - tooltip: Some(InlayHintTooltip::String(format!( - "This pattern matches {} file(s) in the repository", - match_count - ))), - padding_left: Some(true), - padding_right: Some(false), - data: None, - }); - } + for entry in file_entries { + let line_idx = if entry.line_number > 0 { + entry.line_number - 1 + } else { + 0 + }; + let line_length = lines.get(line_idx).map(|l| l.len() as u32).unwrap_or(0); + + let matcher = crate::core::types::codeowners_entry_to_matcher(entry); + + let mut match_count = 0; + for file_entry in &state.cache.files { + if matcher + .override_matcher + .matched(&file_entry.path, false) + .is_whitelist() + { + match_count += 1; } } - self.client - .log_message( - MessageType::INFO, - format!("Generated {} inlay hints", hints.len()), - ) - .await; + hints.push(InlayHint { + position: Position::new(line_idx as u32, line_length), + label: InlayHintLabel::String(format!( + " {} file{}", + match_count, + if match_count == 1 { "" } else { "s" } + )), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: Some(InlayHintTooltip::String(format!( + "This pattern matches {} file(s) in the repository", + match_count + ))), + padding_left: Some(true), + padding_right: Some(false), + data: None, + }); + } + + if !hints.is_empty() { return Ok(Some(hints)); } } - self.client - .log_message( - MessageType::WARNING, - "No workspace found for CODEOWNERS file", - ) - .await; Ok(None) } @@ -459,6 +386,19 @@ impl LanguageServer for LspServer { } match params.command.as_str() { + "codeinput.getFileOwnership" => { + let uri_val = params + .arguments + .first() + .and_then(|v| v.as_str()) + .ok_or_else(|| { + tower_lsp::jsonrpc::Error::invalid_params( + "Expected a single URI string argument", + ) + })?; + let result = self.get_file_ownership_command(uri_val.to_string()).await?; + to_value(result).map(Some) + } "codeinput.listFiles" => { let result = self.list_files(None).await?; to_value(result).map(Some) @@ -475,4 +415,3 @@ impl LanguageServer for LspServer { } } } - diff --git a/extensions/vscode/.gitignore b/extensions/vscode/.gitignore index d61721c..d67180e 100644 --- a/extensions/vscode/.gitignore +++ b/extensions/vscode/.gitignore @@ -22,6 +22,7 @@ Thumbs.db # IDE .idea/ +.vscode/ *.swp *.swo @@ -31,3 +32,6 @@ coverage/ # Temporary files *.tmp *.temp + +# Yarn cache +.yarn/ diff --git a/extensions/vscode/.vscodeignore b/extensions/vscode/.vscodeignore index 1681d2e..d35f90a 100644 --- a/extensions/vscode/.vscodeignore +++ b/extensions/vscode/.vscodeignore @@ -15,3 +15,5 @@ vsc-extension-quickstart.md **/*.map **/*.ts *.vsix +codeinput-*.vsix +favicon.svg diff --git a/extensions/vscode/.yarnrc.yml b/extensions/vscode/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/extensions/vscode/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/extensions/vscode/CHANGELOG.md b/extensions/vscode/CHANGELOG.md new file mode 100644 index 0000000..d83a612 --- /dev/null +++ b/extensions/vscode/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - 2026-03-07 + +### Added +- Initial release +- LSP-based CODEOWNERS integration +- Status bar ownership indicator +- File ownership info command +- Automatic binary download +- Tag support for CODEOWNERS diff --git a/extensions/vscode/LICENSE b/extensions/vscode/LICENSE new file mode 100644 index 0000000..3781661 --- /dev/null +++ b/extensions/vscode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Abid Omar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md new file mode 100644 index 0000000..b0bc7f4 --- /dev/null +++ b/extensions/vscode/README.md @@ -0,0 +1,65 @@ +# CodeInput - CODEOWNERS Integration for VS Code + +A Language Server Protocol (LSP) based extension that provides CODEOWNERS integration for Visual Studio Code. Automatically displays ownership information and tags for files in your workspace. + +## Features + +- **Status Bar Integration**: Shows ownership status at a glance with lock/alert icons +- **File Ownership Info**: View owners and tags for any file via command palette +- **Multi-format Support**: Works with GitHub, GitLab, and Bitbucket CODEOWNERS file locations +- **Tag Support**: Extended CODEOWNERS syntax with tag support (e.g., `*.rs @team #backend #critical`) +- **Automatic Binary Management**: Downloads the required LSP server binary automatically + +## Requirements + +The extension requires the `ci-lsp` binary. It will attempt to download it automatically, or you can specify a custom path in settings. + +## Installation + +1. Install the extension from the VS Code Marketplace +2. Open a workspace containing a CODEOWNERS file +3. The extension will activate automatically + +## Configuration + +| Setting | Description | Default | +| --------------------------- | --------------------------------------- | ------------------- | +| `codeinput.binaryPath` | Path to the codeinput LSP server binary | `ci-lsp` | +| `codeinput.cacheFile` | Name of the cache file | `.codeowners.cache` | +| `codeinput.showInStatusBar` | Show CODEOWNERS info in status bar | `true` | +| `codeinput.showDiagnostics` | Show diagnostics for unowned files | `true` | +| `codeinput.lspTransport` | Transport method for LSP (stdio/tcp) | `stdio` | +| `codeinput.lspPort` | TCP port for LSP communication | `8123` | + +## Commands + +| Command | Description | +| -------------------------------------------------- | ------------------------------------------- | +| `CodeInput: Show CODEOWNERS Info for Current File` | Display owners and tags for the active file | +| `CodeInput: Refresh CODEOWNERS Cache` | Force refresh the ownership cache | + +## CODEOWNERS File Locations + +The extension monitors these locations for CODEOWNERS files: + +- `CODEOWNERS` (root) +- `.github/CODEOWNERS` +- `.gitlab/CODEOWNERS` +- `docs/CODEOWNERS` + +## Extended Syntax + +CodeInput supports an extended CODEOWNERS syntax with tags: + +``` +# Standard GitHub syntax +*.rs @rust-team + +# Extended syntax with tags +src/api/** @backend-team #api #critical +*.md @docs-team #documentation +``` + +## License + +MIT diff --git a/extensions/vscode/icon.png b/extensions/vscode/icon.png new file mode 100644 index 0000000..7cc06ce Binary files /dev/null and b/extensions/vscode/icon.png differ diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index e728742..3bf9eb1 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,9 +2,18 @@ "name": "codeinput", "displayName": "CodeInput - CODEOWNERS Integration", "description": "LSP-based CODEOWNERS integration for VS Code", - "version": "0.0.1", + "version": "0.1.0", "publisher": "codeinput", "license": "MIT", + "icon": "icon.png", + "repository": { + "type": "git", + "url": "https://github.com/code-input/cli.git" + }, + "bugs": { + "url": "https://github.com/code-input/cli/issues" + }, + "homepage": "https://github.com/code-input/cli#readme", "engines": { "vscode": "^1.74.0" }, @@ -27,21 +36,6 @@ "main": "./out/extension.js", "contributes": { "commands": [ - { - "command": "codeinput.showOwners", - "title": "Show CODEOWNERS Owners", - "category": "CodeInput" - }, - { - "command": "codeinput.showTags", - "title": "Show CODEOWNERS Tags", - "category": "CodeInput" - }, - { - "command": "codeinput.addOwner", - "title": "Add CODEOWNERS Entry", - "category": "CodeInput" - }, { "command": "codeinput.refreshCache", "title": "Refresh CODEOWNERS Cache", @@ -91,22 +85,6 @@ "description": "TCP port for LSP communication (when using tcp transport)" } } - }, - "menus": { - "commandPalette": [ - { - "command": "codeinput.showOwners", - "when": "false" - }, - { - "command": "codeinput.showTags", - "when": "false" - }, - { - "command": "codeinput.addOwner", - "when": "false" - } - ] } }, "scripts": { diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index 8aa5287..69bf333 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { + ExecuteCommandRequest, LanguageClient, LanguageClientOptions, ServerOptions, @@ -9,7 +10,7 @@ import { export interface FileOwnershipInfo { path: string; owners: Array<{ identifier: string; owner_type: string }>; - tags: Array<{ 0: string }>; + tags: string[]; is_unowned: boolean; } @@ -97,68 +98,22 @@ export class CodeInputClient { return undefined; } - // Use the hover request to get ownership info try { - const hover = await this.client.sendRequest('textDocument/hover', { - textDocument: { uri: uri.toString() }, - position: { line: 0, character: 0 } - }) as any; + const info = await this.client.sendRequest(ExecuteCommandRequest.type, { + command: 'codeinput.getFileOwnership', + arguments: [uri.toString()] + }) as FileOwnershipInfo | null | undefined; - if (hover && hover.contents) { - // Parse hover contents to extract ownership info - return this.parseHoverContents(hover.contents, uri); + if (info) { + return info; } } catch (error) { - console.error('Error getting file ownership:', error); + console.error('[CodeInput Client] Error:', error); } return undefined; } - private parseHoverContents(contents: any, uri: vscode.Uri): FileOwnershipInfo | undefined { - // The LSP server returns hover contents with owners and tags - // This is a simplified parser - in production you'd want more robust parsing - const info: FileOwnershipInfo = { - path: uri.fsPath, - owners: [], - tags: [], - is_unowned: false - }; - - if (Array.isArray(contents)) { - for (const item of contents) { - if (typeof item === 'string') { - if (item.includes('Owners:')) { - const ownersMatch = item.match(/\*\*Owners:\*\* (.+)/); - if (ownersMatch) { - const ownersStr = ownersMatch[1]; - if (ownersStr !== '(none)') { - info.owners = ownersStr.split(', ').map(o => ({ - identifier: o.replace(/`/g, ''), - owner_type: 'Unknown' - })); - } - } - } - if (item.includes('Tags:')) { - const tagsMatch = item.match(/\*\*Tags:\*\* (.+)/); - if (tagsMatch) { - const tagsStr = tagsMatch[1]; - info.tags = tagsStr.split(', ').map(t => ({ - 0: t.replace(/`#/g, '').replace(/`/g, '') - })); - } - } - if (item.includes('Warning')) { - info.is_unowned = true; - } - } - } - } - - return info; - } - public isRunning(): boolean { return this.client?.isRunning() === true; } diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 902dd94..166c8a6 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -12,7 +12,6 @@ async function checkBinary(binaryPath: string): Promise { const proc = spawn(binaryPath, ['lsp', '--help']); proc.on('error', () => resolve(false)); proc.on('exit', (code) => resolve(code === 0)); - // Timeout after 2 seconds setTimeout(() => { proc.kill(); resolve(false); @@ -23,31 +22,19 @@ async function checkBinary(binaryPath: string): Promise { export async function activate(context: vscode.ExtensionContext) { console.log('CodeInput extension is now active'); - // Check if custom binary path is set const config = vscode.workspace.getConfiguration('codeinput'); - const userBinaryPath = config.get('binaryPath', 'ci-lsp'); + const binaryPath = config.get('binaryPath', 'ci-lsp'); let finalBinaryPath: string | undefined; let binarySource = 'unknown'; - // If user set a custom path, use it - if (userBinaryPath !== 'ci-lsp') { - console.log(`[CodeInput] Checking user-specified binary: ${userBinaryPath}`); - const exists = await checkBinary(userBinaryPath); - if (exists) { - finalBinaryPath = userBinaryPath; - binarySource = 'user-config'; - console.log(`[CodeInput] Using user-specified binary: ${finalBinaryPath}`); - } else { - console.error(`[CodeInput] User-specified binary not found: ${userBinaryPath}`); - vscode.window.showErrorMessage( - `Custom binary not found: ${userBinaryPath}. Please check the path or install codeinput-lsp.` - ); - return; - } + const exists = await checkBinary(binaryPath); + if (exists) { + finalBinaryPath = binaryPath; + binarySource = 'config'; + console.log(`[CodeInput] Using binary from config: ${finalBinaryPath}`); } else { - // Try to download ci-lsp first - console.log('[CodeInput] Attempting to download ci-lsp binary...'); + console.log(`[CodeInput] Binary not found at ${binaryPath}, attempting to download...`); const downloader = new BinaryDownloader(context); const downloadedPath = await downloader.ensureBinary(); @@ -56,24 +43,11 @@ export async function activate(context: vscode.ExtensionContext) { binarySource = 'downloaded'; console.log(`[CodeInput] Using downloaded binary: ${finalBinaryPath}`); } else { - console.log('[CodeInput] Download failed, checking for existing ci binary with LSP support...'); - // Download failed, check if `ci` with LSP exists - const ciExists = await checkBinary('ci'); - if (ciExists) { - binarySource = 'fallback-ci'; - finalBinaryPath = 'ci'; - console.log('[CodeInput] Using fallback ci binary with LSP support'); - vscode.window.showInformationMessage( - 'Using existing `ci` binary with LSP support. Consider downloading `ci-lsp` for better performance.' - ); - } else { - // Neither works - console.error('[CodeInput] No usable binary found (tried downloading ci-lsp and using ci)'); - vscode.window.showErrorMessage( - 'codeinput-lsp not found. Please install it: curl -L https://github.com/code-input/cli/releases/latest/download/ci-lsp-linux-x64 -o ~/bin/ci-lsp && chmod +x ~/bin/ci-lsp' - ); - return; - } + console.error('[CodeInput] No usable binary found'); + vscode.window.showErrorMessage( + 'codeinput binary not found. Install it or set codeinput.binaryPath in settings.' + ); + return; } } @@ -134,38 +108,6 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.showOwners', (uri: string, owners: any[]) => { - if (owners && owners.length > 0) { - const ownerList = owners.map(o => o.identifier).join(', '); - vscode.window.showInformationMessage(`Owners: ${ownerList}`); - } - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.showTags', (uri: string, tags: any[]) => { - if (tags && tags.length > 0) { - const tagList = tags.map(t => `#${t}`).join(', '); - vscode.window.showInformationMessage(`Tags: ${tagList}`); - } - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.addOwner', async (uri: string) => { - const owner = await vscode.window.showInputBox({ - prompt: 'Enter owner (e.g., @username or @org/team)', - placeHolder: '@username' - }); - - if (owner) { - vscode.window.showInformationMessage(`Would add owner ${owner} to ${uri}`); - // TODO: Implement adding owner to CODEOWNERS file - } - }) - ); - // Handle configuration changes context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(e => { diff --git a/extensions/vscode/src/statusbar.ts b/extensions/vscode/src/statusbar.ts index 40808d7..758ece4 100644 --- a/extensions/vscode/src/statusbar.ts +++ b/extensions/vscode/src/statusbar.ts @@ -12,7 +12,6 @@ export class StatusBarManager implements vscode.Disposable { vscode.StatusBarAlignment.Right, 100 ); - this.statusBarItem.command = 'codeinput.showInfo'; // Update when active editor changes this.disposables.push( @@ -55,7 +54,6 @@ export class StatusBarManager implements vscode.Disposable { const fileUri = editor.document.uri; - // Only show for file:// URIs if (fileUri.scheme !== 'file') { this.statusBarItem.hide(); return; @@ -64,56 +62,25 @@ export class StatusBarManager implements vscode.Disposable { try { const info = await this.client.getFileOwnership(fileUri); - if (info) { - if (info.is_unowned || info.owners.length === 0) { - this.statusBarItem.text = '$(warning) Unowned'; - this.statusBarItem.tooltip = 'This file has no CODEOWNERS assignment\nClick to see details'; - this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - } else { - const ownerNames = info.owners.map(o => o.identifier).join(', '); - const ownerInitials = info.owners - .map(o => this.getInitials(o.identifier)) - .join(''); - - this.statusBarItem.text = `$(lock) ${ownerInitials}`; - this.statusBarItem.tooltip = `Owners: ${ownerNames}\nClick to see details`; - this.statusBarItem.backgroundColor = undefined; - } - - this.statusBarItem.show(); + if (info && !info.is_unowned && info.owners.length > 0) { + this.statusBarItem.text = '$(lock)'; + this.statusBarItem.tooltip = 'Has CODEOWNERS'; + this.statusBarItem.backgroundColor = undefined; } else { - // No CODEOWNERS info available (file not in cache) - this.statusBarItem.hide(); + this.statusBarItem.text = '$(alert)'; + this.statusBarItem.tooltip = 'No CODEOWNERS'; + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); } + this.statusBarItem.show(); } catch (error) { console.error('Error updating status bar:', error); - this.statusBarItem.hide(); + this.statusBarItem.text = '$(alert)'; + this.statusBarItem.tooltip = 'No CODEOWNERS'; + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + this.statusBarItem.show(); } } - private getInitials(identifier: string): string { - // Extract initials from owner identifier - // @org/team -> T, @username -> U, email@domain.com -> E - - if (identifier.startsWith('@')) { - // Handle @org/team format - const parts = identifier.split('/'); - if (parts.length > 1) { - return parts[parts.length - 1].charAt(0).toUpperCase(); - } - // Handle @username format - return identifier.charAt(1).toUpperCase(); - } - - if (identifier.includes('@')) { - // Email format - return identifier.charAt(0).toUpperCase(); - } - - // Fallback - return identifier.charAt(0).toUpperCase(); - } - public dispose(): void { this.statusBarItem.dispose(); this.disposables.forEach(d => d.dispose());