Skip to content

Commit

Permalink
Merge pull request #13 from AlexanderBrevig/ready-to-go-public
Browse files Browse the repository at this point in the history
feat: ready for v1 and public repo
  • Loading branch information
AlexanderBrevig authored Mar 4, 2024
2 parents 0930687 + de8caa0 commit e5b99c9
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
- uses: actions/checkout@v4
- name: Run fmt
run: cargo fmt --all --check
- name: Run clippy
run: cargo clippy -- --deny warnings
- name: Run tests
run: cargo test --verbose
- name: Build
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# github-lsp

[![CI](https://github.com/AlexanderBrevig/github-lsp/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexanderBrevig/github-lsp/actions/workflows/ci.yml)

`github-lsp` is an implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) for working with [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) files.

This is a tool for getting link suggestions while writing READMEs and GitHub Wiki pages locally.

Use this LSP in conjunction with some other Markdown LSP if you want gotoDefinition et.al. This LSP only focuses on adding autocomplete to

- [x] `#` Issues and PRs
- [ ] `[` Wiki Pages
- [ ] `:` Organizations / Owners
- [ ] `/` Repositories
- [ ] `@` Users
- [x] `[` Public Wiki Pages
- [x] `:` Organizations / Owners
- [x] `/` Repositories (yours and the orgs you are part of, no global search yet)
- [x] `@` Organization Members

[Issues](https://github.com/AlexanderBrevig/github-lsp/issues) and [PRs](https://github.com/AlexanderBrevig/github-lsp/pulls) are very welcome!

Expand Down
157 changes: 125 additions & 32 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use tower_lsp::lsp_types::{
};
use tower_lsp::{lsp_types::Position, Client};

use crate::gh::{GetDetail, GetEdit, GetLabel};
use crate::gh::wiki::WikiArticle;
use crate::gh::{self, GetDetail, GetEdit, GetLabel};

#[derive(Debug)]
pub struct Backend {
Expand All @@ -18,12 +19,23 @@ pub struct Backend {
pub(crate) repository_map: DashMap<String, Repository>,
pub(crate) issue_map: DashMap<String, Issue>,
pub(crate) member_map: DashMap<String, Author>,
pub(crate) wiki_map: DashMap<String, WikiArticle>,
octocrab: Octocrab,
owner: String,
repo: String,
}

impl Backend {
const PER_PAGE: u8 = 100;

pub(crate) async fn initialize(&self) {
self.initialize_issues().await;
self.initialize_members().await;
self.initialize_repos_as("owner").await;
self.initialize_repos_as("organization_member").await;
self.initialize_wiki().await;
}

pub(crate) async fn search_issue_and_pr(
&self,
position: Position,
Expand All @@ -36,18 +48,6 @@ impl Backend {
)
.await;
//TODO: refresh issues
// let filter = format!("{} repo:{}/{}", needle, self.owner, self.repo);
// let page = self
// .octocrab
// .search()
// .issues_and_pull_requests(&filter)
// .sort("status")
// .per_page(100)
// .send()
// .await
// .map_err(|_| {
// tower_lsp::jsonrpc::Error::new(tower_lsp::jsonrpc::ErrorCode::MethodNotFound)
// })?;
let completion_items = self
.issue_map
.iter()
Expand Down Expand Up @@ -102,11 +102,35 @@ impl Backend {
Ok(completion_items)
}

pub(crate) async fn search_wiki(&self, needle: &str) -> Result<Vec<CompletionItem>> {
pub(crate) async fn search_wiki(
&self,
position: Position,
needle: &str,
) -> Result<Vec<CompletionItem>> {
self.client
.log_message(MessageType::INFO, format!("search_wiki: {}", needle))
.await;
Ok(vec![])
let completion_items = self
.wiki_map
.iter()
.filter(|member| member.title.starts_with(needle)) //TODO: smarter fuzzy match
.map(|member| CompletionItem {
label: member.title.to_owned(),
detail: None,
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range: Range {
start: Position {
line: position.line,
character: position.character - needle.len() as u32 - 1,
},
end: position,
},
new_text: member.get_edit(),
})),
..CompletionItem::default()
})
.collect::<Vec<CompletionItem>>();
Ok(completion_items)
}

pub(crate) async fn search_repo(
Expand Down Expand Up @@ -186,54 +210,122 @@ impl Backend {
self.document_map
.insert(params.uri.to_string(), rope.clone());
}
pub(crate) async fn initialize(&self) {
self.initialize_repos().await;
self.initialize_issues().await;
self.initialize_members().await;
}

async fn initialize_repos(&self) {
let Ok(repos) = self
async fn initialize_repos_as(&self, affiliation: &str) {
self.client
.log_message(
MessageType::INFO,
format!("initialize_repos_as: {}", affiliation),
)
.await;
let mut page: u8 = 0;
let mut repos: Vec<Repository> = vec![];
while let Ok(mut page_repos) = self
.octocrab
.current()
.list_repos_for_authenticated_user()
.affiliation("organization_member")
.affiliation(affiliation)
.sort("updated")
.per_page(100)
.per_page(Backend::PER_PAGE)
.page(page)
.send()
.await
else {
{
if page_repos.items.is_empty() {
break;
}
repos.append(page_repos.items.as_mut());
page += 1;
}
if repos.is_empty() {
self.client
.log_message(
MessageType::WARNING,
format!("No repos found with affiliation {}", affiliation),
)
.await;
return;
};
repos.into_iter().for_each(|repo| {
self.repository_map.insert(repo.name.to_owned(), repo);
let _ = self.repository_map.insert(repo.name.to_owned(), repo);
});
}

async fn initialize_issues(&self) {
let Ok(issues) = self
self.client
.log_message(MessageType::INFO, "initialize_issues")
.await;
let mut page: u8 = 0;
let mut issues: Vec<Issue> = vec![];
while let Ok(mut page_issues) = self
.octocrab
.issues(&self.owner, &self.repo)
.list()
.per_page(Backend::PER_PAGE)
.page(page)
.send()
.await
else {
{
if page_issues.items.is_empty() {
break;
}
issues.append(page_issues.items.as_mut());
page += 1;
}
if issues.is_empty() {
self.client
.log_message(MessageType::WARNING, "No issues found")
.await;
return;
};
issues.into_iter().for_each(|issue| {
self.issue_map.insert(issue.title.to_owned(), issue);
});
}

async fn initialize_wiki(&self) {
self.client
.log_message(MessageType::INFO, "initialize_wiki")
.await;
let wikis = gh::wiki::find_wiki_articles(&self.owner, &self.repo).await;
match wikis {
Ok(articles) => articles.into_iter().for_each(|article| {
self.wiki_map.insert(article.title.to_owned(), article);
}),
Err(_) => {
self.client
.log_message(MessageType::WARNING, "No wiki found")
.await;
}
}
//TODO: load local .md files and make relative links?
}

async fn initialize_members(&self) {
let Ok(members) = self
self.client
.log_message(MessageType::INFO, "initialize_members")
.await;
let mut page: u8 = 0;
let mut members: Vec<Author> = vec![];
while let Ok(mut page_members) = self
.octocrab
.orgs("entur")
.orgs(self.owner.to_owned())
.list_members()
.page(0_u32)
.per_page(Backend::PER_PAGE)
.page(page)
.send()
.await
else {
{
if page_members.items.is_empty() {
break;
}
members.append(page_members.items.as_mut());
page += 1;
}
if members.is_empty() {
self.client
.log_message(MessageType::WARNING, "No members found")
.await;
return;
};
members.into_iter().for_each(|member| {
Expand All @@ -251,6 +343,7 @@ impl Backend {
repository_map: DashMap::new(),
issue_map: DashMap::new(),
member_map: DashMap::new(),
wiki_map: DashMap::new(),
}
}
}
2 changes: 1 addition & 1 deletion src/gh/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod author;
mod issue;
mod repo;
mod wiki;
pub(crate) mod wiki;

use std::fmt;

Expand Down
31 changes: 25 additions & 6 deletions src/gh/wiki.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
use reqwest::Client;
use select::{document::Document, predicate::Name};

use super::GetEdit;

#[derive(Debug)]
pub(crate) struct WikiArticle {
title: String,
uri: String,
pub title: String,
pub uri: String,
}

impl GetEdit for WikiArticle {
fn get_edit(&self) -> String {
let title = self.title.to_owned();
let uri = self.uri.to_owned();
format!("[{title}]({uri})")
}
}
pub(crate) async fn find_wiki_articles(
client: &Client,
url: &str,

pub async fn find_wiki_articles(
owner: &str,
repo: &str,
) -> Result<Vec<WikiArticle>, reqwest::Error> {
let client = Client::new();
//TODO: find a way to support private wikis?
let url = format!("https://github.com/{owner}/{repo}/wiki");
let res = client.get(url).send().await?;
let body = res.text().await?;
let ret: Vec<WikiArticle> = Document::from(body.as_str())
let mut ret: Vec<WikiArticle> = Document::from(body.as_str())
.find(Name("a"))
.filter(|a| a.attr("href").is_some())
.filter(|a| {
Expand All @@ -26,6 +41,10 @@ pub(crate) async fn find_wiki_articles(
uri: link.attr("href").unwrap().to_string(),
})
.collect();
ret.push(WikiArticle {
title: "Home".into(),
uri: format!("/{owner}/{repo}/wiki"),
});

Ok(ret)
}
Loading

0 comments on commit e5b99c9

Please sign in to comment.