Experimental support of workspace completion
Some checks are pending
CI / lint (push) Waiting to run
CI / build (darwin-arm64) (push) Waiting to run
CI / build (linux-x64) (push) Waiting to run
CI / build (win32-x64) (push) Waiting to run
CI / attestation (push) Blocked by required conditions
CI / upload-release-artifacts (push) Blocked by required conditions
CI / publish-vscode (push) Blocked by required conditions
CI / publish-cargo (push) Blocked by required conditions

This commit is contained in:
Shuhei Takahashi 2025-12-16 08:53:49 +09:00
parent 779f4c7b13
commit fae720b2cb
8 changed files with 177 additions and 65 deletions

View file

@ -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<VariableAssignment<'p>>,
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,
}

View file

@ -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 {

View file

@ -60,4 +60,5 @@ pub struct ExperimentalConfigurations {
pub workspace_symbols: bool,
pub target_lens: bool,
pub parallel_indexing: bool,
pub workspace_completion: bool,
}

View file

@ -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(&current_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}"),
}
}

View file

@ -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(&current_file, only_import)),
edit: Some(WorkspaceEdit {
changes: Some(HashMap::from([(
Url::from_file_path(&current_file.document.path).unwrap(),
vec![create_import_edit(&current_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(&current_file, import),
edit: WorkspaceEdit {
changes: Some(HashMap::from([(
Url::from_file_path(&current_file.document.path).unwrap(),
vec![create_import_edit(&current_file, import)],
)])),
..Default::default()
},
})
.collect(),
};

View file

@ -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,
&current_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(&current_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, &current_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(&current_file.workspace_root).join("\n\n"),
})),
additional_text_edits,
..Default::default()
}
}
}
async fn build_identifier_completions(
context: &RequestContext,
current_file: &Arc<AnalyzedFile>,
workspace: &WorkspaceAnalyzer,
offset: usize,
workspace_completion: bool,
) -> Result<Vec<CompletionItem>> {
// 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(&current_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(&current_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<Option<CompletionResponse>> {
let config = context.client.configurations().await;
let path = get_text_document_path(&params.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, &current_file, offset)?;
let items = build_identifier_completions(
context,
&current_file,
&workspace,
offset,
config.experimental.workspace_completion,
)
.await?;
Ok(Some(CompletionResponse::Array(items)))
}

View file

@ -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<Item = &Variable<'_>> + '_ {
self.files
.iter()
.flat_map(|file| file.exports.get().variables.values())
}
pub fn templates(&self) -> impl Iterator<Item = &Template<'_>> + '_ {
self.files
.iter()
.flat_map(|file| file.exports.get().templates.values())
}
}

View file

@ -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,