inspector: Add to support auto complete. (#1272)
<img width="1273" height="1138" alt="image" src="https://github.com/user-attachments/assets/52a474c3-b936-468e-b56d-e3f0a4e3104d" />
This commit is contained in:
parent
f93e9a9475
commit
62e3f8126a
4 changed files with 253 additions and 120 deletions
|
|
@ -259,7 +259,8 @@ impl DiagnosticSet {
|
|||
self.clear();
|
||||
}
|
||||
|
||||
pub fn push(&mut self, diagnostic: Diagnostic) {
|
||||
pub fn push(&mut self, diagnostic: impl Into<Diagnostic>) {
|
||||
let diagnostic = diagnostic.into();
|
||||
let start = self.text.position_to_offset(&diagnostic.range.start);
|
||||
let end = self.text.position_to_offset(&diagnostic.range.end);
|
||||
|
||||
|
|
@ -272,12 +273,13 @@ impl DiagnosticSet {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn extend<I>(&mut self, diagnostics: I)
|
||||
pub fn extend<D, I>(&mut self, diagnostics: D)
|
||||
where
|
||||
I: IntoIterator<Item = Diagnostic>,
|
||||
D: IntoIterator<Item = I>,
|
||||
I: Into<Diagnostic>,
|
||||
{
|
||||
for diagnostic in diagnostics {
|
||||
self.push(diagnostic);
|
||||
self.push(diagnostic.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use gpui::{
|
|||
use lsp_types::{CompletionItem, CompletionTextEdit};
|
||||
|
||||
const MAX_MENU_WIDTH: Pixels = px(320.);
|
||||
const MAX_MENU_HEIGHT: Pixels = px(480.);
|
||||
const MAX_MENU_HEIGHT: Pixels = px(240.);
|
||||
const POPOVER_GAP: Pixels = px(4.);
|
||||
|
||||
use crate::{
|
||||
|
|
@ -171,7 +171,7 @@ impl ListDelegate for ContextMenuDelegate {
|
|||
/// A context menu for code completions and code actions.
|
||||
pub struct CompletionMenu {
|
||||
offset: usize,
|
||||
state: Entity<InputState>,
|
||||
editor: Entity<InputState>,
|
||||
list: Entity<List<ContextMenuDelegate>>,
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
|
@ -187,7 +187,7 @@ impl CompletionMenu {
|
|||
///
|
||||
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
||||
pub(crate) fn new(
|
||||
state: Entity<InputState>,
|
||||
editor: Entity<InputState>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
|
|
@ -221,7 +221,7 @@ impl CompletionMenu {
|
|||
|
||||
Self {
|
||||
offset: 0,
|
||||
state,
|
||||
editor,
|
||||
list,
|
||||
open: false,
|
||||
trigger_start_offset: None,
|
||||
|
|
@ -237,24 +237,24 @@ impl CompletionMenu {
|
|||
let item = item.clone();
|
||||
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
|
||||
|
||||
let state = self.state.clone();
|
||||
let editor = self.editor.clone();
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
state.update_in(cx, |state, window, cx| {
|
||||
state.completion_inserting = true;
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.completion_inserting = true;
|
||||
|
||||
let mut new_text = item.label.clone();
|
||||
if let Some(text_edit) = item.text_edit.as_ref() {
|
||||
match text_edit {
|
||||
CompletionTextEdit::Edit(edit) => {
|
||||
new_text = edit.new_text.clone();
|
||||
range.start = state.text.position_to_offset(&edit.range.start);
|
||||
range.end = state.text.position_to_offset(&edit.range.end);
|
||||
range.start = editor.text.position_to_offset(&edit.range.start);
|
||||
range.end = editor.text.position_to_offset(&edit.range.end);
|
||||
}
|
||||
CompletionTextEdit::InsertAndReplace(edit) => {
|
||||
new_text = edit.new_text.clone();
|
||||
range.start = state.text.position_to_offset(&edit.replace.start);
|
||||
range.end = state.text.position_to_offset(&edit.replace.end);
|
||||
range.start = editor.text.position_to_offset(&edit.replace.start);
|
||||
range.end = editor.text.position_to_offset(&edit.replace.end);
|
||||
}
|
||||
}
|
||||
} else if let Some(insert_text) = item.insert_text.clone() {
|
||||
|
|
@ -262,15 +262,15 @@ impl CompletionMenu {
|
|||
range = offset..offset;
|
||||
}
|
||||
|
||||
state.replace_text_in_range(
|
||||
Some(state.range_to_utf16(&range)),
|
||||
editor.replace_text_in_range(
|
||||
Some(editor.range_to_utf16(&range)),
|
||||
&new_text,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
state.completion_inserting = false;
|
||||
editor.completion_inserting = false;
|
||||
// FIXME: Input not get the focus
|
||||
state.focus(window, cx);
|
||||
editor.focus(window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
|
@ -374,18 +374,18 @@ impl CompletionMenu {
|
|||
}
|
||||
|
||||
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||
let state = self.state.read(cx);
|
||||
let Some(last_layout) = state.last_layout.as_ref() else {
|
||||
let editor = self.editor.read(cx);
|
||||
let Some(last_layout) = editor.last_layout.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
||||
let scroll_origin = self.editor.read(cx).scroll_handle.offset();
|
||||
|
||||
Some(
|
||||
scroll_origin + cursor_origin - state.input_bounds.origin
|
||||
scroll_origin + cursor_origin - editor.input_bounds.origin
|
||||
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||
)
|
||||
}
|
||||
|
|
@ -416,8 +416,10 @@ impl Render for CompletionMenu {
|
|||
.and_then(|item| item.documentation.clone());
|
||||
|
||||
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
||||
let vertical_layout = pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
|
||||
> window.bounds().size.width;
|
||||
let abs_pos = self.editor.read(cx).input_bounds.origin + pos;
|
||||
let vertical_layout =
|
||||
abs_pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
|
||||
> window.bounds().size.width;
|
||||
|
||||
deferred(
|
||||
div()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub trait RopeExt {
|
|||
|
||||
/// Return the lines iterator.
|
||||
///
|
||||
/// Each line is including the `\n` at the end, but not `\n`.
|
||||
/// Each line is including the `\r` at the end, but not `\n`.
|
||||
fn lines(&self) -> RopeLines;
|
||||
|
||||
/// Check is equal to another rope.
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
use std::{cell::OnceCell, collections::HashMap, fmt::Write as _, sync::OnceLock};
|
||||
use std::{cell::OnceCell, collections::HashMap, fmt::Write as _, rc::Rc, sync::OnceLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
actions, div, inspector_reflection::FunctionReflection, prelude::FluentBuilder, px, AnyElement,
|
||||
App, AppContext, Context, DivInspectorState, Entity, Inspector, InspectorElementId,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, ParentElement as _, Refineable as _, Render,
|
||||
SharedString, StyleRefinement, Styled, Subscription, Window,
|
||||
SharedString, StyleRefinement, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit, Diagnostic,
|
||||
DiagnosticSeverity, Position, TextEdit,
|
||||
};
|
||||
use rope::Rope;
|
||||
|
||||
use crate::{
|
||||
alert::Alert,
|
||||
button::{Button, ButtonVariants},
|
||||
clipboard::Clipboard,
|
||||
description_list::DescriptionList,
|
||||
dropdown::{Dropdown, DropdownState, SearchableVec},
|
||||
h_flex,
|
||||
input::{InputEvent, InputState, TabSize, TextInput},
|
||||
input::{CompletionProvider, InputEvent, InputState, RopeExt, TabSize, TextInput},
|
||||
link::Link,
|
||||
v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
|
||||
};
|
||||
|
|
@ -66,7 +71,6 @@ struct EditorState {
|
|||
pub struct DivInspector {
|
||||
inspector_id: Option<InspectorElementId>,
|
||||
inspector_state: Option<DivInspectorState>,
|
||||
rust_dropdown: Entity<DropdownState<SearchableVec<SharedString>>>,
|
||||
rust_state: EditorState,
|
||||
json_state: EditorState,
|
||||
/// Initial style before any edits
|
||||
|
|
@ -78,6 +82,8 @@ pub struct DivInspector {
|
|||
|
||||
impl DivInspector {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let lsp_provider = Rc::new(LspProvider {});
|
||||
|
||||
let json_input_state = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.code_editor("json")
|
||||
|
|
@ -85,30 +91,16 @@ impl DivInspector {
|
|||
});
|
||||
|
||||
let rust_input_state = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
let mut editor = InputState::new(window, cx)
|
||||
.code_editor("rust")
|
||||
.line_number(false)
|
||||
.tab_size(TabSize {
|
||||
tab_size: 4,
|
||||
hard_tabs: false,
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let rust_dropdown = cx.new(|cx| {
|
||||
DropdownState::new(
|
||||
SearchableVec::new({
|
||||
let mut methods: Vec<_> = StyleMethods::get()
|
||||
.table
|
||||
.iter()
|
||||
.map(|(_, method)| method.name.into())
|
||||
.collect();
|
||||
methods.sort();
|
||||
methods
|
||||
}),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
editor.lsp.completion_provider = Some(lsp_provider.clone());
|
||||
editor
|
||||
});
|
||||
|
||||
let _subscriptions = vec![
|
||||
|
|
@ -151,7 +143,6 @@ impl DivInspector {
|
|||
Self {
|
||||
inspector_id: None,
|
||||
inspector_state: None,
|
||||
rust_dropdown,
|
||||
rust_state,
|
||||
json_state,
|
||||
initial_style: Default::default(),
|
||||
|
|
@ -212,8 +203,14 @@ impl DivInspector {
|
|||
return;
|
||||
}
|
||||
|
||||
let (new_style, err) = rust_to_style(self.unconvertible_style.clone(), code);
|
||||
self.rust_state.error = err;
|
||||
let (new_style, diagnostics) = rust_to_style(self.unconvertible_style.clone(), code);
|
||||
self.rust_state.state.update(cx, |state, cx| {
|
||||
if let Some(set) = state.diagnostics_mut() {
|
||||
set.clear();
|
||||
set.extend(diagnostics);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
self.json_state.error = None;
|
||||
self.json_state.editing = false;
|
||||
self.update_json_from_style(&new_style, window, cx);
|
||||
|
|
@ -272,24 +269,6 @@ impl DivInspector {
|
|||
rust_style
|
||||
})
|
||||
}
|
||||
|
||||
fn rust_add_style(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(method) = self.rust_dropdown.read(cx).selected_value() {
|
||||
let code = self.rust_state.state.read(cx).value();
|
||||
let new_code = format!(" .{method}()\n");
|
||||
let Some(insert_pos) = code.rfind('}') else {
|
||||
self.rust_state.error = Some("Failed to add method: Could not find `}`".into());
|
||||
return;
|
||||
};
|
||||
let code = format!("{}{}{}", &code[..insert_pos], new_code, &code[insert_pos..]);
|
||||
|
||||
self.rust_state.editing = true;
|
||||
self.rust_state.state.update(cx, |state, cx| {
|
||||
state.set_value(code, window, cx);
|
||||
// an edit event will be triggered, the style will be updated there
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style_to_json(style: &StyleRefinement) -> String {
|
||||
|
|
@ -348,40 +327,77 @@ fn style_to_rust(input_style: &StyleRefinement) -> (String, StyleRefinement) {
|
|||
(code, style)
|
||||
}
|
||||
|
||||
fn rust_to_style(
|
||||
mut style: StyleRefinement,
|
||||
rust_code: &str,
|
||||
) -> (StyleRefinement, Option<SharedString>) {
|
||||
// remove line comments
|
||||
let rust_code = rust_code
|
||||
.lines()
|
||||
.map(|line| line.find("//").map_or(line, |i| &line[..i]).trim())
|
||||
.collect::<Vec<_>>()
|
||||
.concat();
|
||||
fn rust_to_style(mut style: StyleRefinement, source: &str) -> (StyleRefinement, Vec<Diagnostic>) {
|
||||
let rope = Rope::from(source);
|
||||
let Some(begin) = source.find("div()").map(|i| i + "div()".len()) else {
|
||||
let start_pos = Position::new(0, 0);
|
||||
let end_pos = rope.offset_to_position(rope.len());
|
||||
|
||||
let Some(begin) = rust_code.find("div()").map(|i| i + "div()".len()) else {
|
||||
return (style, Some("Expected `div()`".into()));
|
||||
return (
|
||||
style,
|
||||
vec![Diagnostic {
|
||||
range: lsp_types::Range::new(start_pos, end_pos),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message: "expected `div()`".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
let mut err = String::new();
|
||||
let methods = rust_code[begin..]
|
||||
.split(&['.', '(', ')', '{', '}'])
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let mut methods = vec![];
|
||||
let mut offset = 0;
|
||||
let mut method_offset = 0;
|
||||
let mut method = String::new();
|
||||
for line in rope.lines() {
|
||||
if line.to_string().trim().starts_with("//") {
|
||||
offset += line.len() + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
for c in line.chars() {
|
||||
offset += c.len_utf8();
|
||||
if offset < begin {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c.is_ascii_alphanumeric() || c == '_' {
|
||||
method.push(c);
|
||||
method_offset = offset;
|
||||
} else {
|
||||
if !method.is_empty() {
|
||||
methods.push((method_offset, method.clone()));
|
||||
}
|
||||
method.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// +1 \n
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
let mut diagnostics = vec![];
|
||||
let style_methods = StyleMethods::get();
|
||||
for method in methods {
|
||||
match style_methods.map.get(method) {
|
||||
|
||||
for (offset, method) in methods {
|
||||
match style_methods.map.get(method.as_str()) {
|
||||
Some(method_reflection) => style = method_reflection.invoke(style),
|
||||
None => _ = writeln!(err, "Unknown method: {method}"),
|
||||
None => {
|
||||
let message = format!("unknown method `{}`", method);
|
||||
let start = rope.offset_to_position(offset.saturating_sub(method.len()));
|
||||
let end = rope.offset_to_position(offset);
|
||||
let diagnostic = lsp_types::Diagnostic {
|
||||
range: lsp_types::Range::new(start, end),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let err = if err.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(err.trim_end().to_string().into())
|
||||
};
|
||||
(style, err)
|
||||
(style, diagnostics)
|
||||
}
|
||||
|
||||
impl Render for DivInspector {
|
||||
|
|
@ -401,31 +417,18 @@ impl Render for DivInspector {
|
|||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.h_2_5()
|
||||
.gap_y_3()
|
||||
.child(
|
||||
v_flex().gap_y_2().child("Rust Styles").child(
|
||||
h_flex()
|
||||
.gap_x_2()
|
||||
.child(
|
||||
Dropdown::new(&self.rust_dropdown)
|
||||
.icon(IconName::Search)
|
||||
.small()
|
||||
.cleanable()
|
||||
.flex_1(),
|
||||
)
|
||||
.child(Button::new("rust-add").label("Add").small().on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.rust_add_style(window, cx);
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
Button::new("rust-reset").label("Reset").small().on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.reset_style(window, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_x_2()
|
||||
.child("Rust Styles")
|
||||
.child(Button::new("rust-reset").label("Reset").small().on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.reset_style(window, cx);
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
|
@ -441,8 +444,9 @@ impl Render for DivInspector {
|
|||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_y_3()
|
||||
.h_3_5()
|
||||
.h_2_5()
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
@ -554,3 +558,128 @@ fn render_inspector(
|
|||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
struct LspProvider {}
|
||||
|
||||
impl CompletionProvider for LspProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
rope: &rope::Rope,
|
||||
offset: usize,
|
||||
_: lsp_types::CompletionContext,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> Task<Result<CompletionResponse>> {
|
||||
let mut left_offset = 0;
|
||||
while left_offset < 100 {
|
||||
match rope.char_at(offset.saturating_sub(left_offset)) {
|
||||
Some('.') => {
|
||||
break;
|
||||
}
|
||||
None => break,
|
||||
_ => {}
|
||||
}
|
||||
left_offset += 1;
|
||||
}
|
||||
let start = offset.saturating_sub(left_offset);
|
||||
let trigger_character = rope.slice(start..offset).to_string();
|
||||
if !trigger_character.starts_with('.') {
|
||||
return Task::ready(Ok(CompletionResponse::Array(vec![])));
|
||||
}
|
||||
|
||||
let start_pos = rope.offset_to_position(start);
|
||||
let end_pos = rope.offset_to_position(offset);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let styles = StyleMethods::get()
|
||||
.map
|
||||
.iter()
|
||||
.filter_map(|(name, method)| {
|
||||
let prefix = &trigger_character[1..];
|
||||
if name.starts_with(&prefix) {
|
||||
Some(CompletionItem {
|
||||
label: name.to_string(),
|
||||
filter_text: Some(prefix.to_string()),
|
||||
kind: Some(CompletionItemKind::METHOD),
|
||||
detail: Some("()".to_string()),
|
||||
documentation: method
|
||||
.documentation
|
||||
.as_ref()
|
||||
.map(|doc| lsp_types::Documentation::String(doc.to_string())),
|
||||
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
|
||||
range: lsp_types::Range {
|
||||
start: start_pos,
|
||||
end: end_pos,
|
||||
},
|
||||
new_text: format!(".{}()", name),
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(CompletionResponse::Array(styles))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_completion_trigger(&self, _: usize, _: &str, _: &mut Context<InputState>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{rems, AbsoluteLength, DefiniteLength, Length};
|
||||
use indoc::indoc;
|
||||
use lsp_types::Position;
|
||||
|
||||
#[test]
|
||||
fn test_rust_to_style() {
|
||||
let (style, diagnostics) = super::rust_to_style(
|
||||
Default::default(),
|
||||
indoc! {r#"
|
||||
fn build() -> Div {
|
||||
div()
|
||||
.p_1()
|
||||
// This is a comment
|
||||
.mx_2()
|
||||
}
|
||||
"#},
|
||||
);
|
||||
assert_eq!(diagnostics, vec![]);
|
||||
assert_eq!(
|
||||
style.padding.left,
|
||||
Some(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(0.25))))
|
||||
);
|
||||
assert_eq!(
|
||||
style.margin.left,
|
||||
Some(Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Rems(rems(0.5))
|
||||
)))
|
||||
);
|
||||
|
||||
let (_, diagnostics) = super::rust_to_style(
|
||||
Default::default(),
|
||||
indoc! {r#"
|
||||
fn build() -> Div {
|
||||
div()
|
||||
.p_1()
|
||||
// This is a comment
|
||||
.unknown_method
|
||||
.bad_method()
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
assert_eq!(diagnostics.len(), 2);
|
||||
assert_eq!(diagnostics[0].message, "unknown method `unknown_method`");
|
||||
assert_eq!(diagnostics[0].range.start, Position::new(4, 9));
|
||||
assert_eq!(diagnostics[0].range.end, Position::new(4, 23));
|
||||
assert_eq!(diagnostics[1].message, "unknown method `bad_method`");
|
||||
assert_eq!(diagnostics[1].range.start, Position::new(5, 9));
|
||||
assert_eq!(diagnostics[1].range.end, Position::new(5, 19));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue