diff --git a/src/analyzer/data.rs b/src/analyzer/data.rs index 8ac29ac..dd11844 100644 --- a/src/analyzer/data.rs +++ b/src/analyzer/data.rs @@ -224,7 +224,12 @@ impl<'p> AnalyzedBlock<'p> { let assignment = assignment.as_variable_assignment(self.document); variables .entry(assignment.primary_variable.as_str()) - .or_insert_with(|| Variable::new(!declare_args_stack.is_empty())) + .or_insert_with(|| { + Variable::new( + assignment.primary_variable.as_str(), + !declare_args_stack.is_empty(), + ) + }) .assignments .push(assignment); } @@ -232,7 +237,12 @@ impl<'p> AnalyzedBlock<'p> { let assignment = foreach.as_variable_assignment(self.document); variables .entry(assignment.primary_variable.as_str()) - .or_insert_with(|| Variable::new(!declare_args_stack.is_empty())) + .or_insert_with(|| { + Variable::new( + assignment.primary_variable.as_str(), + !declare_args_stack.is_empty(), + ) + }) .assignments .push(assignment); } @@ -240,7 +250,12 @@ impl<'p> AnalyzedBlock<'p> { for assignment in forward_variables_from.as_variable_assignment(self.document) { variables .entry(assignment.primary_variable.as_str()) - .or_insert_with(|| Variable::new(!declare_args_stack.is_empty())) + .or_insert_with(|| { + Variable::new( + assignment.primary_variable.as_str(), + !declare_args_stack.is_empty(), + ) + }) .assignments .push(assignment); } @@ -526,13 +541,15 @@ impl<'p> AnalyzedTemplate<'p> { #[derive(Clone, Debug)] pub struct Variable<'p> { + pub name: &'p str, pub assignments: Vec>, pub is_args: bool, } -impl Variable<'_> { - pub fn new(is_args: bool) -> Self { +impl<'p> Variable<'p> { + pub fn new(name: &'p str, is_args: bool) -> Self { Self { + name, assignments: Vec::new(), is_args, } diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index c1c1bc8..a09431a 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -586,7 +586,9 @@ impl WorkspaceAnalyzer { exports .variables .entry(identifier.name) - .or_insert_with(|| Variable::new(!declare_args_stack.is_empty())) + .or_insert_with(|| { + Variable::new(identifier.name, !declare_args_stack.is_empty()) + }) .assignments .push(VariableAssignment { document, @@ -645,7 +647,7 @@ impl WorkspaceAnalyzer { .variables .entry(name) .or_insert_with(|| { - Variable::new(!declare_args_stack.is_empty()) + Variable::new(name, !declare_args_stack.is_empty()) }) .assignments .push(VariableAssignment { diff --git a/src/common/config.rs b/src/common/config.rs index 984dab4..a206e19 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -60,4 +60,5 @@ pub struct ExperimentalConfigurations { pub workspace_symbols: bool, pub target_lens: bool, pub parallel_indexing: bool, + pub workspace_completion: bool, } diff --git a/src/server/imports.rs b/src/server/imports.rs index 7d59f6d..f819283 100644 --- a/src/server/imports.rs +++ b/src/server/imports.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashMap; - -use tower_lsp::lsp_types::{Range, TextEdit, Url, WorkspaceEdit}; +use tower_lsp::lsp_types::{Range, TextEdit}; use crate::analyzer::{AnalyzedFile, AnalyzedStatement}; @@ -25,7 +23,7 @@ fn get_import<'p>(statement: &AnalyzedStatement<'p>) -> Option<&'p str> { } } -pub fn create_import_edit(current_file: &AnalyzedFile, import: &str) -> WorkspaceEdit { +pub fn create_import_edit(current_file: &AnalyzedFile, import: &str) -> TextEdit { // Find the first top-level import block. let first_import_block: Vec<_> = current_file .analyzed_root @@ -54,14 +52,8 @@ pub fn create_import_edit(current_file: &AnalyzedFile, import: &str) -> Workspac let insert_pos = current_file.document.line_index.position(insert_offset); - WorkspaceEdit { - changes: Some(HashMap::from([( - Url::from_file_path(¤t_file.document.path).unwrap(), - vec![TextEdit { - range: Range::new(insert_pos, insert_pos), - new_text: format!("{prefix}import(\"{import}\"){suffix}"), - }], - )])), - ..Default::default() + TextEdit { + range: Range::new(insert_pos, insert_pos), + new_text: format!("{prefix}import(\"{import}\"){suffix}"), } } diff --git a/src/server/providers/code_action.rs b/src/server/providers/code_action.rs index 0cbd621..6f14d86 100644 --- a/src/server/providers/code_action.rs +++ b/src/server/providers/code_action.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::Path; +use std::{collections::HashMap, path::Path}; use itertools::Itertools; use tower_lsp::lsp_types::{ CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command, - Diagnostic, NumberOrString, SymbolKind, WorkspaceEdit, + Diagnostic, NumberOrString, SymbolKind, Url, WorkspaceEdit, }; use crate::{ @@ -75,7 +75,13 @@ async fn compute_import_actions( title: format!("Import `{name}` from `{only_import}`"), kind: Some(CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), - edit: Some(create_import_edit(¤t_file, only_import)), + edit: Some(WorkspaceEdit { + changes: Some(HashMap::from([( + Url::from_file_path(¤t_file.document.path).unwrap(), + vec![create_import_edit(¤t_file, only_import)], + )])), + ..Default::default() + }), command: None, is_preferred: Some(true), ..Default::default() @@ -87,7 +93,13 @@ async fn compute_import_actions( .iter() .map(|import| ImportCandidate { import: import.clone(), - edit: create_import_edit(¤t_file, import), + edit: WorkspaceEdit { + changes: Some(HashMap::from([( + Url::from_file_path(¤t_file.document.path).unwrap(), + vec![create_import_edit(¤t_file, import)], + )])), + ..Default::default() + }, }) .collect(), }; diff --git a/src/server/providers/completion.rs b/src/server/providers/completion.rs index d14ad8f..7a5cfd1 100644 --- a/src/server/providers/completion.rs +++ b/src/server/providers/completion.rs @@ -12,19 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{path::Path, sync::Arc}; +use std::{collections::HashSet, path::Path, sync::Arc}; use itertools::Itertools; use tower_lsp::lsp_types::{ - Command, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, - Documentation, MarkupContent, MarkupKind, + Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, + CompletionResponse, Documentation, MarkupContent, MarkupKind, }; use crate::{ - analyzer::AnalyzedFile, - common::{builtins::BUILTINS, error::Result}, + analyzer::{AnalyzedFile, Template, Variable, WorkspaceAnalyzer}, + common::{builtins::BUILTINS, error::Result, utils::format_path}, parser::{Block, Node}, - server::{providers::utils::get_text_document_path, RequestContext}, + server::{ + imports::create_import_edit, providers::utils::get_text_document_path, symbols::SymbolSet, + RequestContext, + }, }; fn get_prefix_string_for_completion<'i>(ast: &Block<'i>, offset: usize) -> Option<&'i str> { @@ -84,10 +87,64 @@ fn is_after_dot(data: &str, offset: usize) -> bool { false } -fn build_identifier_completions( +impl Variable<'_> { + fn as_completion_item(&self, current_file: &AnalyzedFile, need_import: bool) -> CompletionItem { + let first_assignment = self.assignments.first().unwrap(); + let import_path = format_path( + &first_assignment.document.path, + ¤t_file.workspace_root, + ); + let additional_text_edits = if need_import { + Some(vec![create_import_edit(current_file, &import_path)]) + } else { + None + }; + CompletionItem { + label: self.name.to_string(), + kind: Some(CompletionItemKind::VARIABLE), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: self.format_help(¤t_file.workspace_root).join("\n\n"), + })), + label_details: Some(CompletionItemLabelDetails { + detail: None, + description: Some(import_path), + }), + additional_text_edits, + ..Default::default() + } + } +} + +impl Template<'_> { + fn as_completion_item(&self, current_file: &AnalyzedFile, need_import: bool) -> CompletionItem { + let additional_text_edits = if need_import { + Some(vec![create_import_edit( + current_file, + &format_path(&self.document.path, ¤t_file.workspace_root), + )]) + } else { + None + }; + CompletionItem { + label: self.name.to_string(), + kind: Some(CompletionItemKind::FUNCTION), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: self.format_help(¤t_file.workspace_root).join("\n\n"), + })), + additional_text_edits, + ..Default::default() + } + } +} + +async fn build_identifier_completions( context: &RequestContext, current_file: &Arc, + workspace: &WorkspaceAnalyzer, offset: usize, + workspace_completion: bool, ) -> Result> { // Handle identifier completions. // If the cursor is after a dot, we can't make suggestions. @@ -95,37 +152,41 @@ fn build_identifier_completions( return Ok(Vec::new()); } - let environment = context - .analyzer - .analyze_at(current_file, offset, context.request_time)?; + let environment = workspace.analyze_at(current_file, offset, context.request_time); + let symbols = SymbolSet::workspace(workspace).await; - // Enumerate variables at the current scope. - let variable_items = environment.get().variables.iter().map(|(name, variable)| { - let paragraphs = variable.format_help(¤t_file.workspace_root); - CompletionItem { - label: name.to_string(), - kind: Some(CompletionItemKind::VARIABLE), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: paragraphs.join("\n\n"), - })), - ..Default::default() - } - }); + // Enumerate variables/templates already in the scope. + let known_variables: HashSet<&str> = environment.get().variables.keys().copied().collect(); + let known_templates: HashSet<&str> = environment.get().templates.keys().copied().collect(); - // Enumerate templates defined at the current position. - let template_items = environment.get().templates.values().map(|template| { - let paragraphs = template.format_help(¤t_file.workspace_root); - CompletionItem { - label: template.name.to_string(), - kind: Some(CompletionItemKind::FUNCTION), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: paragraphs.join("\n\n"), - })), - ..Default::default() - } - }); + // Enumerate local variables/templates. + let local_variable_items = environment + .get() + .variables + .values() + .map(|variable| variable.as_completion_item(current_file, false)); + let local_template_items = environment + .get() + .templates + .values() + .map(|template| template.as_completion_item(current_file, false)); + + // Enumerate workspace variables/templates. + let workspace_items: Vec<_> = if workspace_completion { + let workspace_variable_items = symbols + .variables() + .filter(|variable| !known_variables.contains(variable.name)) + .map(|variable| variable.as_completion_item(current_file, true)); + let workspace_template_items = symbols + .templates() + .filter(|template| !known_templates.contains(template.name)) + .map(|template| template.as_completion_item(current_file, true)); + workspace_variable_items + .chain(workspace_template_items) + .collect() + } else { + Vec::new() + }; // Enumerate builtins. let builtin_function_items = BUILTINS @@ -163,8 +224,9 @@ fn build_identifier_completions( ..Default::default() }); - Ok(variable_items - .chain(template_items) + Ok(local_variable_items + .chain(local_template_items) + .chain(workspace_items) .chain(builtin_function_items) .chain(builtin_variable_items) .chain(keyword_items) @@ -175,8 +237,10 @@ pub async fn completion( context: &RequestContext, params: CompletionParams, ) -> Result> { + let config = context.client.configurations().await; let path = get_text_document_path(¶ms.text_document_position.text_document)?; - let current_file = context.analyzer.analyze_file(&path, context.request_time)?; + let workspace = context.analyzer.workspace_for(&path)?; + let current_file = workspace.analyze_file(&path, context.request_time); let offset = current_file .document @@ -200,7 +264,14 @@ pub async fn completion( } // Handle identifier completions. - let items = build_identifier_completions(context, ¤t_file, offset)?; + let items = build_identifier_completions( + context, + ¤t_file, + &workspace, + offset, + config.experimental.workspace_completion, + ) + .await?; Ok(Some(CompletionResponse::Array(items))) } diff --git a/src/server/symbols.rs b/src/server/symbols.rs index 53881b5..61098c7 100644 --- a/src/server/symbols.rs +++ b/src/server/symbols.rs @@ -24,7 +24,7 @@ impl Variable<'_> { fn as_symbol_information(&self) -> SymbolInformation { let first_assignment = self.assignments.first().unwrap(); SymbolInformation { - name: first_assignment.primary_variable.as_str().to_string(), + name: self.name.to_string(), kind: if self.is_args { SymbolKind::CONSTANT } else { @@ -112,4 +112,16 @@ impl SymbolSet { variables.chain(templates) }) } + + pub fn variables(&self) -> impl Iterator> + '_ { + self.files + .iter() + .flat_map(|file| file.exports.get().variables.values()) + } + + pub fn templates(&self) -> impl Iterator> + '_ { + self.files + .iter() + .flat_map(|file| file.exports.get().templates.values()) + } } diff --git a/vscode-gn/package.json b/vscode-gn/package.json index 600cfd1..ad6bda1 100644 --- a/vscode-gn/package.json +++ b/vscode-gn/package.json @@ -93,6 +93,11 @@ "default": false, "description": "Enables undefined variable analysis (experimental)." }, + "gn.experimental.workspaceCompletion": { + "type": "boolean", + "default": false, + "description": "Enables workspace completion (experimental)." + }, "gn.experimental.workspaceSymbols": { "type": "boolean", "default": false,