Skip to content

Commit d220181

Browse files
feat: Add file watcher (#20)
1 parent 872ce18 commit d220181

File tree

6 files changed

+110
-26
lines changed

6 files changed

+110
-26
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ indoc = "2.0.6"
1414
itertools = "0.14.0"
1515
ropey = "1.6.1"
1616
serde = "1.0.219"
17+
serde_json = "1.0.140"
1718
similar = { version = "2.7.0", features = ["inline"] }
1819
thiserror = "2.0.12"
1920
tokio = { version = "1.45.1", features = [

crates/roughly/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ tracing-subscriber.workspace = true
2020
tracing-tree.workspace = true
2121
tree-sitter.workspace = true
2222
tree-sitter-r.workspace = true
23+
serde_json.workspace = true
2324

2425
# async-lsp
2526
async-lsp.workspace = true

crates/roughly/src/server.rs

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ use {
77
index::{self, IndexError},
88
lsp_types::{
99
CompletionOptions, CompletionParams, CompletionResponse, DidChangeTextDocumentParams,
10+
DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
1011
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
1112
DocumentFormattingParams, DocumentRangeFormattingParams, DocumentSymbol,
12-
DocumentSymbolParams, DocumentSymbolResponse, InitializeParams, InitializeResult,
13-
MessageType, OneOf, Position, PublishDiagnosticsParams, Range, SaveOptions,
14-
ServerCapabilities, ServerInfo, ShowMessageParams, TextDocumentSyncCapability,
15-
TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions, TextEdit,
16-
WorkspaceSymbolParams, WorkspaceSymbolResponse,
13+
DocumentSymbolParams, DocumentSymbolResponse, FileChangeType, FileSystemWatcher,
14+
GlobPattern, InitializeParams, InitializeResult, InitializedParams, MessageType, OneOf,
15+
Position, PublishDiagnosticsParams, Range, Registration, RegistrationParams,
16+
RelativePattern, SaveOptions, ServerCapabilities, ServerInfo, ShowMessageParams,
17+
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
18+
TextDocumentSyncSaveOptions, TextEdit, Url, WorkspaceSymbolParams,
19+
WorkspaceSymbolResponse,
20+
notification::{DidChangeWatchedFiles, Notification},
1721
},
1822
tree, utils,
1923
},
@@ -78,7 +82,9 @@ struct ServerState {
7882
experimental: bool,
7983
base_path: PathBuf,
8084
document_map: HashMap<PathBuf, Document>,
85+
/// stores symbolds for all other files
8186
document_symbols: HashMap<PathBuf, Vec<DocumentSymbol>>,
87+
/// stores index for all files in R/ folder
8288
workspace_symbols: HashMap<PathBuf, Vec<DocumentSymbol>>,
8389
parser: Parser,
8490
}
@@ -154,6 +160,46 @@ impl LanguageServer for ServerState {
154160
}))
155161
}
156162

163+
fn initialized(&mut self, _: InitializedParams) -> ControlFlow<async_lsp::Result<()>> {
164+
// TODO: consider to negotiate client capabilities
165+
// see: https://github.com/oxalica/nil/blob/870a4b1b5f/crates/nil/src/capabilities.rs
166+
let params = RegistrationParams {
167+
registrations: vec![Registration {
168+
id: DidChangeWatchedFiles::METHOD.into(),
169+
method: DidChangeWatchedFiles::METHOD.into(),
170+
register_options: Some(
171+
serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
172+
watchers: vec![FileSystemWatcher {
173+
glob_pattern: GlobPattern::Relative(RelativePattern {
174+
base_uri: OneOf::Right(
175+
Url::from_file_path(&self.base_path).unwrap(),
176+
),
177+
pattern: "*.[rR]".into(),
178+
}),
179+
kind: None,
180+
}],
181+
})
182+
.unwrap(),
183+
),
184+
}],
185+
};
186+
187+
let mut client = self.client.clone();
188+
tokio::spawn(async move {
189+
if let Err(err) = client.register_capability(params).await {
190+
client
191+
.show_message(ShowMessageParams {
192+
typ: MessageType::ERROR,
193+
message: format!("failed to watch R files: {err:#}"),
194+
})
195+
.unwrap();
196+
}
197+
tracing::info!("registered file watching for R files");
198+
});
199+
200+
ControlFlow::Continue(())
201+
}
202+
157203
//
158204
// TEXT SYNC
159205
//
@@ -177,10 +223,13 @@ impl LanguageServer for ServerState {
177223
diagnostics::Config::from_config(self.config, self.experimental),
178224
);
179225

180-
if !path.starts_with(&self.base_path) {
181-
let symbols = index::index(tree.root_node(), &rope, false);
226+
let symbols = index::index(tree.root_node(), &rope, false);
227+
if path.starts_with(&self.base_path) {
228+
// note: we need to insert into workspace in case a new file is created
229+
self.workspace_symbols.insert(path.clone(), symbols);
230+
} else {
182231
self.document_symbols.insert(path.clone(), symbols);
183-
};
232+
}
184233

185234
self.document_map.insert(path, Document { rope, tree });
186235

@@ -343,6 +392,35 @@ impl LanguageServer for ServerState {
343392
ControlFlow::Continue(())
344393
}
345394

395+
fn did_change_watched_files(
396+
&mut self,
397+
params: DidChangeWatchedFilesParams,
398+
) -> ControlFlow<async_lsp::Result<()>> {
399+
for change in params.changes {
400+
let uri = change.uri;
401+
let typ = change.typ;
402+
let path = uri.to_file_path().unwrap();
403+
404+
tracing::info!(?path, ?typ, "watched file changed");
405+
406+
if path.starts_with(&self.base_path) {
407+
match change.typ {
408+
FileChangeType::CREATED | FileChangeType::CHANGED => {
409+
// note: potential race condition if the user already has the file open and begins editing immediately.
410+
let symbols = index::index_file(&path, &mut self.parser);
411+
self.workspace_symbols.insert(path.clone(), symbols);
412+
}
413+
FileChangeType::DELETED => {
414+
self.workspace_symbols.remove(&path);
415+
}
416+
_ => unreachable!(),
417+
}
418+
}
419+
}
420+
421+
ControlFlow::Continue(())
422+
}
423+
346424
//
347425
// COMPLETION
348426
//

crates/typing/README.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@ x: numeric <- 4
3434
names: character[] <- c("Alice", "Bob")
3535
```
3636

37-
However, this syntax would require major changes to R's parser and is not practical for early experimentation.
37+
However, this syntax would require major changes to R's parser which is not practical for early experimentation.
3838

3939
### Option C: JSDoc-style comments
4040

41-
JSDoc is an alternative syntax for TypeScript that uses the same underlying type system and type checker. In this approach, types are provided in comments, making it practical for prototyping and not requiring changes to R itself.
41+
[JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) is an alternative syntax for TypeScript that uses the same underlying type system and type checker. In this approach, types are provided in comments, so no changes to R itself are required.
4242

4343
```r
4444
#: @param items character[] # character vector input
45-
#: @param count numeric # scalar numeric input (expected length)
46-
#: @return logical # returns a scalar logical
45+
#: @param count numeric # scalar numeric input (expected length)
46+
#: @return logical # returns a scalar logical
4747
has_expected_length <- function(items, count) {
4848
length(items) == count
4949
}
@@ -52,7 +52,7 @@ x <- 4 #: numeric
5252
names <- c("Alice", "Bob") #: character[]
5353
```
5454

55-
> [!INFO]
55+
> [!NOTE]
5656
> To distinguish from roxygen, use `#:` as the comment prefix:
5757
5858
This is the option we use for now, as it enables experimentation without modifying R itself.
@@ -82,9 +82,10 @@ Vectors and lists can have arbitrary length, but some constructs (like `if`) req
8282

8383
- Support for:
8484
- **Scalar** (length-one vector)
85-
- **Array** (vector + array attributes)
86-
- **Record** (lists of fixed size with names)
87-
- **Tuple** (lists of fixed size, no names)
85+
- **Array** (arbitrary length vector)
86+
- **List** (arbitrary length, homogenous list)
87+
- **Record** (fixed length, heterogeneous list with named fields)
88+
- **Tuple** (fixed length, heterogeneous list with positional fields)
8889
- **Unknown** (type could not be inferred)
8990
- **Any** (explicit opt-out of type checking)
9091
- Future:
@@ -250,11 +251,11 @@ Err <- function(error) {
250251
error |> with_tag("Err")
251252
}
252253

253-
#: @param x [Ok<T>, Err <E>]
254+
#: @param x [Ok <T>, Err <E>]
254255
#: @return logical
255256
is_ok <- \(x) tag(x) == "Ok"
256257

257-
#: @param x [Ok<T>, Err <E>]
258+
#: @param x [Ok <T>, Err <E>]
258259
#: @return logical
259260
is_err <- \(x) tag(x) == "Err"
260261
```
@@ -263,23 +264,23 @@ is_err <- \(x) tag(x) == "Err"
263264

264265
A key benefit of union types is that the type checker can enforce exhaustiveness in pattern matching or switch statements. This means that all possible variants of a union type must be handled explicitly, preventing bugs from unhandled cases.
265266

266-
For example, consider a `Maybe` type with two variants: `Just` and `Nothing`:
267+
For example, suppose you have a function parameter that can be either `Just` or `Nothing`, representing a tagged union with two possible variants:
267268

268269
```r
269-
#: @param result [Just numeric, Nothing]
270+
#: @param maybe [Just numeric, Nothing]
270271
#: @return numeric
271-
process_result <- function(result) {
272+
process_maybe <- function(maybe) {
272273
switch(
273-
tag(result),
274-
Just = get_value(result),
274+
tag(maybe),
275+
Just = process(maybe),
275276
Nothing = 0
276-
# If a new variant is added to the Maybe type, the type checker will report an error
277-
# if it is not handled here.
277+
# If a new variant is added to the `maybe` parameter,
278+
# the type checker will report an error if it is not handled here.
278279
)
279280
}
280281
```
281282

282-
If you later extend `Maybe` with a new variant (e.g., `Unknown`), the type checker will require you to update all switch statements that handle `Maybe` to cover the new case, ensuring your code remains correct and robust.
283+
If you later extend the union with a new variant (e.g., `Unknown`), the type checker will require you to update all switch statements that handle `maybe` to cover the new case, ensuring your code remains correct and robust.
283284

284285
## Open Questions
285286

docs/content/development.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ See https://github.com/r-lib/tree-sitter-r/issues/166
188188
* https://github.com/IWANABETHATGUY/tower-lsp-boilerplate
189189
* https://github.com/TenStrings/glicol-lsp/blob/77e97d9c687dc5d66871ad5ec91b6f049de2b8e8/src/main.rs#L16
190190
* https://github.com/jfecher/ante/blob/5f7446375bc1c6c94b44a44bfb89777c1437aaf5/ante-ls/src/main.rs#L163
191+
* async_lsp
192+
* https://github.com/oxalica/nil
191193

192194
### Formatting
193195

0 commit comments

Comments
 (0)