Skip to content

Commit

Permalink
Add debug command for Concrete Syntax Tree
Browse files Browse the repository at this point in the history
  • Loading branch information
SpontanCombust committed Jun 13, 2024
1 parent a35d783 commit dd0ee37
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 33 deletions.
56 changes: 26 additions & 30 deletions crates/core/src/syntax_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,19 @@ impl<'script, T> SyntaxNode<'script, T> {
}


/// Returns tree-sitter's node structure in a form of XML.
/// Use for debugging purposes.
pub fn debug_ts_tree(&self, doc: &ScriptDocument) -> String {
/// Returns tree-sitter's Concrete Syntax Tree in string representation
pub fn cst_to_string(&self) -> String {
self.use_cursor(|mut cursor| {
let mut buf = String::new();

let mut needs_newline = false;
let mut indent_level = 0;
let mut did_visit_children = false;
let mut tags: Vec<&str> = Vec::new();

loop {
let node = cursor.node();
let is_named = node.is_named();
if did_visit_children {
if is_named {
let tag = tags.pop();
buf += &format!("</{}>\n", tag.expect("there is a tag"));
needs_newline = true;
}
if cursor.goto_next_sibling() {
Expand All @@ -193,37 +188,38 @@ impl<'script, T> SyntaxNode<'script, T> {
break;
}
} else {
if needs_newline {
buf.push('\n');
}
for _ in 0..indent_level {
buf.push_str(" ");
}
let start = node.start_position();
let end = node.end_position();
if let Some(field_name) = cursor.field_name() {
buf.push_str(&format!("{field_name}: "));
}

if is_named {
if needs_newline {
buf += &format!("\n");
}
for _ in 0..indent_level {
buf += &format!(" ");
}
buf += &format!("<{}", node.kind());
if let Some(field_name) = cursor.field_name() {
buf += &format!(" type=\"{}\"", field_name);
}
buf += &format!(">");
tags.push(node.kind());
needs_newline = true;
buf.push_str(node.kind());
} else {
buf.push_str(&format!("\"{}\"", node.kind()));
}

buf.push_str(&format!(" [{}, {}] - [{}, {}]",
start.row + 1,
start.column + 1,
end.row + 1,
end.column + 1
));
needs_newline = true;

if cursor.goto_first_child() {
did_visit_children = false;
indent_level += 1;
} else {
let node_range = node.range();
let lsp_range = lsp::Range::new(
lsp::Position::new(node_range.start_point.row as u32, node_range.start_point.column as u32),
lsp::Position::new(node_range.end_point.row as u32, node_range.end_point.column as u32)
);

buf += &doc.text_at(lsp_range);
did_visit_children = true;
}
if node.is_missing() {
buf += &format!("MISSING");
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/lsp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async fn main() {
.custom_method(requests::projects::vanilla_dependency_content::METHOD, Backend::handle_projects_vanilla_dependency_content_request)
.custom_method(requests::scripts::parent_content::METHOD, Backend::handle_scripts_parent_content_request)
.custom_method(requests::debug::script_ast::METHOD, Backend::handle_debug_script_ast_request)
.custom_method(requests::debug::script_cst::METHOD, Backend::handle_debug_script_cst_request)
.custom_method(requests::debug::content_graph_dot::METHOD, Backend::handle_debug_content_graph_dot_request)
.custom_method(requests::debug::script_symbols::METHOD, Backend::handle_debug_script_symbols_request)
.custom_method(notifications::projects::did_import_scripts::METHOD, Backend::handle_projects_did_import_scripts_notification)
Expand Down
23 changes: 23 additions & 0 deletions crates/lsp/src/messaging/custom_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,29 @@ impl Backend {
self.reporter.log_error("Imported files do no belong to a known content!").await;
}
}

pub async fn handle_debug_script_cst_request(&self, params: requests::debug::script_cst::Parameters) -> Result<requests::debug::script_cst::Response> {
let script_path: AbsPath;
if let Ok(path) = AbsPath::try_from(params.script_uri) {
script_path = path;
} else {
return Err(jsonrpc::Error::invalid_params("script_uri parameter is not a valid file URI"));
}

let script_entry = self.scripts.get(&script_path).ok_or(jsonrpc::Error {
code: jsonrpc::ErrorCode::ServerError(-1080),
message: "Script file not found".into(),
data: None
})?;

let script = &script_entry.script;
let cst = script.root_node().cst_to_string();
drop(script_entry);

Ok(requests::debug::script_cst::Response {
cst
})
}
}


Expand Down
19 changes: 19 additions & 0 deletions crates/lsp/src/messaging/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@ pub mod debug {
pub const METHOD: &'static str = "witcherscript-ide/debug/scriptAst";
}

/// Returns script file's CST (Concrete Syntax Tree) representation
pub mod script_cst {
use super::*;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Parameters {
pub script_uri: lsp::Url
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Response {
pub cst: String
}

pub const METHOD: &'static str = "witcherscript-ide/debug/scriptCst";
}

/// Returns the content graph visualization in graphviz .dot format
pub mod content_graph_dot {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
"category": "WitcherScript-IDE (debug)",
"enablement": "witcherscript-ide.languageServerActive && witcherscript-ide.debugFeaturesEnabled && editorLangId == 'witcherscript'"
},
{
"command": "witcherscript-ide.debug.showScriptCst",
"title": "Show script CST",
"category": "WitcherScript-IDE (debug)",
"enablement": "witcherscript-ide.languageServerActive && witcherscript-ide.debugFeaturesEnabled && editorLangId == 'witcherscript'"
},
{
"command": "witcherscript-ide.debug.contentGraphDot",
"title": "Show content graph",
Expand Down
55 changes: 53 additions & 2 deletions editors/vscode/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function registerCommands(context: vscode.ExtensionContext) {
if (cfg.enableDebugFeatures) {
context.subscriptions.push(
vscode.commands.registerCommand("witcherscript-ide.debug.showScriptAst", commandShowScriptAst(context)),
vscode.commands.registerCommand("witcherscript-ide.debug.showScriptCst", commandShowScriptCst(context)),
vscode.commands.registerCommand("witcherscript-ide.debug.contentGraphDot", commandContentGraphDot()),
vscode.commands.registerCommand("witcherscript-ide.debug.showScriptSymbols", commandShowScriptSymbols()),
vscode.commands.registerCommand("witcherscript-ide.debug.clearGlobalState", commandClearGlobalState(context))
Expand Down Expand Up @@ -478,8 +479,6 @@ function commandShowScriptSymbols(): Cmd {
};
}



function commandClearGlobalState(context: vscode.ExtensionContext): Cmd {
return async () => {
const keys = context.globalState.keys();
Expand All @@ -497,6 +496,58 @@ function commandClearGlobalState(context: vscode.ExtensionContext): Cmd {
}
}

function commandShowScriptCst(context: vscode.ExtensionContext): Cmd {
return async () => {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}

const scriptPath = activeEditor.document.uri.fsPath;
const scriptLine = activeEditor.selection.active.line + 1;
const uri = vscode.Uri
.file(scriptPath + tdcp.ScriptCstProvider.pathSuffix)
.with({ scheme: tdcp.ScriptCstProvider.scheme });

const doc = await vscode.workspace.openTextDocument(uri);
const options: vscode.TextDocumentShowOptions = {
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: true
};

tdcp.ScriptCstProvider.getInstance().eventEmitter.fire(uri);

vscode.window.showTextDocument(doc, options).then(async editor => {
const cstText = editor.document.getText();
const match = cstText.search(new RegExp("\\[" + scriptLine));
if (match != -1) {
const targetPos = editor.document.positionAt(match);
editor.revealRange(new vscode.Range(targetPos, targetPos), vscode.TextEditorRevealType.AtTop);
}

const rememberedChoices = state.RememberedChoices.Memento.fetchOrDefault(context);
// using the same memento for AST warning for simplicity
if (!rememberedChoices.neverShowAgainDebugAstNotif) {
enum Answer {
Close = "I understand",
NeverShowAgain = "Never show this message again"
}

const answer = await vscode.window.showInformationMessage(
"Beware! Displayed ranges in the CST may not be accurate if your document is formatted using tabs instead of spaces",
Answer.Close, Answer.NeverShowAgain
);

if (answer == Answer.NeverShowAgain) {
rememberedChoices.neverShowAgainDebugAstNotif = true;
rememberedChoices.store(context);
}
}
});
}
}




Expand Down
1 change: 1 addition & 0 deletions editors/vscode/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as tdcp from './text_document_content_providers';
export function registerProviders(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(tdcp.ScriptAstProvider.scheme, tdcp.ScriptAstProvider.getInstance()),
vscode.workspace.registerTextDocumentContentProvider(tdcp.ScriptCstProvider.scheme, tdcp.ScriptCstProvider.getInstance()),
vscode.workspace.registerTextDocumentContentProvider(tdcp.ContentGraphDotProvider.scheme, tdcp.ContentGraphDotProvider.getInstance()),
vscode.workspace.registerTextDocumentContentProvider(tdcp.ScriptSymbolsProvider.scheme, tdcp.ScriptSymbolsProvider.getInstance()),
);
Expand Down
50 changes: 49 additions & 1 deletion editors/vscode/src/providers/text_document_content_providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,55 @@ export class ScriptAstProvider implements vscode.TextDocumentContentProvider {
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
}
}


export class ScriptCstProvider implements vscode.TextDocumentContentProvider {
private static instance: ScriptCstProvider;

private constructor() {}
public static getInstance(): ScriptCstProvider {
if (!ScriptCstProvider.instance) {
ScriptCstProvider.instance = new ScriptCstProvider();
}

return ScriptCstProvider.instance;
}


public static readonly scheme = "witcherscript-ide-cst";
public static readonly pathSuffix = " - CST";

public eventEmitter = new vscode.EventEmitter<vscode.Uri>();


provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
const client = getLanguageClient();
if (client == undefined) {
vscode.window.showErrorMessage("Language Server is not active!");
return;
}

uri = vscode.Uri.file(uri.fsPath.substring(0, uri.fsPath.length - ScriptCstProvider.pathSuffix.length));

const params: requests.debug.scriptCst.Parameters = {
scriptUri: client.code2ProtocolConverter.asUri(uri)
}
return client.sendRequest(requests.debug.scriptCst.type, params).then(
(response) => {
return response.cst;
},
(error) => {
vscode.window.showErrorMessage(`${error.message} [code ${error.code}]`);
return ""
}
)
}

get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
}


export class ContentGraphDotProvider implements vscode.TextDocumentContentProvider {
Expand Down
12 changes: 12 additions & 0 deletions editors/vscode/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ export namespace debug {
export const type = new RequestType<Parameters, Response, void>("witcherscript-ide/debug/scriptAst");
}

export namespace scriptCst {
export interface Parameters {
scriptUri: string
}

export interface Response {
cst: string
}

export const type = new RequestType<Parameters, Response, void>("witcherscript-ide/debug/scriptCst");
}

export namespace contentGraphDot {
export interface Parameters {

Expand Down

0 comments on commit dd0ee37

Please sign in to comment.