Skip to content
2 changes: 1 addition & 1 deletion crates/glua_code_analysis/resources/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@
"properties": {
"enable": {
"default": true,
"description": "Enable parsing strings for color tags and showing a color picker next to them.",
"description": "Enable color previews and color picker support for strings, GMod color calls, color tuples, and color variables.",
"type": "boolean",
"x-vscode-setting": true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EmmyrcDocumentColor {
/// Enable parsing strings for color tags and showing a color picker next to them.
/// Enable color previews and color picker support for strings, GMod color calls, color tuples, and color variables.
#[serde(default = "default_true")]
#[schemars(extend("x-vscode-setting" = true))]
pub enable: bool,
Expand Down
15 changes: 15 additions & 0 deletions crates/glua_ls/src/context/lsp_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ impl LspFeatures {
false
}

pub fn supports_completion_item_deprecated_tags(&self) -> bool {
self.client_capabilities
.text_document
.as_ref()
.and_then(|text_document| text_document.completion.as_ref())
.and_then(|completion| completion.completion_item.as_ref())
.and_then(|completion_item| completion_item.tag_support.as_ref())
.is_some_and(|tag_support| {
tag_support.value_set.is_empty()
|| tag_support
.value_set
.contains(&lsp_types::CompletionItemTag::DEPRECATED)
})
}

pub fn supports_workspace_diagnostic(&self) -> bool {
self.supports_pull_diagnostic()
}
Expand Down
111 changes: 109 additions & 2 deletions crates/glua_ls/src/handlers/code_actions/actions/build_fix_code.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::HashMap;

use crate::handlers::command::make_auto_doc_tag_command;
use glua_code_analysis::SemanticModel;
use glua_parser::{LuaAstNode, LuaExpr};
use glua_code_analysis::{LuaDocument, SemanticModel};
use glua_parser::{BinaryOperator, LuaAstNode, LuaBinaryExpr, LuaExpr};
use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, Range, TextEdit, WorkspaceEdit};
use rowan::{NodeOrToken, TokenAtOffset};

Expand Down Expand Up @@ -76,6 +76,113 @@ pub fn build_add_doc_tag(
Some(())
}

pub fn build_gmod_null_check(
semantic_model: &SemanticModel,
actions: &mut Vec<CodeActionOrCommand>,
range: Range,
_data: &Option<serde_json::Value>,
) -> Option<()> {
let document = semantic_model.get_document();
let target_range = document.to_rowan_range(range)?;
let root = semantic_model.get_root();
let token = match root.syntax().token_at_offset(target_range.start()) {
TokenAtOffset::Single(token) => token,
TokenAtOffset::Between(_, token) => token,
_ => return None,
};

let mut current_node = token.parent();
while let Some(node) = current_node {
if node.text_range() == target_range {
if let Some(binary_expr) = LuaBinaryExpr::cast(node.clone()) {
if let Some(replacement) =
build_gmod_null_binary_replacement(semantic_model, &document, &binary_expr)
{
push_gmod_null_check_action(actions, &document, range, replacement);
}
return Some(());
}

if LuaExpr::can_cast(node.kind().into()) {
let expr_text = document.get_text_slice(target_range).trim();
if !expr_text.is_empty() {
push_gmod_null_check_action(
actions,
&document,
range,
format!("IsValid({expr_text})"),
);
}
return Some(());
}
}

current_node = node.parent();
}

Some(())
}

fn build_gmod_null_binary_replacement(
semantic_model: &SemanticModel,
document: &LuaDocument,
binary_expr: &LuaBinaryExpr,
) -> Option<String> {
let op = binary_expr.get_op_token()?.get_op();
if !matches!(op, BinaryOperator::OpEq | BinaryOperator::OpNe) {
return None;
}

let (left, right) = binary_expr.get_exprs()?;
let checked_expr = match (
is_nil_expr(semantic_model, &left),
is_nil_expr(semantic_model, &right),
) {
(false, true) => left,
(true, false) => right,
_ => return None,
};

let expr_text = document
.get_text_slice(checked_expr.syntax().text_range())
.trim();
if expr_text.is_empty() {
return None;
}

let is_valid_call = format!("IsValid({expr_text})");
match op {
BinaryOperator::OpEq => Some(format!("not {is_valid_call}")),
BinaryOperator::OpNe => Some(is_valid_call),
_ => None,
}
}

fn is_nil_expr(semantic_model: &SemanticModel, expr: &LuaExpr) -> bool {
semantic_model
.infer_expr(expr.clone())
.is_ok_and(|expr_type| expr_type.is_nil())
}

fn push_gmod_null_check_action(
actions: &mut Vec<CodeActionOrCommand>,
document: &LuaDocument,
range: Range,
new_text: String,
) {
let text_edit = TextEdit { range, new_text };

actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: t!("Use IsValid(...) for GMod NULL check").to_string(),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit {
changes: Some(HashMap::from([(document.get_uri(), vec![text_edit])])),
..Default::default()
}),
..Default::default()
}));
}

