Skip to content

Commit 76bfeab

Browse files
committed
feat: workspace symbols
1 parent 1241b18 commit 76bfeab

File tree

6 files changed

+153
-101
lines changed

6 files changed

+153
-101
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Works for `record`s, `template`s and `env.ref()`!
1414

1515
[![model demo](https://raw.githubusercontent.com/Desdaemon/odoo-lsp/main/static/model.gif)](https://asciinema.org/a/604545)
1616

17+
### Browse models and XML records as workspace symbols
18+
19+
[![symbols demo](https://raw.githubusercontent.com/Desdaemon/odoo-lsp/main/static/symbols.gif)](https://asciinema.org/a/604560)
20+
1721
For more features check out the [wiki].
1822

1923
## Install

client/src/extension.ts

Lines changed: 101 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,31 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
* ------------------------------------------------------------------------------------------ */
55

6+
import { dirname } from "node:path";
67
import {
78
// languages,
89
workspace,
910
window,
10-
commands,
11-
EventEmitter,
11+
// commands,
12+
// EventEmitter,
1213
ExtensionContext,
1314
// InlayHintsProvider,
1415
// TextDocument,
1516
// CancellationToken,
16-
Range,
17+
// Range,
1718
// InlayHint,
18-
TextDocumentChangeEvent,
19+
// TextDocumentChangeEvent,
1920
// ProviderResult,
2021
// WorkspaceEdit,
2122
// TextEdit,
22-
Selection,
23-
Uri,
23+
// Selection,
24+
// Uri,
2425
} from "vscode";
2526

2627
import {
2728
// CloseAction,
2829
// CloseHandlerResult,
29-
Disposable,
30+
// Disposable,
3031
// ErrorAction,
3132
// ErrorHandlerResult,
3233
Executable,
@@ -39,28 +40,29 @@ import {
3940
let client: LanguageClient;
4041
// type a = Parameters<>;
4142

42-
export async function activate(context: ExtensionContext) {
43-
let disposable = commands.registerCommand("helloworld.helloWorld", async (uri) => {
44-
// The code you place here will be executed every time your command is executed
45-
// Display a message box to the user
46-
const url = Uri.parse("/home/victor/Documents/test-dir/nrs/another.nrs");
47-
let document = await workspace.openTextDocument(uri);
48-
await window.showTextDocument(document);
43+
export async function activate(_context: ExtensionContext) {
44+
// let disposable = commands.registerCommand("helloworld.helloWorld", async (uri) => {
45+
// // The code you place here will be executed every time your command is executed
46+
// // Display a message box to the user
47+
// const url = Uri.parse("/home/victor/Documents/test-dir/nrs/another.nrs");
48+
// let document = await workspace.openTextDocument(uri);
49+
// await window.showTextDocument(document);
4950

50-
// console.log(uri)
51-
window.activeTextEditor.document;
52-
let editor = window.activeTextEditor;
53-
let range = new Range(1, 1, 1, 1);
54-
editor.selection = new Selection(range.start, range.end);
55-
});
51+
// // console.log(uri)
52+
// window.activeTextEditor.document;
53+
// let editor = window.activeTextEditor;
54+
// let range = new Range(1, 1, 1, 1);
55+
// editor.selection = new Selection(range.start, range.end);
56+
// });
5657

57-
context.subscriptions.push(disposable);
58+
// context.subscriptions.push(disposable);
5859

5960
const traceOutputChannel = window.createOutputChannel("Odoo LSP");
6061
const command = process.env.SERVER_PATH || "odoo-lsp";
6162
const run: Executable = {
6263
command,
6364
options: {
65+
cwd: workspace.workspaceFolders.length ? dirname(workspace.workspaceFolders[0].uri.fsPath) : void 0,
6466
env: {
6567
...process.env,
6668
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -84,7 +86,7 @@ export async function activate(context: ExtensionContext) {
8486
],
8587
synchronize: {
8688
// Notify the server about file changes to '.clientrc files contained in the workspace
87-
fileEvents: workspace.createFileSystemWatcher("**/.clientrc"),
89+
fileEvents: workspace.createFileSystemWatcher("**/.odoo_lsp*"),
8890
},
8991
traceOutputChannel,
9092
};
@@ -102,80 +104,80 @@ export function deactivate(): Thenable<void> | undefined {
102104
return client.stop();
103105
}
104106

105-
export function activateInlayHints(ctx: ExtensionContext) {
106-
const maybeUpdater = {
107-
hintsProvider: null as Disposable | null,
108-
updateHintsEventEmitter: new EventEmitter<void>(),
109-
110-
async onConfigChange() {
111-
this.dispose();
112-
113-
const event = this.updateHintsEventEmitter.event;
114-
// this.hintsProvider = languages.registerInlayHintsProvider(
115-
// { scheme: "file", language: "nrs" },
116-
// // new (class implements InlayHintsProvider {
117-
// // onDidChangeInlayHints = event;
118-
// // resolveInlayHint(hint: InlayHint, token: CancellationToken): ProviderResult<InlayHint> {
119-
// // const ret = {
120-
// // label: hint.label,
121-
// // ...hint,
122-
// // };
123-
// // return ret;
124-
// // }
125-
// // async provideInlayHints(
126-
// // document: TextDocument,
127-
// // range: Range,
128-
// // token: CancellationToken
129-
// // ): Promise<InlayHint[]> {
130-
// // const hints = (await client
131-
// // .sendRequest("custom/inlay_hint", { path: document.uri.toString() })
132-
// // .catch(err => null)) as [number, number, string][];
133-
// // if (hints == null) {
134-
// // return [];
135-
// // } else {
136-
// // return hints.map(item => {
137-
// // const [start, end, label] = item;
138-
// // let startPosition = document.positionAt(start);
139-
// // let endPosition = document.positionAt(end);
140-
// // return {
141-
// // position: endPosition,
142-
// // paddingLeft: true,
143-
// // label: [
144-
// // {
145-
// // value: `${label}`,
146-
// // // location: {
147-
// // // uri: document.uri,
148-
// // // range: new Range(1, 0, 1, 0)
149-
// // // }
150-
// // command: {
151-
// // title: "hello world",
152-
// // command: "helloworld.helloWorld",
153-
// // arguments: [document.uri],
154-
// // },
155-
// // },
156-
// // ],
157-
// // };
158-
// // });
159-
// // }
160-
// // }
161-
// // })()
162-
// );
163-
},
164-
165-
onDidChangeTextDocument({ contentChanges, document }: TextDocumentChangeEvent) {
166-
// debugger
167-
// this.updateHintsEventEmitter.fire();
168-
},
169-
170-
dispose() {
171-
this.hintsProvider?.dispose();
172-
this.hintsProvider = null;
173-
this.updateHintsEventEmitter.dispose();
174-
},
175-
};
176-
177-
workspace.onDidChangeConfiguration(maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions);
178-
workspace.onDidChangeTextDocument(maybeUpdater.onDidChangeTextDocument, maybeUpdater, ctx.subscriptions);
179-
180-
maybeUpdater.onConfigChange().catch(console.error);
181-
}
107+
// export function activateInlayHints(ctx: ExtensionContext) {
108+
// const maybeUpdater = {
109+
// hintsProvider: null as Disposable | null,
110+
// updateHintsEventEmitter: new EventEmitter<void>(),
111+
112+
// async onConfigChange() {
113+
// this.dispose();
114+
115+
// const event = this.updateHintsEventEmitter.event;
116+
// // this.hintsProvider = languages.registerInlayHintsProvider(
117+
// // { scheme: "file", language: "nrs" },
118+
// // // new (class implements InlayHintsProvider {
119+
// // // onDidChangeInlayHints = event;
120+
// // // resolveInlayHint(hint: InlayHint, token: CancellationToken): ProviderResult<InlayHint> {
121+
// // // const ret = {
122+
// // // label: hint.label,
123+
// // // ...hint,
124+
// // // };
125+
// // // return ret;
126+
// // // }
127+
// // // async provideInlayHints(
128+
// // // document: TextDocument,
129+
// // // range: Range,
130+
// // // token: CancellationToken
131+
// // // ): Promise<InlayHint[]> {
132+
// // // const hints = (await client
133+
// // // .sendRequest("custom/inlay_hint", { path: document.uri.toString() })
134+
// // // .catch(err => null)) as [number, number, string][];
135+
// // // if (hints == null) {
136+
// // // return [];
137+
// // // } else {
138+
// // // return hints.map(item => {
139+
// // // const [start, end, label] = item;
140+
// // // let startPosition = document.positionAt(start);
141+
// // // let endPosition = document.positionAt(end);
142+
// // // return {
143+
// // // position: endPosition,
144+
// // // paddingLeft: true,
145+
// // // label: [
146+
// // // {
147+
// // // value: `${label}`,
148+
// // // // location: {
149+
// // // // uri: document.uri,
150+
// // // // range: new Range(1, 0, 1, 0)
151+
// // // // }
152+
// // // command: {
153+
// // // title: "hello world",
154+
// // // command: "helloworld.helloWorld",
155+
// // // arguments: [document.uri],
156+
// // // },
157+
// // // },
158+
// // // ],
159+
// // // };
160+
// // // });
161+
// // // }
162+
// // // }
163+
// // // })()
164+
// // );
165+
// },
166+
167+
// onDidChangeTextDocument({ contentChanges, document }: TextDocumentChangeEvent) {
168+
// // debugger
169+
// // this.updateHintsEventEmitter.fire();
170+
// },
171+
172+
// dispose() {
173+
// this.hintsProvider?.dispose();
174+
// this.hintsProvider = null;
175+
// this.updateHintsEventEmitter.dispose();
176+
// },
177+
// };
178+
179+
// workspace.onDidChangeConfiguration(maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions);
180+
// workspace.onDidChangeTextDocument(maybeUpdater.onDidChangeTextDocument, maybeUpdater, ctx.subscriptions);
181+
182+
// maybeUpdater.onConfigChange().catch(console.error);
183+
// }

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
"default": "off",
4444
"description": "Traces the communication between VS Code and the language server."
4545
},
46+
"odoo-lsp.symbols.limit": {
47+
"type": "number",
48+
"default": 80,
49+
"description": "Maximum amount of workspace symbols to view at once."
50+
},
4651
"odoo-lsp.module.roots": {
4752
"type": "array",
4853
"scope": "resource",

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ use serde::Deserialize;
33
#[derive(Deserialize, Debug, Clone)]
44
pub struct Config {
55
pub module: Option<ModuleConfig>,
6+
pub symbols: Option<SymbolsConfig>,
67
}
78

89
#[derive(Deserialize, Debug, Clone)]
910
pub struct ModuleConfig {
1011
pub roots: Option<Vec<String>>,
1112
}
13+
14+
#[derive(Deserialize, Debug, Clone)]
15+
pub struct SymbolsConfig {
16+
pub limit: Option<u32>,
17+
}

src/main.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::borrow::Cow;
22
use std::path::{Path, PathBuf};
3-
use std::sync::atomic::AtomicBool;
43
use std::sync::atomic::Ordering::Relaxed;
4+
use std::sync::atomic::{AtomicBool, AtomicUsize};
55

66
use catch_panic::CatchPanic;
77
use dashmap::{DashMap, DashSet};
@@ -17,7 +17,7 @@ use tower_lsp::lsp_types::*;
1717
use tower_lsp::{Client, LanguageServer, LspService, Server};
1818
use tree_sitter::{Parser, Tree};
1919

20-
use odoo_lsp::config::{Config, ModuleConfig};
20+
use odoo_lsp::config::{Config, ModuleConfig, SymbolsConfig};
2121
use odoo_lsp::index::ModuleIndex;
2222
use odoo_lsp::model::ModelLocation;
2323
use odoo_lsp::{format_loc, utils::*};
@@ -35,6 +35,7 @@ pub struct Backend {
3535
roots: DashSet<String>,
3636
capabilities: Capabilities,
3737
root_setup: AtomicBool,
38+
symbols_limit: AtomicUsize,
3839
}
3940

4041
#[derive(Debug, Default)]
@@ -98,6 +99,7 @@ impl LanguageServer for Backend {
9899
capabilities: ServerCapabilities {
99100
definition_provider: Some(OneOf::Left(true)),
100101
references_provider: Some(OneOf::Left(true)),
102+
workspace_symbol_provider: Some(OneOf::Left(true)),
101103
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::INCREMENTAL)),
102104
completion_provider: Some(CompletionOptions {
103105
resolve_provider: None,
@@ -131,6 +133,7 @@ impl LanguageServer for Backend {
131133
roots: _,
132134
capabilities: _,
133135
root_setup: _,
136+
symbols_limit: _,
134137
} = self;
135138
document_map.remove(path);
136139
record_ranges.remove(path);
@@ -454,6 +457,34 @@ impl LanguageServer for Backend {
454457
async fn execute_command(&self, _: ExecuteCommandParams) -> Result<Option<Value>> {
455458
Ok(None)
456459
}
460+
#[allow(deprecated)]
461+
async fn symbol(&self, params: WorkspaceSymbolParams) -> Result<Option<Vec<SymbolInformation>>> {
462+
let query = &params.query;
463+
let records = self.module_index.records.iter().filter_map(|entry| {
464+
entry.id.contains(query).then(|| SymbolInformation {
465+
name: entry.id.to_string(),
466+
kind: SymbolKind::VARIABLE,
467+
tags: None,
468+
deprecated: None,
469+
location: entry.location.clone(),
470+
container_name: None,
471+
})
472+
});
473+
let models = self.module_index.models.iter().filter_map(|entry| {
474+
entry.0.as_ref().and_then(|loc| {
475+
entry.key().contains(query).then(|| SymbolInformation {
476+
name: entry.key().clone(),
477+
kind: SymbolKind::CONSTANT,
478+
tags: None,
479+
deprecated: None,
480+
location: loc.0.clone(),
481+
container_name: None,
482+
})
483+
})
484+
});
485+
let limit = self.symbols_limit.load(Relaxed);
486+
Ok(Some(models.chain(records).take(limit).collect()))
487+
}
457488
}
458489

459490
struct TextDocumentItem {
@@ -667,6 +698,9 @@ impl Backend {
667698
Ok(Some(locations))
668699
}
669700
async fn on_change_config(&self, config: Config) {
701+
if let Some(SymbolsConfig { limit: Some(limit) }) = config.symbols {
702+
self.symbols_limit.store(limit as usize, Relaxed);
703+
}
670704
let Some(ModuleConfig { roots: Some(roots), .. }) = config.module else {
671705
return;
672706
};
@@ -710,6 +744,7 @@ async fn main() {
710744
capabilities: Default::default(),
711745
root_setup: Default::default(),
712746
ast_map: DashMap::new(),
747+
symbols_limit: AtomicUsize::new(80),
713748
})
714749
.finish();
715750

static/symbols.gif

1020 KB
Loading

0 commit comments

Comments
 (0)