mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-26 20:09:29 +00:00
add initial autocomplete for installed templatetags (#46)
This commit is contained in:
parent
5eb8a775e4
commit
c16635b1c0
8 changed files with 359 additions and 58 deletions
|
@ -4,6 +4,7 @@ resolver = "2"
|
|||
|
||||
[workspace.dependencies]
|
||||
djls = { path = "crates/djls" }
|
||||
djls-project = { path = "crates/djls-project" }
|
||||
djls-server = { path = "crates/djls-server" }
|
||||
djls-template-ast = { path = "crates/djls-template-ast" }
|
||||
djls-worker = { path = "crates/djls-worker" }
|
||||
|
@ -16,6 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.42", features = ["full"] }
|
||||
tower-lsp = { version = "0.20", features = ["proposed"] }
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
|
|
8
crates/djls-project/Cargo.toml
Normal file
8
crates/djls-project/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "djls-project"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
pyo3 = { workspace = true }
|
||||
tower-lsp = { workspace = true }
|
62
crates/djls-project/src/lib.rs
Normal file
62
crates/djls-project/src/lib.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
mod templatetags;
|
||||
|
||||
pub use templatetags::TemplateTags;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tower_lsp::lsp_types::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DjangoProject {
|
||||
path: PathBuf,
|
||||
template_tags: Option<TemplateTags>,
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
template_tags: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_initialize_params(params: &InitializeParams) -> Option<Self> {
|
||||
// Try current directory first
|
||||
let path = std::env::current_dir()
|
||||
.ok()
|
||||
// Fall back to workspace root if provided
|
||||
.or_else(|| {
|
||||
params
|
||||
.root_uri
|
||||
.as_ref()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
});
|
||||
|
||||
path.map(Self::new)
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
// Add project to Python path
|
||||
let sys = py.import("sys")?;
|
||||
let py_path = sys.getattr("path")?;
|
||||
py_path.call_method1("append", (self.path.to_str().unwrap(),))?;
|
||||
|
||||
// Setup Django
|
||||
let django = py.import("django")?;
|
||||
django.call_method0("setup")?;
|
||||
|
||||
self.template_tags = Some(TemplateTags::from_python(py)?);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn template_tags(&self) -> Option<&TemplateTags> {
|
||||
self.template_tags.as_ref()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
94
crates/djls-project/src/templatetags.rs
Normal file
94
crates/djls-project/src/templatetags.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyList};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TemplateTags(Vec<TemplateTag>);
|
||||
|
||||
impl Deref for TemplateTags {
|
||||
type Target = Vec<TemplateTag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateTags {
|
||||
fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
fn process_library(
|
||||
module_name: &str,
|
||||
library: &Bound<'_, PyAny>,
|
||||
tags: &mut Vec<TemplateTag>,
|
||||
) -> PyResult<()> {
|
||||
let tags_dict = library.getattr("tags")?;
|
||||
let dict = tags_dict.downcast::<PyDict>()?;
|
||||
|
||||
for (key, value) in dict.iter() {
|
||||
let tag_name = key.extract::<String>()?;
|
||||
let doc = value.getattr("__doc__")?.extract().ok();
|
||||
|
||||
let library_name = if module_name.is_empty() {
|
||||
"builtins".to_string()
|
||||
} else {
|
||||
module_name.split('.').last().unwrap_or("").to_string()
|
||||
};
|
||||
|
||||
tags.push(TemplateTag::new(tag_name, library_name, doc));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_python(py: Python) -> PyResult<TemplateTags> {
|
||||
let mut template_tags = TemplateTags::new();
|
||||
|
||||
let engine = py
|
||||
.import("django.template.engine")?
|
||||
.getattr("Engine")?
|
||||
.call_method0("get_default")?;
|
||||
|
||||
// Built-in template tags
|
||||
let builtins_attr = engine.getattr("template_builtins")?;
|
||||
let builtins = builtins_attr.downcast::<PyList>()?;
|
||||
for builtin in builtins {
|
||||
Self::process_library("", &builtin, &mut template_tags.0)?;
|
||||
}
|
||||
|
||||
// Custom template libraries
|
||||
let libraries_attr = engine.getattr("template_libraries")?;
|
||||
let libraries = libraries_attr.downcast::<PyDict>()?;
|
||||
for (module_name, library) in libraries.iter() {
|
||||
let module_name = module_name.extract::<String>()?;
|
||||
Self::process_library(&module_name, &library, &mut template_tags.0)?;
|
||||
}
|
||||
|
||||
Ok(template_tags)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TemplateTag {
|
||||
name: String,
|
||||
library: String,
|
||||
doc: Option<String>,
|
||||
}
|
||||
|
||||
impl TemplateTag {
|
||||
fn new(name: String, library: String, doc: Option<String>) -> Self {
|
||||
Self { name, library, doc }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn library(&self) -> &String {
|
||||
&self.library
|
||||
}
|
||||
|
||||
pub fn doc(&self) -> &Option<String> {
|
||||
&self.doc
|
||||
}
|
||||
}
|
|
@ -4,13 +4,13 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-project = { workspace = true }
|
||||
djls-template-ast = { workspace = true }
|
||||
djls-worker = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
pyo3 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
tower-lsp = { version = "0.20", features = ["proposed"] }
|
||||
lsp-types = "0.97"
|
||||
tower-lsp = { workspace = true }
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use djls_project::TemplateTags;
|
||||
use std::collections::HashMap;
|
||||
use tower_lsp::lsp_types::{
|
||||
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Position,
|
||||
Range,
|
||||
CompletionItem, CompletionItemKind, CompletionResponse, DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams, DidOpenTextDocumentParams, Documentation, InsertTextFormat,
|
||||
MarkupContent, MarkupKind, Position, Range,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -102,6 +104,56 @@ impl Store {
|
|||
pub fn is_version_valid(&self, uri: &str, version: i32) -> bool {
|
||||
self.get_version(uri).map_or(false, |v| v == version)
|
||||
}
|
||||
|
||||
pub fn get_completions(
|
||||
&self,
|
||||
uri: &str,
|
||||
position: Position,
|
||||
tags: &TemplateTags,
|
||||
) -> Option<CompletionResponse> {
|
||||
let document = self.get_document(uri)?;
|
||||
|
||||
if document.language_id != LanguageId::HtmlDjango {
|
||||
return None;
|
||||
}
|
||||
|
||||
let context = document.get_template_tag_context(position)?;
|
||||
|
||||
let mut completions: Vec<CompletionItem> = tags
|
||||
.iter()
|
||||
.filter(|tag| {
|
||||
context.partial_tag.is_empty() || tag.name().starts_with(&context.partial_tag)
|
||||
})
|
||||
.map(|tag| {
|
||||
let leading_space = if context.needs_leading_space { " " } else { "" };
|
||||
CompletionItem {
|
||||
label: tag.name().to_string(),
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
detail: Some(format!("Template tag from {}", tag.library())),
|
||||
documentation: tag.doc().as_ref().map(|doc| {
|
||||
Documentation::MarkupContent(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: doc.to_string(),
|
||||
})
|
||||
}),
|
||||
insert_text: Some(match context.closing_brace {
|
||||
ClosingBrace::None => format!("{}{} %}}", leading_space, tag.name()),
|
||||
ClosingBrace::PartialClose => format!("{}{} %", leading_space, tag.name()),
|
||||
ClosingBrace::FullClose => format!("{}{} ", leading_space, tag.name()),
|
||||
}),
|
||||
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
completions.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
Some(CompletionResponse::Array(completions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -181,6 +233,32 @@ impl TextDocument {
|
|||
pub fn line_count(&self) -> usize {
|
||||
self.index.line_starts.len()
|
||||
}
|
||||
|
||||
pub fn get_template_tag_context(&self, position: Position) -> Option<TemplateTagContext> {
|
||||
let line = self.get_line(position.line.try_into().ok()?)?;
|
||||
let prefix = &line[..position.character.try_into().ok()?];
|
||||
let rest_of_line = &line[position.character.try_into().ok()?..];
|
||||
let rest_trimmed = rest_of_line.trim_start();
|
||||
|
||||
prefix.rfind("{%").map(|tag_start| {
|
||||
// Check if we're immediately after {% with no space
|
||||
let needs_leading_space = prefix.ends_with("{%");
|
||||
|
||||
let closing_brace = if rest_trimmed.starts_with("%}") {
|
||||
ClosingBrace::FullClose
|
||||
} else if rest_trimmed.starts_with("}") {
|
||||
ClosingBrace::PartialClose
|
||||
} else {
|
||||
ClosingBrace::None
|
||||
};
|
||||
|
||||
TemplateTagContext {
|
||||
partial_tag: prefix[tag_start + 2..].trim().to_string(),
|
||||
closing_brace,
|
||||
needs_leading_space,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -248,3 +326,17 @@ impl From<String> for LanguageId {
|
|||
Self::from(language_id.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClosingBrace {
|
||||
None,
|
||||
PartialClose, // just }
|
||||
FullClose, // %}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TemplateTagContext {
|
||||
pub partial_tag: String,
|
||||
pub closing_brace: ClosingBrace,
|
||||
pub needs_leading_space: bool,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ mod tasks;
|
|||
use crate::notifier::TowerLspNotifier;
|
||||
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
||||
use anyhow::Result;
|
||||
use server::LspResponse;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_lsp::jsonrpc::Result as LspResult;
|
||||
|
@ -19,11 +20,16 @@ struct TowerLspBackend {
|
|||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for TowerLspBackend {
|
||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||
self.server
|
||||
.read()
|
||||
match self
|
||||
.server
|
||||
.write()
|
||||
.await
|
||||
.handle_request(LspRequest::Initialize(params))
|
||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())?
|
||||
{
|
||||
LspResponse::Initialize(result) => Ok(result),
|
||||
_ => Err(tower_lsp::jsonrpc::Error::internal_error()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialized(&self, params: InitializedParams) {
|
||||
|
@ -77,6 +83,19 @@ impl LanguageServer for TowerLspBackend {
|
|||
eprintln!("Error handling document close: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
|
||||
match self
|
||||
.server
|
||||
.write()
|
||||
.await
|
||||
.handle_request(LspRequest::Completion(params))
|
||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())?
|
||||
{
|
||||
LspResponse::Completion(result) => Ok(result),
|
||||
_ => Err(tower_lsp::jsonrpc::Error::internal_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve() -> Result<()> {
|
||||
|
|
|
@ -2,7 +2,10 @@ use crate::documents::Store;
|
|||
use crate::notifier::Notifier;
|
||||
use crate::tasks::DebugTask;
|
||||
use anyhow::Result;
|
||||
use djls_project::DjangoProject;
|
||||
use djls_worker::Worker;
|
||||
use pyo3::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tower_lsp::lsp_types::*;
|
||||
|
@ -12,6 +15,12 @@ const SERVER_VERSION: &str = "0.1.0";
|
|||
|
||||
pub enum LspRequest {
|
||||
Initialize(InitializeParams),
|
||||
Completion(CompletionParams),
|
||||
}
|
||||
|
||||
pub enum LspResponse {
|
||||
Initialize(InitializeResult),
|
||||
Completion(Option<CompletionResponse>),
|
||||
}
|
||||
|
||||
pub enum LspNotification {
|
||||
|
@ -23,6 +32,7 @@ pub enum LspNotification {
|
|||
}
|
||||
|
||||
pub struct DjangoLanguageServer {
|
||||
project: Option<DjangoProject>,
|
||||
notifier: Arc<Box<dyn Notifier>>,
|
||||
documents: Store,
|
||||
worker: Worker,
|
||||
|
@ -33,33 +43,77 @@ impl DjangoLanguageServer {
|
|||
let notifier = Arc::new(notifier);
|
||||
|
||||
Self {
|
||||
project: None,
|
||||
notifier,
|
||||
documents: Store::new(),
|
||||
worker: Worker::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
|
||||
pub fn handle_request(&mut self, request: LspRequest) -> Result<LspResponse> {
|
||||
match request {
|
||||
LspRequest::Initialize(_params) => Ok(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
change: Some(TextDocumentSyncKind::INCREMENTAL),
|
||||
will_save: Some(false),
|
||||
will_save_wait_until: Some(false),
|
||||
save: Some(SaveOptions::default().into()),
|
||||
},
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
offset_encoding: None,
|
||||
server_info: Some(ServerInfo {
|
||||
name: SERVER_NAME.to_string(),
|
||||
version: Some(SERVER_VERSION.to_string()),
|
||||
}),
|
||||
}),
|
||||
LspRequest::Initialize(params) => {
|
||||
if let Some(mut project) = DjangoProject::from_initialize_params(¶ms) {
|
||||
if let Err(e) = project.initialize() {
|
||||
self.notifier.log_message(
|
||||
MessageType::ERROR,
|
||||
&format!("Failed to initialize Django project: {}", e),
|
||||
)?;
|
||||
} else {
|
||||
self.notifier.log_message(
|
||||
MessageType::INFO,
|
||||
&format!("Using project path: {}", project.path().display()),
|
||||
)?;
|
||||
self.project = Some(project);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LspResponse::Initialize(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
completion_provider: Some(CompletionOptions {
|
||||
resolve_provider: Some(false),
|
||||
trigger_characters: Some(vec![
|
||||
"{".to_string(),
|
||||
"%".to_string(),
|
||||
" ".to_string(),
|
||||
]),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
change: Some(TextDocumentSyncKind::INCREMENTAL),
|
||||
will_save: Some(false),
|
||||
will_save_wait_until: Some(false),
|
||||
save: Some(SaveOptions::default().into()),
|
||||
},
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
offset_encoding: None,
|
||||
server_info: Some(ServerInfo {
|
||||
name: SERVER_NAME.to_string(),
|
||||
version: Some(SERVER_VERSION.to_string()),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
LspRequest::Completion(params) => {
|
||||
let completions = if let Some(project) = &self.project {
|
||||
if let Some(tags) = project.template_tags() {
|
||||
self.documents.get_completions(
|
||||
params.text_document_position.text_document.uri.as_str(),
|
||||
params.text_document_position.position,
|
||||
tags,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(LspResponse::Completion(completions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,36 +125,6 @@ impl DjangoLanguageServer {
|
|||
MessageType::INFO,
|
||||
&format!("Opened document: {}", params.text_document.uri),
|
||||
)?;
|
||||
|
||||
// Execute - still sync
|
||||
self.worker.execute(DebugTask::new(
|
||||
"Quick task".to_string(),
|
||||
Duration::from_millis(100),
|
||||
self.notifier.clone(),
|
||||
))?;
|
||||
|
||||
// Submit - spawn async task
|
||||
let worker = self.worker.clone();
|
||||
let task = DebugTask::new(
|
||||
"Important task".to_string(),
|
||||
Duration::from_secs(1),
|
||||
self.notifier.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let _ = worker.submit(task).await;
|
||||
});
|
||||
|
||||
// Wait for result - spawn async task
|
||||
let worker = self.worker.clone();
|
||||
let task = DebugTask::new(
|
||||
"Task with result".to_string(),
|
||||
Duration::from_secs(2),
|
||||
self.notifier.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let _ = worker.wait_for(task).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
LspNotification::DidChangeTextDocument(params) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue