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();
|
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 start = self.text.position_to_offset(&diagnostic.range.start);
|
||||||
let end = self.text.position_to_offset(&diagnostic.range.end);
|
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
|
where
|
||||||
I: IntoIterator<Item = Diagnostic>,
|
D: IntoIterator<Item = I>,
|
||||||
|
I: Into<Diagnostic>,
|
||||||
{
|
{
|
||||||
for diagnostic in diagnostics {
|
for diagnostic in diagnostics {
|
||||||
self.push(diagnostic);
|
self.push(diagnostic.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use gpui::{
|
||||||
use lsp_types::{CompletionItem, CompletionTextEdit};
|
use lsp_types::{CompletionItem, CompletionTextEdit};
|
||||||
|
|
||||||
const MAX_MENU_WIDTH: Pixels = px(320.);
|
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.);
|
const POPOVER_GAP: Pixels = px(4.);
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -171,7 +171,7 @@ impl ListDelegate for ContextMenuDelegate {
|
||||||
/// A context menu for code completions and code actions.
|
/// A context menu for code completions and code actions.
|
||||||
pub struct CompletionMenu {
|
pub struct CompletionMenu {
|
||||||
offset: usize,
|
offset: usize,
|
||||||
state: Entity<InputState>,
|
editor: Entity<InputState>,
|
||||||
list: Entity<List<ContextMenuDelegate>>,
|
list: Entity<List<ContextMenuDelegate>>,
|
||||||
open: bool,
|
open: bool,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
|
|
@ -187,7 +187,7 @@ impl CompletionMenu {
|
||||||
///
|
///
|
||||||
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
/// NOTE: This element should not call from InputState::new, unless that will stack overflow.
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
state: Entity<InputState>,
|
editor: Entity<InputState>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
|
|
@ -221,7 +221,7 @@ impl CompletionMenu {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
state,
|
editor,
|
||||||
list,
|
list,
|
||||||
open: false,
|
open: false,
|
||||||
trigger_start_offset: None,
|
trigger_start_offset: None,
|
||||||
|
|
@ -237,24 +237,24 @@ impl CompletionMenu {
|
||||||
let item = item.clone();
|
let item = item.clone();
|
||||||
let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset;
|
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| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
state.update_in(cx, |state, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
state.completion_inserting = true;
|
editor.completion_inserting = true;
|
||||||
|
|
||||||
let mut new_text = item.label.clone();
|
let mut new_text = item.label.clone();
|
||||||
if let Some(text_edit) = item.text_edit.as_ref() {
|
if let Some(text_edit) = item.text_edit.as_ref() {
|
||||||
match text_edit {
|
match text_edit {
|
||||||
CompletionTextEdit::Edit(edit) => {
|
CompletionTextEdit::Edit(edit) => {
|
||||||
new_text = edit.new_text.clone();
|
new_text = edit.new_text.clone();
|
||||||
range.start = state.text.position_to_offset(&edit.range.start);
|
range.start = editor.text.position_to_offset(&edit.range.start);
|
||||||
range.end = state.text.position_to_offset(&edit.range.end);
|
range.end = editor.text.position_to_offset(&edit.range.end);
|
||||||
}
|
}
|
||||||
CompletionTextEdit::InsertAndReplace(edit) => {
|
CompletionTextEdit::InsertAndReplace(edit) => {
|
||||||
new_text = edit.new_text.clone();
|
new_text = edit.new_text.clone();
|
||||||
range.start = state.text.position_to_offset(&edit.replace.start);
|
range.start = editor.text.position_to_offset(&edit.replace.start);
|
||||||
range.end = state.text.position_to_offset(&edit.replace.end);
|
range.end = editor.text.position_to_offset(&edit.replace.end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(insert_text) = item.insert_text.clone() {
|
} else if let Some(insert_text) = item.insert_text.clone() {
|
||||||
|
|
@ -262,15 +262,15 @@ impl CompletionMenu {
|
||||||
range = offset..offset;
|
range = offset..offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.replace_text_in_range(
|
editor.replace_text_in_range(
|
||||||
Some(state.range_to_utf16(&range)),
|
Some(editor.range_to_utf16(&range)),
|
||||||
&new_text,
|
&new_text,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
state.completion_inserting = false;
|
editor.completion_inserting = false;
|
||||||
// FIXME: Input not get the focus
|
// FIXME: Input not get the focus
|
||||||
state.focus(window, cx);
|
editor.focus(window, cx);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
@ -374,18 +374,18 @@ impl CompletionMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
|
||||||
let state = self.state.read(cx);
|
let editor = self.editor.read(cx);
|
||||||
let Some(last_layout) = state.last_layout.as_ref() else {
|
let Some(last_layout) = editor.last_layout.as_ref() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
let scroll_origin = self.editor.read(cx).scroll_handle.offset();
|
||||||
|
|
||||||
Some(
|
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.)),
|
+ Point::new(-px(4.), last_layout.line_height + px(4.)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +416,9 @@ impl Render for CompletionMenu {
|
||||||
.and_then(|item| item.documentation.clone());
|
.and_then(|item| item.documentation.clone());
|
||||||
|
|
||||||
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
|
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
|
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;
|
> window.bounds().size.width;
|
||||||
|
|
||||||
deferred(
|
deferred(
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub trait RopeExt {
|
||||||
|
|
||||||
/// Return the lines iterator.
|
/// 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;
|
fn lines(&self) -> RopeLines;
|
||||||
|
|
||||||
/// Check is equal to another rope.
|
/// 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::{
|
use gpui::{
|
||||||
actions, div, inspector_reflection::FunctionReflection, prelude::FluentBuilder, px, AnyElement,
|
actions, div, inspector_reflection::FunctionReflection, prelude::FluentBuilder, px, AnyElement,
|
||||||
App, AppContext, Context, DivInspectorState, Entity, Inspector, InspectorElementId,
|
App, AppContext, Context, DivInspectorState, Entity, Inspector, InspectorElementId,
|
||||||
InteractiveElement as _, IntoElement, KeyBinding, ParentElement as _, Refineable as _, Render,
|
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::{
|
use crate::{
|
||||||
alert::Alert,
|
alert::Alert,
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
clipboard::Clipboard,
|
clipboard::Clipboard,
|
||||||
description_list::DescriptionList,
|
description_list::DescriptionList,
|
||||||
dropdown::{Dropdown, DropdownState, SearchableVec},
|
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{InputEvent, InputState, TabSize, TextInput},
|
input::{CompletionProvider, InputEvent, InputState, RopeExt, TabSize, TextInput},
|
||||||
link::Link,
|
link::Link,
|
||||||
v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
|
v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +71,6 @@ struct EditorState {
|
||||||
pub struct DivInspector {
|
pub struct DivInspector {
|
||||||
inspector_id: Option<InspectorElementId>,
|
inspector_id: Option<InspectorElementId>,
|
||||||
inspector_state: Option<DivInspectorState>,
|
inspector_state: Option<DivInspectorState>,
|
||||||
rust_dropdown: Entity<DropdownState<SearchableVec<SharedString>>>,
|
|
||||||
rust_state: EditorState,
|
rust_state: EditorState,
|
||||||
json_state: EditorState,
|
json_state: EditorState,
|
||||||
/// Initial style before any edits
|
/// Initial style before any edits
|
||||||
|
|
@ -78,6 +82,8 @@ pub struct DivInspector {
|
||||||
|
|
||||||
impl DivInspector {
|
impl DivInspector {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let lsp_provider = Rc::new(LspProvider {});
|
||||||
|
|
||||||
let json_input_state = cx.new(|cx| {
|
let json_input_state = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.code_editor("json")
|
.code_editor("json")
|
||||||
|
|
@ -85,30 +91,16 @@ impl DivInspector {
|
||||||
});
|
});
|
||||||
|
|
||||||
let rust_input_state = cx.new(|cx| {
|
let rust_input_state = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
let mut editor = InputState::new(window, cx)
|
||||||
.code_editor("rust")
|
.code_editor("rust")
|
||||||
.line_number(false)
|
.line_number(false)
|
||||||
.tab_size(TabSize {
|
.tab_size(TabSize {
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
hard_tabs: false,
|
hard_tabs: false,
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let rust_dropdown = cx.new(|cx| {
|
editor.lsp.completion_provider = Some(lsp_provider.clone());
|
||||||
DropdownState::new(
|
editor
|
||||||
SearchableVec::new({
|
|
||||||
let mut methods: Vec<_> = StyleMethods::get()
|
|
||||||
.table
|
|
||||||
.iter()
|
|
||||||
.map(|(_, method)| method.name.into())
|
|
||||||
.collect();
|
|
||||||
methods.sort();
|
|
||||||
methods
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
|
|
@ -151,7 +143,6 @@ impl DivInspector {
|
||||||
Self {
|
Self {
|
||||||
inspector_id: None,
|
inspector_id: None,
|
||||||
inspector_state: None,
|
inspector_state: None,
|
||||||
rust_dropdown,
|
|
||||||
rust_state,
|
rust_state,
|
||||||
json_state,
|
json_state,
|
||||||
initial_style: Default::default(),
|
initial_style: Default::default(),
|
||||||
|
|
@ -212,8 +203,14 @@ impl DivInspector {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (new_style, err) = rust_to_style(self.unconvertible_style.clone(), code);
|
let (new_style, diagnostics) = rust_to_style(self.unconvertible_style.clone(), code);
|
||||||
self.rust_state.error = err;
|
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.error = None;
|
||||||
self.json_state.editing = false;
|
self.json_state.editing = false;
|
||||||
self.update_json_from_style(&new_style, window, cx);
|
self.update_json_from_style(&new_style, window, cx);
|
||||||
|
|
@ -272,24 +269,6 @@ impl DivInspector {
|
||||||
rust_style
|
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 {
|
fn style_to_json(style: &StyleRefinement) -> String {
|
||||||
|
|
@ -348,40 +327,77 @@ fn style_to_rust(input_style: &StyleRefinement) -> (String, StyleRefinement) {
|
||||||
(code, style)
|
(code, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rust_to_style(
|
fn rust_to_style(mut style: StyleRefinement, source: &str) -> (StyleRefinement, Vec<Diagnostic>) {
|
||||||
mut style: StyleRefinement,
|
let rope = Rope::from(source);
|
||||||
rust_code: &str,
|
let Some(begin) = source.find("div()").map(|i| i + "div()".len()) else {
|
||||||
) -> (StyleRefinement, Option<SharedString>) {
|
let start_pos = Position::new(0, 0);
|
||||||
// remove line comments
|
let end_pos = rope.offset_to_position(rope.len());
|
||||||
let rust_code = rust_code
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.find("//").map_or(line, |i| &line[..i]).trim())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.concat();
|
|
||||||
|
|
||||||
let Some(begin) = rust_code.find("div()").map(|i| i + "div()".len()) else {
|
return (
|
||||||
return (style, Some("Expected `div()`".into()));
|
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 mut methods = vec![];
|
||||||
let methods = rust_code[begin..]
|
let mut offset = 0;
|
||||||
.split(&['.', '(', ')', '{', '}'])
|
let mut method_offset = 0;
|
||||||
.map(str::trim)
|
let mut method = String::new();
|
||||||
.filter(|s| !s.is_empty());
|
for line in rope.lines() {
|
||||||
let style_methods = StyleMethods::get();
|
if line.to_string().trim().starts_with("//") {
|
||||||
for method in methods {
|
offset += line.len() + 1;
|
||||||
match style_methods.map.get(method) {
|
continue;
|
||||||
Some(method_reflection) => style = method_reflection.invoke(style),
|
|
||||||
None => _ = writeln!(err, "Unknown method: {method}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let err = if err.is_empty() {
|
for c in line.chars() {
|
||||||
None
|
offset += c.len_utf8();
|
||||||
|
if offset < begin {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.is_ascii_alphanumeric() || c == '_' {
|
||||||
|
method.push(c);
|
||||||
|
method_offset = offset;
|
||||||
} else {
|
} else {
|
||||||
Some(err.trim_end().to_string().into())
|
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 (offset, method) in methods {
|
||||||
|
match style_methods.map.get(method.as_str()) {
|
||||||
|
Some(method_reflection) => style = method_reflection.invoke(style),
|
||||||
|
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()
|
||||||
};
|
};
|
||||||
(style, err)
|
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(style, diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for DivInspector {
|
impl Render for DivInspector {
|
||||||
|
|
@ -401,31 +417,18 @@ impl Render for DivInspector {
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
.h_2_5()
|
||||||
.gap_y_3()
|
.gap_y_3()
|
||||||
.child(
|
.child(
|
||||||
v_flex().gap_y_2().child("Rust Styles").child(
|
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
.gap_x_2()
|
.gap_x_2()
|
||||||
.child(
|
.child("Rust Styles")
|
||||||
Dropdown::new(&self.rust_dropdown)
|
.child(Button::new("rust-reset").label("Reset").small().on_click(
|
||||||
.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| {
|
cx.listener(|this, _, window, cx| {
|
||||||
this.reset_style(window, cx);
|
this.reset_style(window, cx);
|
||||||
}),
|
}),
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|
@ -441,8 +444,9 @@ impl Render for DivInspector {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
.gap_y_3()
|
.gap_y_3()
|
||||||
.h_3_5()
|
.h_2_5()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -554,3 +558,128 @@ fn render_inspector(
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.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