Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2e0b86e

Browse files
coder3101asharkhan3101
andauthoredAug 19, 2024
improve: parse workspace in worker thread with progress (#24)
Co-authored-by: mohammadkhan <mohammadkhan@digitalocean.com>
1 parent 815de55 commit 2e0b86e

File tree

6 files changed

+167
-61
lines changed

6 files changed

+167
-61
lines changed
 

‎src/lsp.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::fs::read_to_string;
22
use std::ops::ControlFlow;
3+
use std::sync::mpsc;
4+
use std::thread;
35
use tracing::{error, info};
46

57
use async_lsp::lsp_types::{
@@ -9,9 +11,10 @@ use async_lsp::lsp_types::{
911
DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind,
1012
FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover,
1113
HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf,
12-
PrepareRenameResponse, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities,
13-
ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url,
14-
WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
14+
PrepareRenameResponse, ProgressParams, RenameFilesParams, RenameOptions,
15+
RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams,
16+
TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkspaceEdit,
17+
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
1518
WorkspaceServerCapabilities,
1619
};
1720
use async_lsp::{LanguageClient, LanguageServer, ResponseError};
@@ -46,6 +49,25 @@ impl LanguageServer for ProtoLanguageServer {
4649
},
4750
}];
4851

52+
let worktoken = params.work_done_progress_params.work_done_token;
53+
let (tx, rx) = mpsc::channel();
54+
let mut socket = self.client.clone();
55+
56+
thread::spawn(move || {
57+
let Some(token) = worktoken else {
58+
return;
59+
};
60+
61+
while let Ok(value) = rx.recv() {
62+
if let Err(e) = socket.progress(ProgressParams {
63+
token: token.clone(),
64+
value,
65+
}) {
66+
error!(error=%e, "failed to report parse progress");
67+
}
68+
}
69+
});
70+
4971
let file_registration_option = FileOperationRegistrationOptions {
5072
filters: file_operation_filers.clone(),
5173
};
@@ -54,7 +76,7 @@ impl LanguageServer for ProtoLanguageServer {
5476
if let Some(folders) = params.workspace_folders {
5577
for workspace in folders {
5678
info!("Workspace folder: {workspace:?}");
57-
self.state.add_workspace_folder(workspace)
79+
self.state.add_workspace_folder_async(workspace, tx.clone())
5880
}
5981
workspace_capabilities = Some(WorkspaceServerCapabilities {
6082
workspace_folders: Some(WorkspaceFoldersServerCapabilities {

‎src/parser/definition.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ impl ParsedTree {
2222
return;
2323
}
2424

25-
if !identifier.contains(".") {
25+
if !identifier.contains('.') {
2626
let locations: Vec<Location> = self
2727
.filter_nodes_from(n, NodeKind::is_userdefined)
2828
.into_iter()
@@ -41,7 +41,7 @@ impl ParsedTree {
4141
}
4242

4343
// Safety: identifier contains a .
44-
let (parent_identifier, remaining) = identifier.split_once(".").unwrap();
44+
let (parent_identifier, remaining) = identifier.split_once('.').unwrap();
4545
let child_node = self
4646
.filter_nodes_from(n, NodeKind::is_userdefined)
4747
.into_iter()

‎src/parser/hover.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl ParsedTree {
6464
return;
6565
}
6666

67-
if !identifier.contains(".") {
67+
if !identifier.contains('.') {
6868
let comments: Vec<MarkedString> = self
6969
.filter_nodes_from(n, NodeKind::is_userdefined)
7070
.into_iter()
@@ -78,7 +78,7 @@ impl ParsedTree {
7878
}
7979

8080
// Safety: identifier contains a .
81-
let (parent_identifier, remaining) = identifier.split_once(".").unwrap();
81+
let (parent_identifier, remaining) = identifier.split_once('.').unwrap();
8282
let child_node = self
8383
.filter_nodes_from(n, NodeKind::is_userdefined)
8484
.into_iter()

‎src/parser/mod.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use async_lsp::lsp_types::Url;
24
use tree_sitter::Tree;
35

@@ -12,9 +14,10 @@ pub struct ProtoParser {
1214
parser: tree_sitter::Parser,
1315
}
1416

17+
#[derive(Clone)]
1518
pub struct ParsedTree {
1619
pub uri: Url,
17-
tree: Tree,
20+
tree: Arc<Tree>,
1821
}
1922

2023
impl ProtoParser {
@@ -27,8 +30,9 @@ impl ProtoParser {
2730
}
2831

2932
pub fn parse(&mut self, uri: Url, contents: impl AsRef<[u8]>) -> Option<ParsedTree> {
30-
self.parser
31-
.parse(contents, None)
32-
.map(|t| ParsedTree { tree: t, uri })
33+
self.parser.parse(contents, None).map(|t| ParsedTree {
34+
tree: Arc::new(t),
35+
uri,
36+
})
3337
}
3438
}

‎src/state.rs

Lines changed: 125 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
use std::{collections::HashMap, fs::read_to_string};
1+
use std::{
2+
collections::HashMap,
3+
fs::read_to_string,
4+
sync::{mpsc::Sender, Arc, Mutex, MutexGuard, RwLock, RwLockWriteGuard},
5+
thread,
6+
};
27
use tracing::{error, info};
38

49
use async_lsp::lsp_types::{
5-
CompletionItem, CompletionItemKind, PublishDiagnosticsParams, Url, WorkspaceFolder,
10+
CompletionItem, CompletionItemKind, ProgressParamsValue, PublishDiagnosticsParams, Url,
11+
WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport,
12+
WorkspaceFolder,
613
};
714
use tree_sitter::Node;
815
use walkdir::WalkDir;
@@ -13,79 +20,146 @@ use crate::{
1320
};
1421

1522
pub struct ProtoLanguageState {
16-
documents: HashMap<Url, String>,
17-
pub trees: HashMap<Url, ParsedTree>,
18-
parser: ProtoParser,
23+
documents: Arc<RwLock<HashMap<Url, String>>>,
24+
trees: Arc<RwLock<HashMap<Url, ParsedTree>>>,
25+
parser: Arc<Mutex<ProtoParser>>,
1926
}
2027

2128
impl ProtoLanguageState {
2229
pub fn new() -> Self {
2330
ProtoLanguageState {
2431
documents: Default::default(),
2532
trees: Default::default(),
26-
parser: ProtoParser::new(),
33+
parser: Arc::new(Mutex::new(ProtoParser::new())),
2734
}
2835
}
2936

30-
pub fn get_content(&self, uri: &Url) -> &str {
37+
pub fn get_content(&self, uri: &Url) -> String {
3138
self.documents
39+
.read()
40+
.expect("poison")
3241
.get(uri)
33-
.map(|s| s.as_str())
42+
.map(|s| s.to_string())
3443
.unwrap_or_default()
3544
}
3645

37-
pub fn get_tree(&self, uri: &Url) -> Option<&ParsedTree> {
38-
self.trees.get(uri)
46+
pub fn get_tree(&self, uri: &Url) -> Option<ParsedTree> {
47+
self.trees.read().expect("poison").get(uri).cloned()
3948
}
4049

41-
pub fn get_trees_for_package(&self, package: &str) -> Vec<&ParsedTree> {
50+
pub fn get_trees_for_package(&self, package: &str) -> Vec<ParsedTree> {
4251
self.trees
52+
.read()
53+
.expect("poison")
4354
.values()
4455
.filter(|tree| {
4556
let content = self.get_content(&tree.uri);
4657
tree.get_package_name(content.as_bytes())
4758
.unwrap_or_default()
4859
== package
4960
})
61+
.map(ToOwned::to_owned)
5062
.collect()
5163
}
5264

53-
pub fn upsert_content(&mut self, uri: &Url, content: String) -> bool {
54-
if let Some(parsed) = self.parser.parse(uri.clone(), content.as_bytes()) {
55-
self.trees.insert(uri.clone(), parsed);
56-
self.documents.insert(uri.clone(), content);
65+
fn upsert_content_impl(
66+
mut parser: MutexGuard<ProtoParser>,
67+
uri: &Url,
68+
content: String,
69+
mut docs: RwLockWriteGuard<HashMap<Url, String>>,
70+
mut trees: RwLockWriteGuard<HashMap<Url, ParsedTree>>,
71+
) -> bool {
72+
if let Some(parsed) = parser.parse(uri.clone(), content.as_bytes()) {
73+
trees.insert(uri.clone(), parsed);
74+
docs.insert(uri.clone(), content);
5775
true
5876
} else {
59-
error!(uri=%uri, "failed to parse content");
6077
false
6178
}
6279
}
6380

64-
pub fn add_workspace_folder(&mut self, workspace: WorkspaceFolder) {
65-
for entry in WalkDir::new(workspace.uri.path())
66-
.into_iter()
67-
.filter_map(|e| e.ok())
68-
{
69-
let path = entry.path();
70-
if path.is_absolute() && path.is_file() {
71-
let Some(ext) = path.extension() else {
72-
continue;
73-
};
74-
75-
let Ok(content) = read_to_string(path) else {
76-
continue;
77-
};
78-
79-
let Ok(uri) = Url::from_file_path(path) else {
80-
continue;
81-
};
82-
83-
if ext == "proto" {
84-
let r = self.upsert_content(&uri, content);
85-
info!("workspace parse file: {}, result: {}", path.display(), r);
81+
pub fn upsert_content(&mut self, uri: &Url, content: String) -> bool {
82+
let parser = self.parser.lock().expect("poison");
83+
let tree = self.trees.write().expect("poison");
84+
let docs = self.documents.write().expect("poison");
85+
Self::upsert_content_impl(parser, uri, content, docs, tree)
86+
}
87+
88+
pub fn add_workspace_folder_async(
89+
&mut self,
90+
workspace: WorkspaceFolder,
91+
tx: Sender<ProgressParamsValue>,
92+
) {
93+
let parser = self.parser.clone();
94+
let tree = self.trees.clone();
95+
let docs = self.documents.clone();
96+
97+
let begin = ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin {
98+
title: String::from("indexing"),
99+
cancellable: Some(false),
100+
percentage: Some(0),
101+
..Default::default()
102+
}));
103+
104+
if let Err(e) = tx.send(begin) {
105+
error!(error=%e, "failed to send work begin progress");
106+
}
107+
108+
thread::spawn(move || {
109+
let files: Vec<_> = WalkDir::new(workspace.uri.path())
110+
.into_iter()
111+
.filter_map(|e| e.ok())
112+
.filter(|e| e.path().extension().is_some())
113+
.filter(|e| e.path().extension().unwrap() == "proto")
114+
.collect();
115+
116+
let total_files = files.len();
117+
let mut current = 0;
118+
119+
for file in files.into_iter() {
120+
let path = file.path();
121+
if path.is_absolute() && path.is_file() {
122+
let Ok(content) = read_to_string(path) else {
123+
continue;
124+
};
125+
126+
let Ok(uri) = Url::from_file_path(path) else {
127+
continue;
128+
};
129+
130+
Self::upsert_content_impl(
131+
parser.lock().expect("poison"),
132+
&uri,
133+
content,
134+
docs.write().expect("poison"),
135+
tree.write().expect("poison"),
136+
);
137+
138+
current += 1;
139+
140+
let report = ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
141+
WorkDoneProgressReport {
142+
cancellable: Some(false),
143+
message: Some(path.display().to_string()),
144+
percentage: Some((current * 100 / total_files) as u32),
145+
},
146+
));
147+
148+
if let Err(e) = tx.send(report) {
149+
error!(error=%e, "failed to send work report progress");
150+
}
86151
}
87152
}
88-
}
153+
let report =
154+
ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd {
155+
message: Some(String::from("completed")),
156+
}));
157+
158+
info!(len = total_files, "workspace file parsing completed");
159+
if let Err(e) = tx.send(report) {
160+
error!(error=%e, "failed to send work completed result");
161+
}
162+
});
89163
}
90164

91165
pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option<PublishDiagnosticsParams> {
@@ -96,20 +170,26 @@ impl ProtoLanguageState {
96170

97171
pub fn delete_file(&mut self, uri: &Url) {
98172
info!(uri=%uri, "deleting file");
99-
self.documents.remove(uri);
100-
self.trees.remove(uri);
173+
self.documents.write().expect("poison").remove(uri);
174+
self.trees.write().expect("poison").remove(uri);
101175
}
102176

103177
pub fn rename_file(&mut self, new_uri: &Url, old_uri: &Url) {
104178
info!(new_uri=%new_uri, old_uri=%new_uri, "renaming file");
105179

106-
if let Some(v) = self.documents.remove(old_uri) {
107-
self.documents.insert(new_uri.clone(), v);
180+
if let Some(v) = self.documents.write().expect("poison").remove(old_uri) {
181+
self.documents
182+
.write()
183+
.expect("poison")
184+
.insert(new_uri.clone(), v);
108185
}
109186

110-
if let Some(mut v) = self.trees.remove(old_uri) {
187+
if let Some(mut v) = self.trees.write().expect("poison").remove(old_uri) {
111188
v.uri = new_uri.clone();
112-
self.trees.insert(new_uri.clone(), v);
189+
self.trees
190+
.write()
191+
.expect("poison")
192+
.insert(new_uri.clone(), v);
113193
}
114194
}
115195

‎src/utils.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ fn is_first_lower_case(s: &&str) -> bool {
3030
}
3131

3232
pub fn is_inner_identifier(s: &str) -> bool {
33-
if !s.contains(".") {
33+
if !s.contains('.') {
3434
return false;
3535
}
36-
s.split(".").all(is_title_case)
36+
s.split('.').all(is_title_case)
3737
}
3838

3939
pub fn split_identifier_package(s: &str) -> (&str, &str) {
40-
if is_inner_identifier(s) || !s.contains(".") {
40+
if is_inner_identifier(s) || !s.contains('.') {
4141
return ("", s);
4242
}
4343

4444
let i = s
45-
.split(".")
45+
.split('.')
4646
.take_while(is_first_lower_case)
4747
.fold(0, |mut c, s| {
4848
if c != 0 {

0 commit comments

Comments
 (0)