mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-24 12:53:52 +00:00
add djls-django crate (#6)
* add djls-django crate * rework * oops * add check for GDAL and GeoDjango * lots of things * remove unused scripts * move scripts to dedicated mod and make static consts * inline gdal check * rename mod * rename mod * move server info to consts * adjust pyproject * hide rustfmt config * simplify django setup * adjust printing
This commit is contained in:
parent
b7a1de98dd
commit
fce343f44d
32 changed files with 1139 additions and 291 deletions
12
crates/djls-django/Cargo.toml
Normal file
12
crates/djls-django/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "djls-django"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-python = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
thiserror = "2.0.4"
|
64
crates/djls-django/src/apps.rs
Normal file
64
crates/djls-django/src/apps.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use djls_python::{Python, RunnerError, ScriptRunner};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::scripts;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App(String);
|
||||
|
||||
impl App {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for App {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Apps(Vec<App>);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InstalledAppsCheck {
|
||||
has_app: bool,
|
||||
}
|
||||
|
||||
impl ScriptRunner for InstalledAppsCheck {
|
||||
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK;
|
||||
}
|
||||
|
||||
impl Apps {
|
||||
pub fn from_strings(apps: Vec<String>) -> Self {
|
||||
Self(apps.into_iter().map(App).collect())
|
||||
}
|
||||
|
||||
pub fn apps(&self) -> &[App] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn has_app(&self, name: &str) -> bool {
|
||||
self.0.iter().any(|app| app.0 == name)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &App> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> {
|
||||
let result = InstalledAppsCheck::run_with_py_args(py, app)?;
|
||||
Ok(result.has_app)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Apps {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for app in &self.0 {
|
||||
writeln!(f, " {}", app)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
120
crates/djls-django/src/django.rs
Normal file
120
crates/djls-django/src/django.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use crate::apps::Apps;
|
||||
use crate::gis::{check_gis_setup, GISError};
|
||||
use crate::scripts;
|
||||
use crate::templates::TemplateTag;
|
||||
use djls_python::{ImportCheck, Python, RunnerError, ScriptRunner};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DjangoProject {
|
||||
py: Python,
|
||||
settings_module: String,
|
||||
installed_apps: Apps,
|
||||
templatetags: Vec<TemplateTag>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DjangoSetup {
|
||||
installed_apps: Vec<String>,
|
||||
templatetags: Vec<TemplateTag>,
|
||||
}
|
||||
|
||||
impl ScriptRunner for DjangoSetup {
|
||||
const SCRIPT: &'static str = scripts::DJANGO_SETUP;
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
fn new(
|
||||
py: Python,
|
||||
settings_module: String,
|
||||
installed_apps: Apps,
|
||||
templatetags: Vec<TemplateTag>,
|
||||
) -> Self {
|
||||
Self {
|
||||
py,
|
||||
settings_module,
|
||||
installed_apps,
|
||||
templatetags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<Self, ProjectError> {
|
||||
let settings_module =
|
||||
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
|
||||
|
||||
let py = Python::initialize()?;
|
||||
|
||||
let has_django = ImportCheck::check(&py, "django")?;
|
||||
|
||||
if !has_django {
|
||||
return Err(ProjectError::DjangoNotFound);
|
||||
}
|
||||
|
||||
if !check_gis_setup(&py)? {
|
||||
eprintln!("Warning: GeoDjango detected but GDAL is not available.");
|
||||
eprintln!("Django initialization will be skipped. Some features may be limited.");
|
||||
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
|
||||
|
||||
return Ok(Self {
|
||||
py,
|
||||
settings_module,
|
||||
installed_apps: Apps::default(),
|
||||
templatetags: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let setup = DjangoSetup::run_with_py(&py)?;
|
||||
|
||||
Ok(Self::new(
|
||||
py,
|
||||
settings_module,
|
||||
Apps::from_strings(setup.installed_apps.to_vec()),
|
||||
setup.templatetags.to_vec(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn py(&self) -> &Python {
|
||||
&self.py
|
||||
}
|
||||
|
||||
fn settings_module(&self) -> &String {
|
||||
&self.settings_module
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DjangoProject {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Django Project")?;
|
||||
writeln!(f, "Settings Module: {}", self.settings_module)?;
|
||||
writeln!(f, "Installed Apps:")?;
|
||||
write!(f, "{}", self.installed_apps)?;
|
||||
writeln!(f, "Template Tags:")?;
|
||||
for tag in &self.templatetags {
|
||||
write!(f, "{}", tag)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProjectError {
|
||||
#[error("Django is not installed or cannot be imported")]
|
||||
DjangoNotFound,
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("GIS error: {0}")]
|
||||
Gis(#[from] GISError),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] djls_python::PythonError),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] RunnerError),
|
||||
}
|
26
crates/djls-django/src/gis.rs
Normal file
26
crates/djls-django/src/gis.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use crate::apps::Apps;
|
||||
use djls_python::{Python, RunnerError};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn check_gis_setup(py: &Python) -> Result<bool, GISError> {
|
||||
let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?;
|
||||
let gdal_is_installed = Command::new("gdalinfo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(!has_geodjango || gdal_is_installed)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GISError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] RunnerError),
|
||||
}
|
7
crates/djls-django/src/lib.rs
Normal file
7
crates/djls-django/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod apps;
|
||||
mod django;
|
||||
mod gis;
|
||||
mod scripts;
|
||||
mod templates;
|
||||
|
||||
pub use django::DjangoProject;
|
4
crates/djls-django/src/scripts.rs
Normal file
4
crates/djls-django/src/scripts.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
use djls_python::include_script;
|
||||
|
||||
pub const DJANGO_SETUP: &str = include_script!("django_setup");
|
||||
pub const INSTALLED_APPS_CHECK: &str = include_script!("installed_apps_check");
|
30
crates/djls-django/src/templates.rs
Normal file
30
crates/djls-django/src/templates.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TemplateTag {
|
||||
name: String,
|
||||
library: String,
|
||||
doc: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for TemplateTag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let library = if self.library.is_empty() {
|
||||
"builtins"
|
||||
} else {
|
||||
&self.library
|
||||
};
|
||||
|
||||
write!(f, "{} ({})", self.name, library)?;
|
||||
writeln!(f)?;
|
||||
|
||||
if let Some(doc) = &self.doc {
|
||||
for line in doc.trim_end().split("\n") {
|
||||
writeln!(f, "{}", line)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
mod packaging;
|
||||
mod python;
|
||||
mod runner;
|
||||
mod scripts;
|
||||
|
||||
pub use python::Python;
|
||||
pub use crate::packaging::ImportCheck;
|
||||
pub use crate::python::Python;
|
||||
pub use crate::python::PythonError;
|
||||
pub use crate::runner::Runner;
|
||||
pub use crate::runner::RunnerError;
|
||||
pub use crate::runner::ScriptRunner;
|
||||
|
|
|
@ -1,37 +1,18 @@
|
|||
use crate::python::Python;
|
||||
use crate::runner::{RunnerError, ScriptRunner};
|
||||
use crate::scripts;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Package {
|
||||
name: String,
|
||||
version: String,
|
||||
location: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
fn new(name: String, version: String, location: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
version,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &String {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn location(&self) -> &Option<PathBuf> {
|
||||
&self.location
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Package {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {}", self.name, self.version)?;
|
||||
|
@ -42,60 +23,12 @@ impl fmt::Display for Package {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Packages(HashMap<String, Package>);
|
||||
|
||||
impl Packages {
|
||||
fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn from_executable(executable: &Path) -> Result<Self, PackagingError> {
|
||||
let output = Command::new(executable)
|
||||
.args([
|
||||
"-c",
|
||||
r#"
|
||||
import json
|
||||
import importlib.metadata
|
||||
|
||||
packages = {}
|
||||
for dist in importlib.metadata.distributions():
|
||||
try:
|
||||
packages[dist.metadata["Name"]] = {
|
||||
"name": dist.metadata["Name"],
|
||||
"version": dist.version,
|
||||
"location": dist.locate_file("").parent.as_posix() if dist.locate_file("") else None
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
print(json.dumps(packages))
|
||||
"#,
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let packages_info: serde_json::Value = serde_json::from_str(&output_str)?;
|
||||
|
||||
Ok(packages_info
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(name, info)| {
|
||||
(
|
||||
name.clone(),
|
||||
Package {
|
||||
name: name.clone(),
|
||||
version: info["version"].as_str().unwrap().to_string(),
|
||||
location: info["location"].as_str().map(PathBuf::from),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn has_package(&self, name: &str) -> bool {
|
||||
self.0.iter().any(|pkg| pkg.1.name == name)
|
||||
pub fn packages(&self) -> Vec<&Package> {
|
||||
self.0.values().collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +40,7 @@ impl FromIterator<(String, Package)> for Packages {
|
|||
|
||||
impl fmt::Display for Packages {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut packages: Vec<_> = self.0.values().collect();
|
||||
let mut packages: Vec<_> = self.packages();
|
||||
packages.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
if packages.is_empty() {
|
||||
|
@ -121,6 +54,26 @@ impl fmt::Display for Packages {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImportCheck {
|
||||
can_import: bool,
|
||||
}
|
||||
|
||||
impl ScriptRunner for ImportCheck {
|
||||
const SCRIPT: &'static str = scripts::HAS_IMPORT;
|
||||
}
|
||||
|
||||
impl ImportCheck {
|
||||
pub fn can_import(&self) -> bool {
|
||||
self.can_import
|
||||
}
|
||||
|
||||
pub fn check(py: &Python, module: &str) -> Result<bool, RunnerError> {
|
||||
let result = ImportCheck::run_with_py_args(py, module)?;
|
||||
Ok(result.can_import)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PackagingError {
|
||||
#[error("IO error: {0}")]
|
||||
|
@ -129,6 +82,15 @@ pub enum PackagingError {
|
|||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] Box<RunnerError>),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl From<RunnerError> for PackagingError {
|
||||
fn from(err: RunnerError) -> Self {
|
||||
PackagingError::Runner(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,17 @@
|
|||
use crate::packaging::{Packages, PackagingError};
|
||||
use crate::runner::{Runner, RunnerError, ScriptRunner};
|
||||
use crate::scripts;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::path::{Path, PathBuf};
|
||||
use which::which;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct VersionInfo {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
pub patch: u8,
|
||||
pub suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl VersionInfo {
|
||||
fn new(major: u8, minor: u8, patch: u8, suffix: Option<String>) -> Self {
|
||||
Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_executable(executable: &PathBuf) -> Result<Self, PythonError> {
|
||||
let output = Command::new(executable)
|
||||
.args(["-c", "import sys; print(sys.version.split()[0])"])
|
||||
.output()?;
|
||||
|
||||
let version_str = String::from_utf8(output.stdout)?.trim().to_string();
|
||||
let parts: Vec<&str> = version_str.split('.').collect();
|
||||
|
||||
let major: u8 = parts[0].parse()?;
|
||||
let minor: u8 = parts[1].parse()?;
|
||||
|
||||
let last_part = parts[2];
|
||||
let (patch_str, suffix) = if last_part.chars().any(|c| !c.is_ascii_digit()) {
|
||||
let idx = last_part.find(|c: char| !c.is_ascii_digit()).unwrap();
|
||||
(&last_part[..idx], Some(last_part[idx..].to_string()))
|
||||
} else {
|
||||
(last_part, None)
|
||||
};
|
||||
let patch: u8 = patch_str.parse()?;
|
||||
|
||||
Ok(Self::new(major, minor, patch, suffix))
|
||||
}
|
||||
major: u8,
|
||||
minor: u8,
|
||||
patch: u8,
|
||||
suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
|
@ -69,25 +36,6 @@ pub struct SysconfigPaths {
|
|||
stdlib: PathBuf,
|
||||
}
|
||||
|
||||
impl SysconfigPaths {
|
||||
pub fn from_executable(executable: &PathBuf) -> Result<Self, PythonError> {
|
||||
let output = Command::new(executable)
|
||||
.args([
|
||||
"-c",
|
||||
r#"
|
||||
import json
|
||||
import sysconfig
|
||||
paths = sysconfig.get_paths()
|
||||
print(json.dumps(paths))
|
||||
"#,
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
Ok(serde_json::from_str(&output_str)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SysconfigPaths {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "data: {}", self.data.display())?;
|
||||
|
@ -101,7 +49,7 @@ impl fmt::Display for SysconfigPaths {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Python {
|
||||
version_info: VersionInfo,
|
||||
sysconfig_paths: SysconfigPaths,
|
||||
|
@ -112,124 +60,30 @@ pub struct Python {
|
|||
packages: Packages,
|
||||
}
|
||||
|
||||
impl Python {
|
||||
fn new(
|
||||
version_info: VersionInfo,
|
||||
sysconfig_paths: SysconfigPaths,
|
||||
sys_prefix: PathBuf,
|
||||
sys_base_prefix: PathBuf,
|
||||
sys_executable: PathBuf,
|
||||
sys_path: Vec<PathBuf>,
|
||||
packages: Packages,
|
||||
) -> Self {
|
||||
Self {
|
||||
version_info,
|
||||
sysconfig_paths,
|
||||
sys_prefix,
|
||||
sys_base_prefix,
|
||||
sys_executable,
|
||||
sys_path,
|
||||
packages,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PythonSetup(Python);
|
||||
|
||||
impl ScriptRunner for PythonSetup {
|
||||
const SCRIPT: &'static str = scripts::PYTHON_SETUP;
|
||||
}
|
||||
|
||||
impl From<PythonSetup> for Python {
|
||||
fn from(setup: PythonSetup) -> Self {
|
||||
setup.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Python {
|
||||
pub fn initialize() -> Result<Self, PythonError> {
|
||||
let executable = which("python")?;
|
||||
let output = Command::new(&executable)
|
||||
.args([
|
||||
"-c",
|
||||
r#"
|
||||
import sys, json
|
||||
print(json.dumps({
|
||||
'prefix': sys.prefix,
|
||||
'base_prefix': sys.base_prefix,
|
||||
'executable': sys.executable,
|
||||
'path': [p for p in sys.path if p],
|
||||
}))
|
||||
"#,
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let sys_info: serde_json::Value = serde_json::from_str(&output_str)?;
|
||||
|
||||
Ok(Self::new(
|
||||
VersionInfo::from_executable(&executable)?,
|
||||
SysconfigPaths::from_executable(&executable)?,
|
||||
PathBuf::from(sys_info["prefix"].as_str().unwrap()),
|
||||
PathBuf::from(sys_info["base_prefix"].as_str().unwrap()),
|
||||
PathBuf::from(sys_info["executable"].as_str().unwrap()),
|
||||
sys_info["path"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| PathBuf::from(p.as_str().unwrap()))
|
||||
.collect(),
|
||||
Packages::from_executable(&executable)?,
|
||||
))
|
||||
Ok(PythonSetup::run_with_path(&executable)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version_info(&self) -> &VersionInfo {
|
||||
&self.version_info
|
||||
}
|
||||
|
||||
pub fn sysconfig_paths(&self) -> &SysconfigPaths {
|
||||
&self.sysconfig_paths
|
||||
}
|
||||
|
||||
pub fn sys_prefix(&self) -> &PathBuf {
|
||||
&self.sys_prefix
|
||||
}
|
||||
|
||||
pub fn sys_base_prefix(&self) -> &PathBuf {
|
||||
&self.sys_base_prefix
|
||||
}
|
||||
|
||||
pub fn sys_executable(&self) -> &PathBuf {
|
||||
impl Runner for Python {
|
||||
fn get_executable(&self) -> &Path {
|
||||
&self.sys_executable
|
||||
}
|
||||
|
||||
pub fn sys_path(&self) -> &Vec<PathBuf> {
|
||||
&self.sys_path
|
||||
}
|
||||
|
||||
pub fn packages(&self) -> &Packages {
|
||||
&self.packages
|
||||
}
|
||||
|
||||
pub fn run_python(&self, code: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.sys_executable())
|
||||
.args(["-c", code])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Python execution failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
pub fn run(&self, command: &str, args: &[&str]) -> std::io::Result<String> {
|
||||
let output = Command::new(self.sys_executable())
|
||||
.arg("-m")
|
||||
.arg(command)
|
||||
.args(args)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Command failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Python {
|
||||
|
@ -251,15 +105,12 @@ impl fmt::Display for Python {
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PythonError {
|
||||
#[error("Failed to locate Python executable: {0}")]
|
||||
PythonNotFound(#[from] which::Error),
|
||||
#[error("Python execution failed: {0}")]
|
||||
Execution(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
|
@ -268,4 +119,19 @@ pub enum PythonError {
|
|||
|
||||
#[error("Integer parsing error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
|
||||
#[error("Failed to locate Python executable: {0}")]
|
||||
PythonNotFound(#[from] which::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] Box<RunnerError>),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl From<RunnerError> for PythonError {
|
||||
fn from(err: RunnerError) -> Self {
|
||||
PythonError::Runner(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
|
179
crates/djls-python/src/runner.rs
Normal file
179
crates/djls-python/src/runner.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use crate::python::{Python, PythonError};
|
||||
use serde::ser::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub trait Runner {
|
||||
fn get_executable(&self) -> &Path;
|
||||
|
||||
fn run_module(&self, command: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.arg("-m")
|
||||
.arg(command)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Command failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_module_with_args(&self, command: &str, args: &[&str]) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.arg("-m")
|
||||
.arg(command)
|
||||
.args(args)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Command failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_python_code(&self, code: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.args(["-c", code])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Python execution failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_python_code_with_args(&self, code: &str, args: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.args(["-c", code, args])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Python execution failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_script<T>(&self, script: &str) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = self
|
||||
.run_python_code(script)
|
||||
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
serde_json::from_str(&result)
|
||||
}
|
||||
|
||||
fn run_script_with_args<T>(&self, script: &str, args: &str) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = self
|
||||
.run_python_code_with_args(script, args)
|
||||
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
serde_json::from_str(&result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SimpleRunner {
|
||||
executable: PathBuf,
|
||||
}
|
||||
|
||||
impl SimpleRunner {
|
||||
pub fn new(executable: PathBuf) -> Self {
|
||||
Self { executable }
|
||||
}
|
||||
}
|
||||
|
||||
impl Runner for SimpleRunner {
|
||||
fn get_executable(&self) -> &Path {
|
||||
&self.executable
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ScriptRunner: Sized {
|
||||
const SCRIPT: &'static str;
|
||||
|
||||
fn run_with_exe<R: Runner>(runner: &R) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = runner.run_script(Self::SCRIPT).map_err(RunnerError::from)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_with_exe_args<R: Runner>(runner: &R, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = runner
|
||||
.run_script_with_args(Self::SCRIPT, args)
|
||||
.map_err(RunnerError::from)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_with_path(executable: &Path) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let runner = &SimpleRunner::new(executable.to_path_buf());
|
||||
Self::run_with_exe(runner)
|
||||
}
|
||||
|
||||
fn run_with_path_args(executable: &Path, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let runner = &SimpleRunner::new(executable.to_path_buf());
|
||||
Self::run_with_exe_args(runner, args)
|
||||
}
|
||||
|
||||
fn run_with_py(python: &Python) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Self::run_with_exe(python)
|
||||
}
|
||||
|
||||
fn run_with_py_args(python: &Python, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Self::run_with_exe_args(python, args)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunnerError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] PythonError),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
14
crates/djls-python/src/scripts.rs
Normal file
14
crates/djls-python/src/scripts.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
#[macro_export]
|
||||
macro_rules! include_script {
|
||||
($name:expr) => {
|
||||
include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/python/djls/",
|
||||
$name,
|
||||
".py"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
pub const HAS_IMPORT: &str = include_script!("has_import");
|
||||
pub const PYTHON_SETUP: &str = include_script!["python_setup"];
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-django = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
@ -12,3 +13,4 @@ serde_json = { workspace = true }
|
|||
|
||||
tokio = { version = "1.42.0", features = ["full"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
lsp-types = "0.97.0"
|
||||
|
|
|
@ -1,57 +1,56 @@
|
|||
mod notifier;
|
||||
mod server;
|
||||
|
||||
use crate::notifier::TowerLspNotifier;
|
||||
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
||||
use anyhow::Result;
|
||||
use djls_django::DjangoProject;
|
||||
use tower_lsp::jsonrpc::Result as LspResult;
|
||||
use tower_lsp::lsp_types::*;
|
||||
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
||||
use tower_lsp::{LanguageServer, LspService, Server};
|
||||
|
||||
use djls_python::Python;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Backend {
|
||||
client: Client,
|
||||
python: Python,
|
||||
struct TowerLspBackend {
|
||||
server: DjangoLanguageServer,
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, _params: InitializeParams) -> LspResult<InitializeResult> {
|
||||
Ok(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::INCREMENTAL,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
offset_encoding: None,
|
||||
server_info: Some(ServerInfo {
|
||||
name: String::from("Django Language Server"),
|
||||
version: Some(String::from("0.1.0")),
|
||||
}),
|
||||
})
|
||||
impl LanguageServer for TowerLspBackend {
|
||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||
self.server
|
||||
.handle_request(LspRequest::Initialize(params))
|
||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
||||
}
|
||||
|
||||
async fn initialized(&self, _: InitializedParams) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, "server initialized!")
|
||||
.await;
|
||||
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("\n{}", self.python))
|
||||
.await;
|
||||
async fn initialized(&self, params: InitializedParams) {
|
||||
if self
|
||||
.server
|
||||
.handle_notification(LspNotification::Initialized(params))
|
||||
.is_err()
|
||||
{
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> LspResult<()> {
|
||||
Ok(())
|
||||
self.server
|
||||
.handle_notification(LspNotification::Shutdown)
|
||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let python = Python::initialize()?;
|
||||
let django = DjangoProject::setup()?;
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
||||
let (service, socket) = LspService::build(|client| Backend { client, python }).finish();
|
||||
let (service, socket) = LspService::build(|client| {
|
||||
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
|
||||
let server = DjangoLanguageServer::new(django, notifier);
|
||||
TowerLspBackend { server }
|
||||
})
|
||||
.finish();
|
||||
|
||||
Server::new(stdin, stdout, socket).serve(service).await;
|
||||
|
||||
|
|
59
crates/djls/src/notifier.rs
Normal file
59
crates/djls/src/notifier.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use anyhow::Result;
|
||||
use tower_lsp::async_trait;
|
||||
use tower_lsp::lsp_types::MessageActionItem;
|
||||
use tower_lsp::lsp_types::MessageType;
|
||||
use tower_lsp::Client;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Notifier: Send + Sync {
|
||||
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()>;
|
||||
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()>;
|
||||
async fn show_message_request(
|
||||
&self,
|
||||
typ: MessageType,
|
||||
msg: &str,
|
||||
actions: Option<Vec<MessageActionItem>>,
|
||||
) -> Result<Option<MessageActionItem>>;
|
||||
}
|
||||
|
||||
pub struct TowerLspNotifier {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl TowerLspNotifier {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Notifier for TowerLspNotifier {
|
||||
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
tokio::spawn(async move {
|
||||
client.log_message(typ, msg).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
tokio::spawn(async move {
|
||||
client.show_message(typ, msg).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_message_request(
|
||||
&self,
|
||||
typ: MessageType,
|
||||
msg: &str,
|
||||
actions: Option<Vec<MessageActionItem>>,
|
||||
) -> Result<Option<MessageActionItem>> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
Ok(client.show_message_request(typ, msg, actions).await?)
|
||||
}
|
||||
}
|
60
crates/djls/src/server.rs
Normal file
60
crates/djls/src/server.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use crate::notifier::Notifier;
|
||||
use anyhow::Result;
|
||||
use djls_django::DjangoProject;
|
||||
use tower_lsp::lsp_types::*;
|
||||
|
||||
const SERVER_NAME: &str = "Django Language Server";
|
||||
const SERVER_VERSION: &str = "0.1.0";
|
||||
|
||||
pub enum LspRequest {
|
||||
Initialize(InitializeParams),
|
||||
}
|
||||
|
||||
pub enum LspNotification {
|
||||
Initialized(InitializedParams),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct DjangoLanguageServer {
|
||||
django: DjangoProject,
|
||||
notifier: Box<dyn Notifier>,
|
||||
}
|
||||
|
||||
impl DjangoLanguageServer {
|
||||
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
|
||||
Self { django, notifier }
|
||||
}
|
||||
|
||||
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
|
||||
match request {
|
||||
LspRequest::Initialize(_params) => Ok(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::INCREMENTAL,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
offset_encoding: None,
|
||||
server_info: Some(ServerInfo {
|
||||
name: SERVER_NAME.to_string(),
|
||||
version: Some(SERVER_VERSION.to_string()),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_notification(&self, notification: LspNotification) -> Result<()> {
|
||||
match notification {
|
||||
LspNotification::Initialized(_) => {
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, "server initialized!")?;
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, &format!("\n{}", self.django.py()))?;
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, &format!("\n{}", self.django))?;
|
||||
Ok(())
|
||||
}
|
||||
LspNotification::Shutdown => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue