Skip to content

Commit

Permalink
feat(vscode): more hover information, prioritizing required fields, a…
Browse files Browse the repository at this point in the history
…nd respective access modifiers (#4538)

### Operation: Information Overload

Added more information to most hover cases. This includes additional types, function args, class constructor args.

Most language servers are pretty conservative with the information they provide. Not sure if it's a performance thing or it's just extremely difficult (impossible?) to present this information in a pretty way. This PR is attempting to show more information much more often, even though it may be a bit much.

Maybe this is bad? Not sure, I think it's worth the experiment

### Sorting

Fixes #3724

- Properties are sorted by their optionality and then alphabetically when applicable (e.g. struct construction)

### Filters

Fixes #4460

- (hover) class, interfaces, and structs now only show public members on hover
- (hover) init and inflight init are hidden
- (completion) class completions respect access modifier: hides private members unless accessing `this`
  - `super` does not work yet #4537

### Misc

- The debug symbol to print the symbol environment now goes to stderr. Previously it would break the language server (which listens to stdout)
- Per the above, hangar now attaches stderr (if applicable) to the snapshots

## Current Samples

### Struct
<img width="797" alt="image" src="https://github.com/winglang/wing/assets/1237390/1e6a594a-10e2-4522-9d77-4d8c6838dbcb">

### Class
<img width="1208" alt="image" src="https://github.com/winglang/wing/assets/1237390/9d4989ed-2c06-412a-b9db-7fc28c9514b4">

### Method
<img width="757" alt="image" src="https://github.com/winglang/wing/assets/1237390/55b67db0-fea5-44e8-82fe-4cdf7c3164ae">


*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
MarkMcCulloh authored Oct 14, 2023
1 parent 16fd099 commit 6d28ff0
Show file tree
Hide file tree
Showing 58 changed files with 732 additions and 507 deletions.
7 changes: 0 additions & 7 deletions examples/tests/sdk_tests/fs/yaml.main.w
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ let data = Json {
"arr": [1, 2, 3, "test", { "foo": "bar" }]
};

try {
fs.writeFile(filepath, "invalid: {{ content }}, invalid");
fs.readYaml(filepath);
} catch e {
assert(regex.match("^bad indentation", e) == true);
}

fs.writeYaml(filepath, data, data);
assert(fs.exists(filepath) == true);

Expand Down
214 changes: 136 additions & 78 deletions libs/wingc/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ use std::collections::BTreeMap;
use itertools::Itertools;

use crate::{
ast::Phase,
ast::{AccessModifier, Phase, Symbol},
closure_transform::CLOSURE_CLASS_PREFIX,
jsify::codemaker::CodeMaker,
type_check::{
jsii_importer::is_construct_base, Class, ClassLike, Enum, FunctionSignature, Interface, Namespace, Struct,
SymbolKind, Type, TypeRef, VariableInfo, VariableKind,
SymbolKind, Type, TypeRef, VariableInfo, VariableKind, CLASS_INFLIGHT_INIT_NAME, CLASS_INIT_NAME,
},
};

const FIELD_HEADER: &'static str = "### Fields";
const METHOD_HEADER: &'static str = "### Methods";
const PARAMETER_HEADER: &'static str = "### Parameters";

#[derive(Debug, Default, Clone)]
pub struct Docs {
pub summary: Option<String>,
Expand Down Expand Up @@ -120,31 +124,29 @@ impl Documented for VariableInfo {
markdown.line("```wing");
markdown.line(format!("{name_str}: {}", self.type_));

if let Some(d) = &self.docs {
markdown.line("```");

markdown.line("---");
let type_docs = self.type_.render_docs();

if let Some(summary) = &d.summary {
markdown.line(summary);
markdown.empty_line();
if !type_docs.is_empty() {
if type_docs.starts_with("```wing") {
// skip the first line to combine the code block
markdown.line(type_docs.lines().skip(1).join("\n"));
} else {
markdown.line("```");
markdown.line("---");
markdown.line(type_docs);
}

render_docs(&mut markdown, d);
} else {
let type_docs = self.type_.render_docs();

if !type_docs.is_empty() {
if type_docs.starts_with("```wing") {
// skip the first line to combine the code block
markdown.line(type_docs.lines().skip(1).join("\n"));
} else {
markdown.line("```");
markdown.line("---");
markdown.line(type_docs);
markdown.line("```");

if let Some(d) = &self.docs {
markdown.line("---");

if let Some(summary) = &d.summary {
markdown.line(summary);
markdown.empty_line();
}
} else {
markdown.line("```");

render_docs(&mut markdown, d);
}
}

Expand Down Expand Up @@ -207,37 +209,55 @@ fn render_docs(markdown: &mut CodeMaker, docs: &Docs) {
}
}

fn render_signature_help(f: &FunctionSignature) -> String {
let mut markdown = CodeMaker::default();

for (param_idx, param) in f.parameters.iter().enumerate() {
let param_type = param.typeref;
let param_type_unwrapped = param.typeref.maybe_unwrap_option();
let is_last = param_idx == f.parameters.len() - 1;

let param_name: &String = &param.name;
let detail_text = if let Some(summary) = &param.docs.summary {
format!("— `{param_type}` — {summary}")
} else {
format!("— `{param_type}`")
};

if !is_last || !param_type_unwrapped.is_struct() {
markdown.line(format!("- `{param_name}` {detail_text}"));
} else {
markdown.line(format!("- `...{param_name}` {detail_text}"));

let structy = param_type_unwrapped.as_struct().unwrap();
let struct_text = render_classlike_members(structy);

markdown.indent();
markdown.line(struct_text.replace(FIELD_HEADER, ""));
markdown.unindent();
}
}

markdown.to_string().trim().to_string()
}

fn render_function(f: &FunctionSignature) -> String {
let mut markdown = CodeMaker::default();

if let Some(s) = &f.docs.summary {
markdown.line(s);
}

if has_parameters_documentation(f) {
markdown.empty_line();
markdown.line("### Parameters");

for p in &f.parameters {
let summary = if let Some(s) = &p.docs.summary {
format!(" — {}", s)
} else {
String::default()
};

markdown.line(format!("- `{}`{}", p.name, summary));
}
if f.parameters.len() > 0 {
markdown.line(PARAMETER_HEADER);
markdown.line(render_signature_help(f));
}

render_docs(&mut markdown, &f.docs);

markdown.to_string().trim().to_string()
}

fn has_parameters_documentation(f: &FunctionSignature) -> bool {
f.parameters.iter().any(|p| p.docs.summary.is_some())
}

fn render_struct(s: &Struct) -> String {
let mut markdown = CodeMaker::default();

Expand All @@ -257,25 +277,7 @@ fn render_struct(s: &Struct) -> String {
markdown.line(s);
}

if s.fields(true).count() > 0 {
markdown.line("### Fields");
}

for field in s.env.iter(true) {
let Some(variable) = field.1.as_variable() else {
continue;
};
let optional = if variable.type_.is_option() { "?" } else { "" };
markdown.line(&format!(
"- `{}{optional}` — {}\n",
field.0,
variable
.docs
.as_ref()
.and_then(|d| d.summary.clone())
.unwrap_or(format!("{}", variable.type_))
));
}
markdown.line(render_classlike_members(s));

render_docs(&mut markdown, &s.docs);

Expand Down Expand Up @@ -307,21 +309,7 @@ fn render_interface(i: &Interface) -> String {
markdown.line(s);
}

if i.env.iter(true).next().is_some() {
markdown.line("### Methods");
}

for prop in i.env.iter(true) {
let prop_docs = prop
.1
.as_variable()
.and_then(|v| v.docs.as_ref().and_then(|d| d.summary.clone()));
markdown.line(&format!(
"- `{}` — {}\n",
prop.0,
prop_docs.unwrap_or(format!("`{}`", prop.1.as_variable().unwrap().type_))
));
}
markdown.line(render_classlike_members(i));

markdown.empty_line();

Expand Down Expand Up @@ -393,10 +381,80 @@ fn render_class(c: &Class) -> String {
}
render_docs(&mut markdown, &c.docs);

// if let Some(initializer) = c.get_init() {
// let rfn = initializer.render_docs();
// markdown.line(rfn);
// }
if matches!(c.phase, Phase::Preflight | Phase::Independent) {
if let Some(initializer) = c.get_method(&Symbol::global(CLASS_INIT_NAME)) {
let function_sig = initializer.type_.as_function_sig().unwrap();
if function_sig.parameters.len() > 0 {
markdown.line("### Initializer");
markdown.line(render_signature_help(&function_sig));
}
}
}

if let Some(initializer) = c.get_method(&Symbol::global(CLASS_INFLIGHT_INIT_NAME)) {
let function_sig = initializer.type_.as_function_sig().unwrap();
if function_sig.parameters.len() > 0 {
markdown.line("### Initializer (`inflight`)");
markdown.line(render_signature_help(&function_sig));
}
}

markdown.line(render_classlike_members(c));

markdown.to_string().trim().to_string()
}

fn render_classlike_members(classlike: &impl ClassLike) -> String {
let mut field_markdown = CodeMaker::default();
let mut method_markdown = CodeMaker::default();

let public_member_variables = classlike
.get_env()
.iter(true)
.flat_map(|f| if f.2.init { None } else { f.1.as_variable() })
.filter(|v| v.access_modifier == AccessModifier::Public)
// show optionals first, then sort alphabetically by name
.sorted_by(|a, b| {
let a_is_option = a.type_.is_option();
let b_is_option = b.type_.is_option();
a_is_option
.cmp(&b_is_option)
.then_with(|| a.name.name.cmp(&b.name.name))
})
.collect_vec();

for member in public_member_variables {
let member_type = member.type_;
let option_text = if member_type.is_option() { "?" } else { "" };
let member_name = &member.name.name;
let member_docs = member.docs.as_ref().and_then(|d| d.summary.clone());
let text = if let Some(member_docs) = member_docs {
format!("- `{member_name}{option_text}` — `{member_type}` — {member_docs}")
} else {
format!("- `{member_name}{option_text}` — `{member_type}`")
};

if member_type.maybe_unwrap_option().is_function_sig() {
if member.name.name == CLASS_INIT_NAME || member.name.name == CLASS_INFLIGHT_INIT_NAME {
continue;
}

method_markdown.line(text);
} else {
field_markdown.line(text);
}
}

let mut markdown = CodeMaker::default();

if !field_markdown.is_empty() {
markdown.line(FIELD_HEADER);
markdown.add_code(field_markdown);
}
if !method_markdown.is_empty() {
markdown.line(METHOD_HEADER);
markdown.add_code(method_markdown);
}

markdown.to_string().trim().to_string()
}
Expand Down
5 changes: 5 additions & 0 deletions libs/wingc/src/jsify/codemaker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ impl CodeMaker {
code.line(s);
code
}

/// Checks if there are no lines of code
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
}

impl ToString for CodeMaker {
Expand Down
Loading

0 comments on commit 6d28ff0

Please sign in to comment.