pub fn build_preferred_local_alias_fix(
semantic_model: &SemanticModel,
actions: &mut Vec<CodeActionOrCommand>,
Expand Down
5 changes: 4 additions & 1 deletion crates/glua_ls/src/handlers/code_actions/build_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use lsp_types::{

use super::actions::{
build_add_doc_tag, build_disable_file_changes, build_disable_next_line_changes,
build_need_check_nil, build_preferred_local_alias_fix,
build_gmod_null_check, build_need_check_nil, build_preferred_local_alias_fix,
};
use crate::handlers::command::{DisableAction, make_disable_code_command};

Expand Down Expand Up @@ -72,6 +72,9 @@ fn add_fix_code_action(
DiagnosticCode::PreferredLocalAlias => {
build_preferred_local_alias_fix(semantic_model, actions, range, data)
}
DiagnosticCode::GmodNullCheck => {
build_gmod_null_check(semantic_model, actions, range, data)
}
_ => Some(()),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use glua_code_analysis::{DbIndex, LuaDeclId, LuaSemanticDeclId, LuaType};
use glua_parser::{LuaAstNode, LuaExpr, LuaSyntaxKind};
use lsp_types::CompletionItem;

use crate::handlers::completion::{
add_completions::get_function_snippet, completion_builder::CompletionBuilder,
completion_data::CompletionData,
add_completions::get_function_snippet,
completion_builder::CompletionBuilder,
completion_data::{CompletionColorInfo, CompletionData},
};

use super::{
CallDisplay, check_visibility, get_completion_kind, get_description, get_detail, is_deprecated,
CallDisplay, check_visibility, color_label_detail,
completion_item_info::{
color_info_from_expr, color_info_from_type, gmod_constructor_literal_detail, is_color_type,
is_gmod_literal_constructor_type, scalar_literal_description, scalar_literal_detail,
},
get_completion_tags, get_decl_completion_kind, get_description, get_detail, is_deprecated,
};

pub fn add_decl_completion(
Expand All @@ -20,20 +27,52 @@ pub fn add_decl_completion(
check_visibility(builder, property_owner.clone())?;

let overload_count = count_function_overloads(builder.semantic_model.get_db(), typ);
let (color, constructor_literal_detail) =
get_decl_completion_literal_info(builder, decl_id, typ);
let completion_data_color = builder
.semantic_model
.get_emmyrc()
.document_color
.enable
.then(|| color.clone())
.flatten();
let literal_detail = scalar_literal_detail(typ);

let mut completion_item = CompletionItem {
label: name.to_string(),
kind: Some(get_completion_kind(typ)),
data: CompletionData::from_property_owner_id(builder, decl_id.into(), overload_count),
kind: Some(if color.is_some() {
lsp_types::CompletionItemKind::COLOR
} else {
get_decl_completion_kind(builder, decl_id, typ)
}),
data: match completion_data_color {
Some(color) => CompletionData::from_property_owner_id_with_color(
builder,
decl_id.into(),
overload_count,
color,
),
None => CompletionData::from_property_owner_id(builder, decl_id.into(), overload_count),
},
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail: get_detail(builder, typ, CallDisplay::None),
description: get_description(builder, typ),
detail: color
.as_ref()
.map(color_label_detail)
.or_else(|| get_detail(builder, typ, CallDisplay::None))
.or(constructor_literal_detail)
.or(literal_detail),
description: if color.is_some() {
Some("Color".to_string())
} else {
scalar_literal_description(typ).or_else(|| get_description(builder, typ))
},
}),
..Default::default()
};

if is_deprecated(builder, property_owner.clone()) {
let deprecated = is_deprecated(builder, property_owner.clone());
if deprecated {
completion_item.deprecated = Some(true);
completion_item.tags = get_completion_tags(builder, Some(true));
}

if builder.support_snippets(typ) {
Expand Down Expand Up @@ -66,3 +105,65 @@ fn count_function_overloads(db: &DbIndex, typ: &LuaType) -> Option<usize> {
}
if count == 0 { None } else { Some(count) }
}

fn get_decl_completion_literal_info(
builder: &CompletionBuilder,
decl_id: LuaDeclId,
typ: &LuaType,
) -> (Option<CompletionColorInfo>, Option<String>) {
let mut color = color_info_from_type(typ);
let should_inspect_color =
color.is_none() && (is_color_type(typ) || matches!(typ, LuaType::Unknown));
let should_inspect_constructor =
is_gmod_literal_constructor_type(typ) || matches!(typ, LuaType::Unknown);
if !should_inspect_color && !should_inspect_constructor {
return (color, None);
}

let value_expr = get_decl_value_expr(builder, decl_id);
if should_inspect_color {
color = value_expr.as_ref().and_then(color_info_from_expr);
}

let constructor_literal_detail = if color.is_none() && should_inspect_constructor {
value_expr
.as_ref()
.and_then(gmod_constructor_literal_detail)
} else {
None
};

(color, constructor_literal_detail)
}

fn get_decl_value_expr(builder: &CompletionBuilder, decl_id: LuaDeclId) -> Option<LuaExpr> {
let decl = builder
.semantic_model
.get_db()
.get_decl_index()
.get_decl(&decl_id)?;
let value_syntax_id = decl.get_value_syntax_id()?;
if !can_expr_syntax_have_completion_literal(value_syntax_id.get_kind()) {
return None;
}
let tree = builder
.semantic_model
.get_db()
.get_vfs()
.get_syntax_tree(&decl_id.file_id)?;
let value_node = value_syntax_id.to_node_from_root(&tree.get_red_root())?;
LuaExpr::cast(value_node)
}

fn can_expr_syntax_have_completion_literal(kind: LuaSyntaxKind) -> bool {
matches!(
kind,
LuaSyntaxKind::CallExpr
| LuaSyntaxKind::LiteralExpr
| LuaSyntaxKind::RequireCallExpr
| LuaSyntaxKind::AssertCallExpr
| LuaSyntaxKind::ErrorCallExpr
| LuaSyntaxKind::TypeCallExpr
| LuaSyntaxKind::SetmetatableCallExpr
)
}
Loading
Loading