Experimental support of code lens

This commit is contained in:
Shuhei Takahashi 2025-12-12 00:54:31 +09:00
parent 8cee5ab3b3
commit ae8ad15785
8 changed files with 211 additions and 9 deletions

View file

@ -45,4 +45,5 @@ impl Default for Configurations {
pub struct ExperimentalConfigurations {
pub undefined_variable_analysis: bool,
pub workspace_symbols: bool,
pub target_lens: bool,
}

View file

@ -55,4 +55,10 @@ impl From<std::io::Error> for Error {
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Error::General(error.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View file

@ -22,14 +22,14 @@ use std::{
use tokio::spawn;
use tower_lsp::{
lsp_types::{
CompletionOptions, CompletionParams, CompletionResponse, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DocumentFormattingParams, DocumentLink, DocumentLinkOptions, DocumentLinkParams,
DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse,
Hover, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
InitializedParams, Location, MessageType, OneOf, ReferenceParams, ServerCapabilities,
SymbolInformation, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url,
WorkspaceSymbolParams,
CodeLens, CodeLensOptions, CodeLensParams, CompletionOptions, CompletionParams,
CompletionResponse, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
DocumentLink, DocumentLinkOptions, DocumentLinkParams, DocumentSymbolParams,
DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams,
HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, Location,
MessageType, OneOf, ReferenceParams, ServerCapabilities, SymbolInformation,
TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceSymbolParams,
},
LanguageServer, LspService, Server,
};
@ -168,6 +168,9 @@ impl LanguageServer for Backend {
document_formatting_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(true),
}),
..Default::default()
},
..Default::default()
@ -265,6 +268,14 @@ impl LanguageServer for Backend {
) -> RpcResult<Option<Vec<SymbolInformation>>> {
Ok(providers::workspace_symbol::workspace_symbol(&self.context.request(), params).await?)
}
async fn code_lens(&self, params: CodeLensParams) -> RpcResult<Option<Vec<CodeLens>>> {
Ok(providers::code_lens::code_lens(&self.context.request(), params).await?)
}
async fn code_lens_resolve(&self, partial_lens: CodeLens) -> RpcResult<CodeLens> {
Ok(providers::code_lens::code_lens_resolve(&self.context.request(), partial_lens).await?)
}
}
pub async fn run() {

View file

@ -0,0 +1,143 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::PathBuf;
use serde_json::Value;
use tower_lsp::lsp_types::{CodeLens, CodeLensParams, Command, Position};
use crate::{
common::error::Result,
server::{
providers::{
references::target_references,
utils::{format_path, get_text_document_path},
},
RequestContext,
},
};
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
enum CodeLensData {
TargetReferences(CodeLensDataTargetReferences),
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CodeLensDataTargetReferences {
pub path: PathBuf,
pub position: Position,
pub target_name: String,
}
pub async fn code_lens(
context: &RequestContext,
params: CodeLensParams,
) -> Result<Option<Vec<CodeLens>>> {
if !context
.client
.configurations()
.await
.experimental
.target_lens
{
return Ok(None);
}
let path = get_text_document_path(&params.text_document)?;
let current_file = context.analyzer.analyze_file(&path, context.request_time)?;
Ok(Some(
current_file
.analyzed_root
.targets()
.flat_map(|target| {
let range = current_file.document.line_index.range(target.call.span);
let label = format!(
"{}:{}",
format_path(
current_file.document.path.parent().unwrap(),
&current_file.workspace_root
),
target.name
);
let position = current_file
.document
.line_index
.position(target.call.span.start());
[
CodeLens {
range,
command: None,
data: Some(
serde_json::to_value(CodeLensData::TargetReferences(
CodeLensDataTargetReferences {
path: path.clone(),
position,
target_name: target.name.to_string(),
},
))
.unwrap(),
),
},
CodeLens {
range,
command: Some(Command {
title: "Copy".to_string(),
command: "gn.copyTargetLabel".to_string(),
arguments: Some(vec![Value::String(label)]),
}),
data: None,
},
]
})
.collect(),
))
}
pub async fn code_lens_resolve(
context: &RequestContext,
partial_lens: CodeLens,
) -> Result<CodeLens> {
let data = serde_json::from_value::<CodeLensData>(partial_lens.data.unwrap())?;
match data {
CodeLensData::TargetReferences(CodeLensDataTargetReferences {
path,
position,
target_name,
}) => {
let current_file = context.analyzer.analyze_file(&path, context.request_time)?;
let references = target_references(context, &current_file, &target_name)
.await?
.unwrap_or_default();
let title = match references.len() {
0 => "No references".to_string(),
1 => "1 reference".to_string(),
n => format!("{n} references"),
};
Ok(CodeLens {
range: partial_lens.range,
command: Some(Command {
command: "gn.showTargetReferences".to_string(),
title,
arguments: Some(vec![
serde_json::to_value(position).unwrap(),
serde_json::to_value(references).unwrap(),
]),
}),
data: None,
})
}
}
}

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod code_lens;
pub mod completion;
pub mod configuration;
pub mod diagnostics;

View file

@ -30,7 +30,7 @@ fn get_overlapping_targets<'i>(root: &AnalyzedBlock<'i, '_>, prefix: &str) -> Ve
.collect()
}
async fn target_references(
pub async fn target_references(
context: &RequestContext,
current_file: &AnalyzedFile,
target_name: &str,

View file

@ -78,6 +78,11 @@
"default": true,
"description": "Reports syntax errors."
},
"gn.experimental.targetLens": {
"type": "boolean",
"default": false,
"description": "Enables code lens for build targets (experimental)."
},
"gn.experimental.undefinedVariableAnalysis": {
"type": "boolean",
"default": false,

View file

@ -16,10 +16,13 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as p2c from 'vscode-languageclient/lib/common/protocolConverter';
import {
LanguageClient,
LanguageClientOptions,
Location,
MessageSignature,
Position,
ResponseError,
ServerOptions,
TransportKind,
@ -126,6 +129,29 @@ async function openBuildFile(): Promise<void> {
);
}
async function showTargetReferences(
converter: p2c.Converter,
position: Position,
locations: Location[]
): Promise<void> {
const documentUri = vscode.window.activeTextEditor?.document?.uri;
if (!documentUri) {
void vscode.window.showErrorMessage('No open editor.');
return;
}
await vscode.commands.executeCommand(
'editor.action.showReferences',
documentUri,
converter.asPosition(position),
locations.map(converter.asLocation)
);
}
async function copyTargetLabel(label: string): Promise<void> {
await vscode.env.clipboard.writeText(label);
}
class GnLanguageClient extends LanguageClient {
constructor(context: vscode.ExtensionContext, output: vscode.OutputChannel) {
const clientOptions: LanguageClientOptions = {
@ -189,6 +215,15 @@ async function startLanguageServer(
const client = new GnLanguageClient(context, output);
context.subscriptions.push(client);
await client.start();
context.subscriptions.push(
vscode.commands.registerCommand(
'gn.showTargetReferences',
(position: Position, locations: Location[]) =>
showTargetReferences(client.protocol2CodeConverter, position, locations)
),
vscode.commands.registerCommand('gn.copyTargetLabel', copyTargetLabel),
);
}
export function activate(context: vscode.ExtensionContext): void {