mirror of
https://github.com/google/gn-language-server.git
synced 2025-12-23 12:26:43 +00:00
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
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:
parent
779f4c7b13
commit
fae720b2cb
8 changed files with 177 additions and 65 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -60,4 +60,5 @@ pub struct ExperimentalConfigurations {
|
|||
pub workspace_symbols: bool,
|
||||
pub target_lens: bool,
|
||||
pub parallel_indexing: bool,
|
||||
pub workspace_completion: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<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(¤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<Option<CompletionResponse>> {
|
||||
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)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue