diff options
Diffstat (limited to 'crates/ra_lsp_server/src')
-rw-r--r-- | crates/ra_lsp_server/src/diagnostics.rs | 85 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop.rs | 76 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 37 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/world.rs | 11 |
5 files changed, 140 insertions, 70 deletions
diff --git a/crates/ra_lsp_server/src/diagnostics.rs b/crates/ra_lsp_server/src/diagnostics.rs new file mode 100644 index 000000000..ea08bce24 --- /dev/null +++ b/crates/ra_lsp_server/src/diagnostics.rs | |||
@@ -0,0 +1,85 @@ | |||
1 | //! Book keeping for keeping diagnostics easily in sync with the client. | ||
2 | use lsp_types::{CodeActionOrCommand, Diagnostic, Range}; | ||
3 | use ra_ide::FileId; | ||
4 | use std::{collections::HashMap, sync::Arc}; | ||
5 | |||
6 | pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>; | ||
7 | |||
8 | #[derive(Debug, Default, Clone)] | ||
9 | pub struct DiagnosticCollection { | ||
10 | pub native: HashMap<FileId, Vec<Diagnostic>>, | ||
11 | pub check: HashMap<FileId, Vec<Diagnostic>>, | ||
12 | pub check_fixes: CheckFixes, | ||
13 | } | ||
14 | |||
15 | #[derive(Debug, Clone)] | ||
16 | pub struct Fix { | ||
17 | pub range: Range, | ||
18 | pub action: CodeActionOrCommand, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug)] | ||
22 | pub enum DiagnosticTask { | ||
23 | ClearCheck, | ||
24 | AddCheck(FileId, Diagnostic, Vec<CodeActionOrCommand>), | ||
25 | SetNative(FileId, Vec<Diagnostic>), | ||
26 | } | ||
27 | |||
28 | impl DiagnosticCollection { | ||
29 | pub fn clear_check(&mut self) -> Vec<FileId> { | ||
30 | Arc::make_mut(&mut self.check_fixes).clear(); | ||
31 | self.check.drain().map(|(key, _value)| key).collect() | ||
32 | } | ||
33 | |||
34 | pub fn add_check_diagnostic( | ||
35 | &mut self, | ||
36 | file_id: FileId, | ||
37 | diagnostic: Diagnostic, | ||
38 | fixes: Vec<CodeActionOrCommand>, | ||
39 | ) { | ||
40 | let diagnostics = self.check.entry(file_id).or_default(); | ||
41 | for existing_diagnostic in diagnostics.iter() { | ||
42 | if are_diagnostics_equal(&existing_diagnostic, &diagnostic) { | ||
43 | return; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | let check_fixes = Arc::make_mut(&mut self.check_fixes); | ||
48 | check_fixes | ||
49 | .entry(file_id) | ||
50 | .or_default() | ||
51 | .extend(fixes.into_iter().map(|action| Fix { range: diagnostic.range, action })); | ||
52 | diagnostics.push(diagnostic); | ||
53 | } | ||
54 | |||
55 | pub fn set_native_diagnostics(&mut self, file_id: FileId, diagnostics: Vec<Diagnostic>) { | ||
56 | self.native.insert(file_id, diagnostics); | ||
57 | } | ||
58 | |||
59 | pub fn diagnostics_for(&self, file_id: FileId) -> impl Iterator<Item = &Diagnostic> { | ||
60 | let native = self.native.get(&file_id).into_iter().flatten(); | ||
61 | let check = self.check.get(&file_id).into_iter().flatten(); | ||
62 | native.chain(check) | ||
63 | } | ||
64 | |||
65 | pub fn handle_task(&mut self, task: DiagnosticTask) -> Vec<FileId> { | ||
66 | match task { | ||
67 | DiagnosticTask::ClearCheck => self.clear_check(), | ||
68 | DiagnosticTask::AddCheck(file_id, diagnostic, fixes) => { | ||
69 | self.add_check_diagnostic(file_id, diagnostic, fixes); | ||
70 | vec![file_id] | ||
71 | } | ||
72 | DiagnosticTask::SetNative(file_id, diagnostics) => { | ||
73 | self.set_native_diagnostics(file_id, diagnostics); | ||
74 | vec![file_id] | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool { | ||
81 | left.source == right.source | ||
82 | && left.severity == right.severity | ||
83 | && left.range == right.range | ||
84 | && left.message == right.message | ||
85 | } | ||
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs index 2ca149fd5..1208c1343 100644 --- a/crates/ra_lsp_server/src/lib.rs +++ b/crates/ra_lsp_server/src/lib.rs | |||
@@ -29,6 +29,7 @@ mod markdown; | |||
29 | pub mod req; | 29 | pub mod req; |
30 | mod config; | 30 | mod config; |
31 | mod world; | 31 | mod world; |
32 | mod diagnostics; | ||
32 | 33 | ||
33 | pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; | 34 | pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; |
34 | pub use crate::{ | 35 | pub use crate::{ |
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 508fe08c0..12961ba37 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs | |||
@@ -17,16 +17,17 @@ use std::{ | |||
17 | use crossbeam_channel::{select, unbounded, RecvError, Sender}; | 17 | use crossbeam_channel::{select, unbounded, RecvError, Sender}; |
18 | use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; | 18 | use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; |
19 | use lsp_types::{ClientCapabilities, NumberOrString}; | 19 | use lsp_types::{ClientCapabilities, NumberOrString}; |
20 | use ra_cargo_watch::{CheckOptions, CheckTask}; | 20 | use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckTask}; |
21 | use ra_ide::{Canceled, FeatureFlags, FileId, LibraryData, SourceRootId}; | 21 | use ra_ide::{Canceled, FeatureFlags, FileId, LibraryData, SourceRootId}; |
22 | use ra_prof::profile; | 22 | use ra_prof::profile; |
23 | use ra_vfs::{VfsTask, Watch}; | 23 | use ra_vfs::{VfsFile, VfsTask, Watch}; |
24 | use relative_path::RelativePathBuf; | 24 | use relative_path::RelativePathBuf; |
25 | use rustc_hash::FxHashSet; | 25 | use rustc_hash::FxHashSet; |
26 | use serde::{de::DeserializeOwned, Serialize}; | 26 | use serde::{de::DeserializeOwned, Serialize}; |
27 | use threadpool::ThreadPool; | 27 | use threadpool::ThreadPool; |
28 | 28 | ||
29 | use crate::{ | 29 | use crate::{ |
30 | diagnostics::DiagnosticTask, | ||
30 | main_loop::{ | 31 | main_loop::{ |
31 | pending_requests::{PendingRequest, PendingRequests}, | 32 | pending_requests::{PendingRequest, PendingRequests}, |
32 | subscriptions::Subscriptions, | 33 | subscriptions::Subscriptions, |
@@ -254,6 +255,7 @@ pub fn main_loop( | |||
254 | enum Task { | 255 | enum Task { |
255 | Respond(Response), | 256 | Respond(Response), |
256 | Notify(Notification), | 257 | Notify(Notification), |
258 | Diagnostic(DiagnosticTask), | ||
257 | } | 259 | } |
258 | 260 | ||
259 | enum Event { | 261 | enum Event { |
@@ -359,7 +361,7 @@ fn loop_turn( | |||
359 | world_state.maybe_collect_garbage(); | 361 | world_state.maybe_collect_garbage(); |
360 | loop_state.in_flight_libraries -= 1; | 362 | loop_state.in_flight_libraries -= 1; |
361 | } | 363 | } |
362 | Event::CheckWatcher(task) => on_check_task(pool, task, world_state, task_sender)?, | 364 | Event::CheckWatcher(task) => on_check_task(task, world_state, task_sender)?, |
363 | Event::Msg(msg) => match msg { | 365 | Event::Msg(msg) => match msg { |
364 | Message::Request(req) => on_request( | 366 | Message::Request(req) => on_request( |
365 | world_state, | 367 | world_state, |
@@ -464,6 +466,7 @@ fn on_task( | |||
464 | Task::Notify(n) => { | 466 | Task::Notify(n) => { |
465 | msg_sender.send(n.into()).unwrap(); | 467 | msg_sender.send(n.into()).unwrap(); |
466 | } | 468 | } |
469 | Task::Diagnostic(task) => on_diagnostic_task(task, msg_sender, state), | ||
467 | } | 470 | } |
468 | } | 471 | } |
469 | 472 | ||
@@ -621,23 +624,26 @@ fn on_notification( | |||
621 | } | 624 | } |
622 | 625 | ||
623 | fn on_check_task( | 626 | fn on_check_task( |
624 | pool: &ThreadPool, | ||
625 | task: CheckTask, | 627 | task: CheckTask, |
626 | world_state: &mut WorldState, | 628 | world_state: &mut WorldState, |
627 | task_sender: &Sender<Task>, | 629 | task_sender: &Sender<Task>, |
628 | ) -> Result<()> { | 630 | ) -> Result<()> { |
629 | let urls = match task { | 631 | match task { |
630 | CheckTask::ClearDiagnostics => { | 632 | CheckTask::ClearDiagnostics => { |
631 | let state = Arc::get_mut(&mut world_state.check_watcher.state) | 633 | task_sender.send(Task::Diagnostic(DiagnosticTask::ClearCheck))?; |
632 | .expect("couldn't get check watcher state as mutable"); | ||
633 | state.clear() | ||
634 | } | 634 | } |
635 | 635 | ||
636 | CheckTask::AddDiagnostic(url, diagnostic) => { | 636 | CheckTask::AddDiagnostic { url, diagnostic, fixes } => { |
637 | let state = Arc::get_mut(&mut world_state.check_watcher.state) | 637 | let path = url.to_file_path().map_err(|()| format!("invalid uri: {}", url))?; |
638 | .expect("couldn't get check watcher state as mutable"); | 638 | let file_id = world_state |
639 | state.add_diagnostic_with_fixes(url.clone(), diagnostic); | 639 | .vfs |
640 | vec![url] | 640 | .read() |
641 | .path2file(&path) | ||
642 | .map(|it| FileId(it.0)) | ||
643 | .ok_or_else(|| format!("unknown file: {}", path.to_string_lossy()))?; | ||
644 | |||
645 | task_sender | ||
646 | .send(Task::Diagnostic(DiagnosticTask::AddCheck(file_id, diagnostic, fixes)))?; | ||
641 | } | 647 | } |
642 | 648 | ||
643 | CheckTask::Status(progress) => { | 649 | CheckTask::Status(progress) => { |
@@ -647,31 +653,30 @@ fn on_check_task( | |||
647 | }; | 653 | }; |
648 | let not = notification_new::<req::Progress>(params); | 654 | let not = notification_new::<req::Progress>(params); |
649 | task_sender.send(Task::Notify(not)).unwrap(); | 655 | task_sender.send(Task::Notify(not)).unwrap(); |
650 | Vec::new() | ||
651 | } | 656 | } |
652 | }; | 657 | }; |
653 | 658 | ||
654 | let subscriptions = urls | 659 | Ok(()) |
655 | .into_iter() | 660 | } |
656 | .map(|url| { | ||
657 | let path = url.to_file_path().map_err(|()| format!("invalid uri: {}", url))?; | ||
658 | Ok(world_state.vfs.read().path2file(&path).map(|it| FileId(it.0))) | ||
659 | }) | ||
660 | .filter_map(|res| res.transpose()) | ||
661 | .collect::<Result<Vec<_>>>()?; | ||
662 | 661 | ||
663 | // We manually send a diagnostic update when the watcher asks | 662 | fn on_diagnostic_task(task: DiagnosticTask, msg_sender: &Sender<Message>, state: &mut WorldState) { |
664 | // us to, to avoid the issue of having to change the file to | 663 | let subscriptions = state.diagnostics.handle_task(task); |
665 | // receive updated diagnostics. | ||
666 | update_file_notifications_on_threadpool( | ||
667 | pool, | ||
668 | world_state.snapshot(), | ||
669 | false, | ||
670 | task_sender.clone(), | ||
671 | subscriptions, | ||
672 | ); | ||
673 | 664 | ||
674 | Ok(()) | 665 | for file_id in subscriptions { |
666 | let path = state.vfs.read().file2path(VfsFile(file_id.0)); | ||
667 | let uri = match url_from_path_with_drive_lowercasing(&path) { | ||
668 | Ok(uri) => uri, | ||
669 | Err(err) => { | ||
670 | log::error!("Couldn't convert path to url ({}): {:?}", err, path.to_string_lossy()); | ||
671 | continue; | ||
672 | } | ||
673 | }; | ||
674 | |||
675 | let diagnostics = state.diagnostics.diagnostics_for(file_id).cloned().collect(); | ||
676 | let params = req::PublishDiagnosticsParams { uri, diagnostics, version: None }; | ||
677 | let not = notification_new::<req::PublishDiagnostics>(params); | ||
678 | msg_sender.send(not.into()).unwrap(); | ||
679 | } | ||
675 | } | 680 | } |
676 | 681 | ||
677 | struct PoolDispatcher<'a> { | 682 | struct PoolDispatcher<'a> { |
@@ -819,9 +824,8 @@ fn update_file_notifications_on_threadpool( | |||
819 | log::error!("failed to compute diagnostics: {:?}", e); | 824 | log::error!("failed to compute diagnostics: {:?}", e); |
820 | } | 825 | } |
821 | } | 826 | } |
822 | Ok(params) => { | 827 | Ok(task) => { |
823 | let not = notification_new::<req::PublishDiagnostics>(params); | 828 | task_sender.send(Task::Diagnostic(task)).unwrap(); |
824 | task_sender.send(Task::Notify(not)).unwrap(); | ||
825 | } | 829 | } |
826 | } | 830 | } |
827 | } | 831 | } |
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 9aa1e7eea..839bca342 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs | |||
@@ -33,6 +33,7 @@ use crate::{ | |||
33 | to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, | 33 | to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, |
34 | TryConvWithToVec, | 34 | TryConvWithToVec, |
35 | }, | 35 | }, |
36 | diagnostics::DiagnosticTask, | ||
36 | req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind}, | 37 | req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind}, |
37 | world::WorldSnapshot, | 38 | world::WorldSnapshot, |
38 | LspError, Result, | 39 | LspError, Result, |
@@ -656,6 +657,7 @@ pub fn handle_code_action( | |||
656 | .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some()) | 657 | .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some()) |
657 | .map(|(_range, fix)| fix); | 658 | .map(|(_range, fix)| fix); |
658 | 659 | ||
660 | // TODO: When done, we won't need this, only the one pulling from world.diagnostics | ||
659 | for source_edit in fixes_from_diagnostics { | 661 | for source_edit in fixes_from_diagnostics { |
660 | let title = source_edit.label.clone(); | 662 | let title = source_edit.label.clone(); |
661 | let edit = source_edit.try_conv_with(&world)?; | 663 | let edit = source_edit.try_conv_with(&world)?; |
@@ -676,28 +678,12 @@ pub fn handle_code_action( | |||
676 | res.push(action.into()); | 678 | res.push(action.into()); |
677 | } | 679 | } |
678 | 680 | ||
679 | for fix in world.check_watcher.fixes_for(¶ms.text_document.uri).into_iter().flatten() { | 681 | for fix in world.check_fixes.get(&file_id).into_iter().flatten() { |
680 | let fix_range = fix.location.range.conv_with(&line_index); | 682 | let fix_range = fix.range.conv_with(&line_index); |
681 | if fix_range.intersection(&range).is_none() { | 683 | if fix_range.intersection(&range).is_none() { |
682 | continue; | 684 | continue; |
683 | } | 685 | } |
684 | 686 | res.push(fix.action.clone()); | |
685 | let edit = { | ||
686 | let edits = vec![TextEdit::new(fix.location.range, fix.replacement.clone())]; | ||
687 | let mut edit_map = std::collections::HashMap::new(); | ||
688 | edit_map.insert(fix.location.uri.clone(), edits); | ||
689 | WorkspaceEdit::new(edit_map) | ||
690 | }; | ||
691 | |||
692 | let action = CodeAction { | ||
693 | title: fix.title.clone(), | ||
694 | kind: Some("quickfix".to_string()), | ||
695 | diagnostics: Some(fix.diagnostics.clone()), | ||
696 | edit: Some(edit), | ||
697 | command: None, | ||
698 | is_preferred: None, | ||
699 | }; | ||
700 | res.push(action.into()); | ||
701 | } | 687 | } |
702 | 688 | ||
703 | for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() { | 689 | for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() { |
@@ -875,14 +861,10 @@ pub fn handle_document_highlight( | |||
875 | )) | 861 | )) |
876 | } | 862 | } |
877 | 863 | ||
878 | pub fn publish_diagnostics( | 864 | pub fn publish_diagnostics(world: &WorldSnapshot, file_id: FileId) -> Result<DiagnosticTask> { |
879 | world: &WorldSnapshot, | ||
880 | file_id: FileId, | ||
881 | ) -> Result<req::PublishDiagnosticsParams> { | ||
882 | let _p = profile("publish_diagnostics"); | 865 | let _p = profile("publish_diagnostics"); |
883 | let uri = world.file_id_to_uri(file_id)?; | ||
884 | let line_index = world.analysis().file_line_index(file_id)?; | 866 | let line_index = world.analysis().file_line_index(file_id)?; |
885 | let mut diagnostics: Vec<Diagnostic> = world | 867 | let diagnostics: Vec<Diagnostic> = world |
886 | .analysis() | 868 | .analysis() |
887 | .diagnostics(file_id)? | 869 | .diagnostics(file_id)? |
888 | .into_iter() | 870 | .into_iter() |
@@ -896,10 +878,7 @@ pub fn publish_diagnostics( | |||
896 | tags: None, | 878 | tags: None, |
897 | }) | 879 | }) |
898 | .collect(); | 880 | .collect(); |
899 | if let Some(check_diags) = world.check_watcher.diagnostics_for(&uri) { | 881 | Ok(DiagnosticTask::SetNative(file_id, diagnostics)) |
900 | diagnostics.extend(check_diags.iter().cloned()); | ||
901 | } | ||
902 | Ok(req::PublishDiagnosticsParams { uri, diagnostics, version: None }) | ||
903 | } | 882 | } |
904 | 883 | ||
905 | pub fn publish_decorations( | 884 | pub fn publish_decorations( |
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs index 3059ef9ec..1ee02b47c 100644 --- a/crates/ra_lsp_server/src/world.rs +++ b/crates/ra_lsp_server/src/world.rs | |||
@@ -12,9 +12,7 @@ use crossbeam_channel::{unbounded, Receiver}; | |||
12 | use lsp_server::ErrorCode; | 12 | use lsp_server::ErrorCode; |
13 | use lsp_types::Url; | 13 | use lsp_types::Url; |
14 | use parking_lot::RwLock; | 14 | use parking_lot::RwLock; |
15 | use ra_cargo_watch::{ | 15 | use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckWatcher}; |
16 | url_from_path_with_drive_lowercasing, CheckOptions, CheckState, CheckWatcher, | ||
17 | }; | ||
18 | use ra_ide::{ | 16 | use ra_ide::{ |
19 | Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData, | 17 | Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData, |
20 | SourceRootId, | 18 | SourceRootId, |
@@ -25,6 +23,7 @@ use ra_vfs_glob::{Glob, RustPackageFilterBuilder}; | |||
25 | use relative_path::RelativePathBuf; | 23 | use relative_path::RelativePathBuf; |
26 | 24 | ||
27 | use crate::{ | 25 | use crate::{ |
26 | diagnostics::{CheckFixes, DiagnosticCollection}, | ||
28 | main_loop::pending_requests::{CompletedRequest, LatestRequests}, | 27 | main_loop::pending_requests::{CompletedRequest, LatestRequests}, |
29 | LspError, Result, | 28 | LspError, Result, |
30 | }; | 29 | }; |
@@ -55,6 +54,7 @@ pub struct WorldState { | |||
55 | pub task_receiver: Receiver<VfsTask>, | 54 | pub task_receiver: Receiver<VfsTask>, |
56 | pub latest_requests: Arc<RwLock<LatestRequests>>, | 55 | pub latest_requests: Arc<RwLock<LatestRequests>>, |
57 | pub check_watcher: CheckWatcher, | 56 | pub check_watcher: CheckWatcher, |
57 | pub diagnostics: DiagnosticCollection, | ||
58 | } | 58 | } |
59 | 59 | ||
60 | /// An immutable snapshot of the world's state at a point in time. | 60 | /// An immutable snapshot of the world's state at a point in time. |
@@ -63,7 +63,7 @@ pub struct WorldSnapshot { | |||
63 | pub workspaces: Arc<Vec<ProjectWorkspace>>, | 63 | pub workspaces: Arc<Vec<ProjectWorkspace>>, |
64 | pub analysis: Analysis, | 64 | pub analysis: Analysis, |
65 | pub latest_requests: Arc<RwLock<LatestRequests>>, | 65 | pub latest_requests: Arc<RwLock<LatestRequests>>, |
66 | pub check_watcher: CheckState, | 66 | pub check_fixes: CheckFixes, |
67 | vfs: Arc<RwLock<Vfs>>, | 67 | vfs: Arc<RwLock<Vfs>>, |
68 | } | 68 | } |
69 | 69 | ||
@@ -159,6 +159,7 @@ impl WorldState { | |||
159 | task_receiver, | 159 | task_receiver, |
160 | latest_requests: Default::default(), | 160 | latest_requests: Default::default(), |
161 | check_watcher, | 161 | check_watcher, |
162 | diagnostics: Default::default(), | ||
162 | } | 163 | } |
163 | } | 164 | } |
164 | 165 | ||
@@ -220,7 +221,7 @@ impl WorldState { | |||
220 | analysis: self.analysis_host.analysis(), | 221 | analysis: self.analysis_host.analysis(), |
221 | vfs: Arc::clone(&self.vfs), | 222 | vfs: Arc::clone(&self.vfs), |
222 | latest_requests: Arc::clone(&self.latest_requests), | 223 | latest_requests: Arc::clone(&self.latest_requests), |
223 | check_watcher: (*self.check_watcher.state).clone(), | 224 | check_fixes: Arc::clone(&self.diagnostics.check_fixes), |
224 | } | 225 | } |
225 | } | 226 | } |
226 | 227 | ||