aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmil Lauridsen <[email protected]>2019-11-18 17:02:28 +0000
committerEmil Lauridsen <[email protected]>2019-11-19 16:23:50 +0000
commitdadad36bb9770f9b13ed84bc219ea0168a7a5bf1 (patch)
tree00051540da204b4294501f3c56960975177ae502
parentc24ee0990486b04723534f072d7a58e829bbd1bd (diff)
Move type inlay hint truncation to language server
This commit implements a general truncation framework for HirFormatter that keeps track of how much has been output so far. This information can then be used to perform truncation inside the language server, instead of relying on the client. Initial support is implemented for truncating types hints using the maxInlayHintLength server config option. The existing solution in the VSCode extension has been removed in favor of letting the server truncate type hints.
-rw-r--r--crates/ra_hir/src/ty.rs20
-rw-r--r--crates/ra_hir/src/ty/display.rs43
-rw-r--r--crates/ra_ide_api/src/inlay_hints.rs37
-rw-r--r--crates/ra_ide_api/src/lib.rs10
-rw-r--r--crates/ra_lsp_server/src/config.rs3
-rw-r--r--crates/ra_lsp_server/src/main_loop.rs1
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs2
-rw-r--r--crates/ra_lsp_server/src/world.rs1
-rw-r--r--editors/code/src/commands/inlay_hints.ts14
-rw-r--r--editors/code/src/server.ts1
10 files changed, 97 insertions, 35 deletions
diff --git a/crates/ra_hir/src/ty.rs b/crates/ra_hir/src/ty.rs
index b7f50b714..776613c7c 100644
--- a/crates/ra_hir/src/ty.rs
+++ b/crates/ra_hir/src/ty.rs
@@ -800,6 +800,10 @@ impl HirDisplay for &Ty {
800 800
801impl HirDisplay for ApplicationTy { 801impl HirDisplay for ApplicationTy {
802 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result { 802 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result {
803 if f.should_truncate() {
804 return write!(f, "…");
805 }
806
803 match self.ctor { 807 match self.ctor {
804 TypeCtor::Bool => write!(f, "bool")?, 808 TypeCtor::Bool => write!(f, "bool")?,
805 TypeCtor::Char => write!(f, "char")?, 809 TypeCtor::Char => write!(f, "char")?,
@@ -901,6 +905,10 @@ impl HirDisplay for ApplicationTy {
901 905
902impl HirDisplay for ProjectionTy { 906impl HirDisplay for ProjectionTy {
903 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result { 907 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result {
908 if f.should_truncate() {
909 return write!(f, "…");
910 }
911
904 let trait_name = self 912 let trait_name = self
905 .associated_ty 913 .associated_ty
906 .parent_trait(f.db) 914 .parent_trait(f.db)
@@ -919,6 +927,10 @@ impl HirDisplay for ProjectionTy {
919 927
920impl HirDisplay for Ty { 928impl HirDisplay for Ty {
921 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result { 929 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result {
930 if f.should_truncate() {
931 return write!(f, "…");
932 }
933
922 match self { 934 match self {
923 Ty::Apply(a_ty) => a_ty.hir_fmt(f)?, 935 Ty::Apply(a_ty) => a_ty.hir_fmt(f)?,
924 Ty::Projection(p_ty) => p_ty.hir_fmt(f)?, 936 Ty::Projection(p_ty) => p_ty.hir_fmt(f)?,
@@ -1001,6 +1013,10 @@ impl HirDisplay for Ty {
1001 1013
1002impl TraitRef { 1014impl TraitRef {
1003 fn hir_fmt_ext(&self, f: &mut HirFormatter<impl HirDatabase>, use_as: bool) -> fmt::Result { 1015 fn hir_fmt_ext(&self, f: &mut HirFormatter<impl HirDatabase>, use_as: bool) -> fmt::Result {
1016 if f.should_truncate() {
1017 return write!(f, "…");
1018 }
1019
1004 self.substs[0].hir_fmt(f)?; 1020 self.substs[0].hir_fmt(f)?;
1005 if use_as { 1021 if use_as {
1006 write!(f, " as ")?; 1022 write!(f, " as ")?;
@@ -1031,6 +1047,10 @@ impl HirDisplay for &GenericPredicate {
1031 1047
1032impl HirDisplay for GenericPredicate { 1048impl HirDisplay for GenericPredicate {
1033 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result { 1049 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result {
1050 if f.should_truncate() {
1051 return write!(f, "…");
1052 }
1053
1034 match self { 1054 match self {
1035 GenericPredicate::Implemented(trait_ref) => trait_ref.hir_fmt(f)?, 1055 GenericPredicate::Implemented(trait_ref) => trait_ref.hir_fmt(f)?,
1036 GenericPredicate::Projection(projection_pred) => { 1056 GenericPredicate::Projection(projection_pred) => {
diff --git a/crates/ra_hir/src/ty/display.rs b/crates/ra_hir/src/ty/display.rs
index 7910429d7..9bb3ece6c 100644
--- a/crates/ra_hir/src/ty/display.rs
+++ b/crates/ra_hir/src/ty/display.rs
@@ -7,15 +7,30 @@ use crate::db::HirDatabase;
7pub struct HirFormatter<'a, 'b, DB> { 7pub struct HirFormatter<'a, 'b, DB> {
8 pub db: &'a DB, 8 pub db: &'a DB,
9 fmt: &'a mut fmt::Formatter<'b>, 9 fmt: &'a mut fmt::Formatter<'b>,
10 buf: String,
11 curr_size: usize,
12 max_size: Option<usize>,
10} 13}
11 14
12pub trait HirDisplay { 15pub trait HirDisplay {
13 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result; 16 fn hir_fmt(&self, f: &mut HirFormatter<impl HirDatabase>) -> fmt::Result;
17
14 fn display<'a, DB>(&'a self, db: &'a DB) -> HirDisplayWrapper<'a, DB, Self> 18 fn display<'a, DB>(&'a self, db: &'a DB) -> HirDisplayWrapper<'a, DB, Self>
15 where 19 where
16 Self: Sized, 20 Self: Sized,
17 { 21 {
18 HirDisplayWrapper(db, self) 22 HirDisplayWrapper(db, self, None)
23 }
24
25 fn display_truncated<'a, DB>(
26 &'a self,
27 db: &'a DB,
28 max_size: Option<usize>,
29 ) -> HirDisplayWrapper<'a, DB, Self>
30 where
31 Self: Sized,
32 {
33 HirDisplayWrapper(db, self, max_size)
19 } 34 }
20} 35}
21 36
@@ -41,11 +56,25 @@ where
41 56
42 /// This allows using the `write!` macro directly with a `HirFormatter`. 57 /// This allows using the `write!` macro directly with a `HirFormatter`.
43 pub fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { 58 pub fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result {
44 fmt::write(self.fmt, args) 59 // We write to a buffer first to track output size
60 self.buf.clear();
61 fmt::write(&mut self.buf, args)?;
62 self.curr_size += self.buf.len();
63
64 // Then we write to the internal formatter from the buffer
65 self.fmt.write_str(&self.buf)
66 }
67
68 pub fn should_truncate(&self) -> bool {
69 if let Some(max_size) = self.max_size {
70 self.curr_size >= max_size
71 } else {
72 false
73 }
45 } 74 }
46} 75}
47 76
48pub struct HirDisplayWrapper<'a, DB, T>(&'a DB, &'a T); 77pub struct HirDisplayWrapper<'a, DB, T>(&'a DB, &'a T, Option<usize>);
49 78
50impl<'a, DB, T> fmt::Display for HirDisplayWrapper<'a, DB, T> 79impl<'a, DB, T> fmt::Display for HirDisplayWrapper<'a, DB, T>
51where 80where
@@ -53,6 +82,12 @@ where
53 T: HirDisplay, 82 T: HirDisplay,
54{ 83{
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 84 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 self.1.hir_fmt(&mut HirFormatter { db: self.0, fmt: f }) 85 self.1.hir_fmt(&mut HirFormatter {
86 db: self.0,
87 fmt: f,
88 buf: String::with_capacity(20),
89 curr_size: 0,
90 max_size: self.2,
91 })
57 } 92 }
58} 93}
diff --git a/crates/ra_ide_api/src/inlay_hints.rs b/crates/ra_ide_api/src/inlay_hints.rs
index 0cd959848..6cd492b2a 100644
--- a/crates/ra_ide_api/src/inlay_hints.rs
+++ b/crates/ra_ide_api/src/inlay_hints.rs
@@ -19,10 +19,15 @@ pub struct InlayHint {
19 pub label: SmolStr, 19 pub label: SmolStr,
20} 20}
21 21
22pub(crate) fn inlay_hints(db: &RootDatabase, file_id: FileId, file: &SourceFile) -> Vec<InlayHint> { 22pub(crate) fn inlay_hints(
23 db: &RootDatabase,
24 file_id: FileId,
25 file: &SourceFile,
26 max_inlay_hint_length: Option<usize>,
27) -> Vec<InlayHint> {
23 file.syntax() 28 file.syntax()
24 .descendants() 29 .descendants()
25 .map(|node| get_inlay_hints(db, file_id, &node).unwrap_or_default()) 30 .map(|node| get_inlay_hints(db, file_id, &node, max_inlay_hint_length).unwrap_or_default())
26 .flatten() 31 .flatten()
27 .collect() 32 .collect()
28} 33}
@@ -31,6 +36,7 @@ fn get_inlay_hints(
31 db: &RootDatabase, 36 db: &RootDatabase,
32 file_id: FileId, 37 file_id: FileId,
33 node: &SyntaxNode, 38 node: &SyntaxNode,
39 max_inlay_hint_length: Option<usize>,
34) -> Option<Vec<InlayHint>> { 40) -> Option<Vec<InlayHint>> {
35 let analyzer = SourceAnalyzer::new(db, hir::Source::new(file_id.into(), node), None); 41 let analyzer = SourceAnalyzer::new(db, hir::Source::new(file_id.into(), node), None);
36 match_ast! { 42 match_ast! {
@@ -40,7 +46,7 @@ fn get_inlay_hints(
40 return None; 46 return None;
41 } 47 }
42 let pat = it.pat()?; 48 let pat = it.pat()?;
43 Some(get_pat_type_hints(db, &analyzer, pat, false)) 49 Some(get_pat_type_hints(db, &analyzer, pat, false, max_inlay_hint_length))
44 }, 50 },
45 ast::LambdaExpr(it) => { 51 ast::LambdaExpr(it) => {
46 it.param_list().map(|param_list| { 52 it.param_list().map(|param_list| {
@@ -48,22 +54,22 @@ fn get_inlay_hints(
48 .params() 54 .params()
49 .filter(|closure_param| closure_param.ascribed_type().is_none()) 55 .filter(|closure_param| closure_param.ascribed_type().is_none())
50 .filter_map(|closure_param| closure_param.pat()) 56 .filter_map(|closure_param| closure_param.pat())
51 .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, false)) 57 .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, false, max_inlay_hint_length))
52 .flatten() 58 .flatten()
53 .collect() 59 .collect()
54 }) 60 })
55 }, 61 },
56 ast::ForExpr(it) => { 62 ast::ForExpr(it) => {
57 let pat = it.pat()?; 63 let pat = it.pat()?;
58 Some(get_pat_type_hints(db, &analyzer, pat, false)) 64 Some(get_pat_type_hints(db, &analyzer, pat, false, max_inlay_hint_length))
59 }, 65 },
60 ast::IfExpr(it) => { 66 ast::IfExpr(it) => {
61 let pat = it.condition()?.pat()?; 67 let pat = it.condition()?.pat()?;
62 Some(get_pat_type_hints(db, &analyzer, pat, true)) 68 Some(get_pat_type_hints(db, &analyzer, pat, true, max_inlay_hint_length))
63 }, 69 },
64 ast::WhileExpr(it) => { 70 ast::WhileExpr(it) => {
65 let pat = it.condition()?.pat()?; 71 let pat = it.condition()?.pat()?;
66 Some(get_pat_type_hints(db, &analyzer, pat, true)) 72 Some(get_pat_type_hints(db, &analyzer, pat, true, max_inlay_hint_length))
67 }, 73 },
68 ast::MatchArmList(it) => { 74 ast::MatchArmList(it) => {
69 Some( 75 Some(
@@ -71,7 +77,7 @@ fn get_inlay_hints(
71 .arms() 77 .arms()
72 .map(|match_arm| match_arm.pats()) 78 .map(|match_arm| match_arm.pats())
73 .flatten() 79 .flatten()
74 .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, true)) 80 .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, true, max_inlay_hint_length))
75 .flatten() 81 .flatten()
76 .collect(), 82 .collect(),
77 ) 83 )
@@ -86,6 +92,7 @@ fn get_pat_type_hints(
86 analyzer: &SourceAnalyzer, 92 analyzer: &SourceAnalyzer,
87 root_pat: ast::Pat, 93 root_pat: ast::Pat,
88 skip_root_pat_hint: bool, 94 skip_root_pat_hint: bool,
95 max_inlay_hint_length: Option<usize>,
89) -> Vec<InlayHint> { 96) -> Vec<InlayHint> {
90 let original_pat = &root_pat.clone(); 97 let original_pat = &root_pat.clone();
91 98
@@ -99,7 +106,7 @@ fn get_pat_type_hints(
99 .map(|(range, pat_type)| InlayHint { 106 .map(|(range, pat_type)| InlayHint {
100 range, 107 range,
101 kind: InlayKind::TypeHint, 108 kind: InlayKind::TypeHint,
102 label: pat_type.display(db).to_string().into(), 109 label: pat_type.display_truncated(db, max_inlay_hint_length).to_string().into(),
103 }) 110 })
104 .collect() 111 .collect()
105} 112}
@@ -209,7 +216,7 @@ fn main() {
209}"#, 216}"#,
210 ); 217 );
211 218
212 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 219 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
213 [ 220 [
214 InlayHint { 221 InlayHint {
215 range: [193; 197), 222 range: [193; 197),
@@ -278,7 +285,7 @@ fn main() {
278}"#, 285}"#,
279 ); 286 );
280 287
281 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 288 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
282 [ 289 [
283 InlayHint { 290 InlayHint {
284 range: [21; 30), 291 range: [21; 30),
@@ -307,7 +314,7 @@ fn main() {
307}"#, 314}"#,
308 ); 315 );
309 316
310 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 317 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
311 [ 318 [
312 InlayHint { 319 InlayHint {
313 range: [21; 30), 320 range: [21; 30),
@@ -355,7 +362,7 @@ fn main() {
355}"#, 362}"#,
356 ); 363 );
357 364
358 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 365 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
359 [ 366 [
360 InlayHint { 367 InlayHint {
361 range: [166; 170), 368 range: [166; 170),
@@ -418,7 +425,7 @@ fn main() {
418}"#, 425}"#,
419 ); 426 );
420 427
421 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 428 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
422 [ 429 [
423 InlayHint { 430 InlayHint {
424 range: [166; 170), 431 range: [166; 170),
@@ -481,7 +488,7 @@ fn main() {
481}"#, 488}"#,
482 ); 489 );
483 490
484 assert_debug_snapshot!(analysis.inlay_hints(file_id).unwrap(), @r###" 491 assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###"
485 [ 492 [
486 InlayHint { 493 InlayHint {
487 range: [311; 315), 494 range: [311; 315),
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs
index 110ddcd62..fcb3da90e 100644
--- a/crates/ra_ide_api/src/lib.rs
+++ b/crates/ra_ide_api/src/lib.rs
@@ -338,8 +338,14 @@ impl Analysis {
338 } 338 }
339 339
340 /// Returns a list of the places in the file where type hints can be displayed. 340 /// Returns a list of the places in the file where type hints can be displayed.
341 pub fn inlay_hints(&self, file_id: FileId) -> Cancelable<Vec<InlayHint>> { 341 pub fn inlay_hints(
342 self.with_db(|db| inlay_hints::inlay_hints(db, file_id, &db.parse(file_id).tree())) 342 &self,
343 file_id: FileId,
344 max_inlay_hint_length: Option<usize>,
345 ) -> Cancelable<Vec<InlayHint>> {
346 self.with_db(|db| {
347 inlay_hints::inlay_hints(db, file_id, &db.parse(file_id).tree(), max_inlay_hint_length)
348 })
343 } 349 }
344 350
345 /// Returns the set of folding ranges. 351 /// Returns the set of folding ranges.
diff --git a/crates/ra_lsp_server/src/config.rs b/crates/ra_lsp_server/src/config.rs
index 9871a3b37..8045f3d60 100644
--- a/crates/ra_lsp_server/src/config.rs
+++ b/crates/ra_lsp_server/src/config.rs
@@ -29,6 +29,8 @@ pub struct ServerConfig {
29 29
30 pub lru_capacity: Option<usize>, 30 pub lru_capacity: Option<usize>,
31 31
32 pub max_inlay_hint_length: Option<usize>,
33
32 /// For internal usage to make integrated tests faster. 34 /// For internal usage to make integrated tests faster.
33 #[serde(deserialize_with = "nullable_bool_true")] 35 #[serde(deserialize_with = "nullable_bool_true")]
34 pub with_sysroot: bool, 36 pub with_sysroot: bool,
@@ -44,6 +46,7 @@ impl Default for ServerConfig {
44 exclude_globs: Vec::new(), 46 exclude_globs: Vec::new(),
45 use_client_watching: false, 47 use_client_watching: false,
46 lru_capacity: None, 48 lru_capacity: None,
49 max_inlay_hint_length: None,
47 with_sysroot: true, 50 with_sysroot: true,
48 feature_flags: FxHashMap::default(), 51 feature_flags: FxHashMap::default(),
49 } 52 }
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs
index 379dab438..3e2ac3683 100644
--- a/crates/ra_lsp_server/src/main_loop.rs
+++ b/crates/ra_lsp_server/src/main_loop.rs
@@ -123,6 +123,7 @@ pub fn main_loop(
123 .and_then(|it| it.folding_range.as_ref()) 123 .and_then(|it| it.folding_range.as_ref())
124 .and_then(|it| it.line_folding_only) 124 .and_then(|it| it.line_folding_only)
125 .unwrap_or(false), 125 .unwrap_or(false),
126 max_inlay_hint_length: config.max_inlay_hint_length,
126 } 127 }
127 }; 128 };
128 129
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs
index 20f9aee13..7347a78c7 100644
--- a/crates/ra_lsp_server/src/main_loop/handlers.rs
+++ b/crates/ra_lsp_server/src/main_loop/handlers.rs
@@ -870,7 +870,7 @@ pub fn handle_inlay_hints(
870 let analysis = world.analysis(); 870 let analysis = world.analysis();
871 let line_index = analysis.file_line_index(file_id)?; 871 let line_index = analysis.file_line_index(file_id)?;
872 Ok(analysis 872 Ok(analysis
873 .inlay_hints(file_id)? 873 .inlay_hints(file_id, world.options.max_inlay_hint_length)?
874 .into_iter() 874 .into_iter()
875 .map(|api_type| InlayHint { 875 .map(|api_type| InlayHint {
876 label: api_type.label.to_string(), 876 label: api_type.label.to_string(),
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs
index 51824e7a3..9bdea70c7 100644
--- a/crates/ra_lsp_server/src/world.rs
+++ b/crates/ra_lsp_server/src/world.rs
@@ -28,6 +28,7 @@ pub struct Options {
28 pub publish_decorations: bool, 28 pub publish_decorations: bool,
29 pub supports_location_link: bool, 29 pub supports_location_link: bool,
30 pub line_folding_only: bool, 30 pub line_folding_only: bool,
31 pub max_inlay_hint_length: Option<usize>,
31} 32}
32 33
33/// `WorldState` is the primary mutable state of the language server 34/// `WorldState` is the primary mutable state of the language server
diff --git a/editors/code/src/commands/inlay_hints.ts b/editors/code/src/commands/inlay_hints.ts
index ffaaaebcb..0dbdd94fb 100644
--- a/editors/code/src/commands/inlay_hints.ts
+++ b/editors/code/src/commands/inlay_hints.ts
@@ -87,7 +87,7 @@ export class HintsUpdater {
87 range: hint.range, 87 range: hint.range,
88 renderOptions: { 88 renderOptions: {
89 after: { 89 after: {
90 contentText: `: ${this.truncateHint(hint.label)}` 90 contentText: `: ${hint.label}`
91 } 91 }
92 } 92 }
93 })); 93 }));
@@ -98,18 +98,6 @@ export class HintsUpdater {
98 } 98 }
99 } 99 }
100 100
101 private truncateHint(label: string): string {
102 if (!Server.config.maxInlayHintLength) {
103 return label;
104 }
105
106 let newLabel = label.substring(0, Server.config.maxInlayHintLength);
107 if (label.length > Server.config.maxInlayHintLength) {
108 newLabel += '…';
109 }
110 return newLabel;
111 }
112
113 private async queryHints(documentUri: string): Promise<InlayHint[] | null> { 101 private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
114 const request: InlayHintsParams = { 102 const request: InlayHintsParams = {
115 textDocument: { uri: documentUri } 103 textDocument: { uri: documentUri }
diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts
index a3ef21a16..7907b70bc 100644
--- a/editors/code/src/server.ts
+++ b/editors/code/src/server.ts
@@ -43,6 +43,7 @@ export class Server {
43 initializationOptions: { 43 initializationOptions: {
44 publishDecorations: true, 44 publishDecorations: true,
45 lruCapacity: Server.config.lruCapacity, 45 lruCapacity: Server.config.lruCapacity,
46 maxInlayHintLength: Server.config.maxInlayHintLength,
46 excludeGlobs: Server.config.excludeGlobs, 47 excludeGlobs: Server.config.excludeGlobs,
47 useClientWatching: Server.config.useClientWatching, 48 useClientWatching: Server.config.useClientWatching,
48 featureFlags: Server.config.featureFlags 49 featureFlags: Server.config.featureFlags