Detailed changes
@@ -1,14 +1,13 @@
+use crate::api_key::ApiKeyState;
use crate::ui::InstructionListItem;
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState};
use anthropic::{
AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent, ToolResultContent,
ToolResultPart, Usage,
};
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use collections::{BTreeMap, HashMap};
use editor::{Editor, EditorElement, EditorStyle};
-use futures::Stream;
-use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
+use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, FontStyle, Task, TextStyle, WhiteSpace};
use http_client::HttpClient;
use language_model::{
@@ -157,7 +156,7 @@ impl AnthropicLanguageModelProvider {
}
fn settings(cx: &App) -> &AnthropicSettings {
- &AllLanguageModelSettings::get_global(cx).anthropic
+ &crate::AllLanguageModelSettings::get_global(cx).anthropic
}
fn api_url(cx: &App) -> SharedString {
@@ -220,11 +219,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
}
// Override with available models from settings
- for model in AllLanguageModelSettings::get_global(cx)
- .anthropic
- .available_models
- .iter()
- {
+ for model in &AnthropicLanguageModelProvider::settings(cx).available_models {
models.insert(
model.name.clone(),
anthropic::Model::Custom {
@@ -363,16 +358,11 @@ impl AnthropicModel {
> {
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = AnthropicLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err.into())).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
let beta_headers = self.model.beta_headers();
@@ -938,6 +928,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -2,7 +2,7 @@ use anyhow::{Result, anyhow};
use collections::{BTreeMap, HashMap};
use editor::{Editor, EditorElement, EditorStyle};
use futures::Stream;
-use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
+use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, SharedString, Task, TextStyle, WhiteSpace,
Window,
@@ -26,7 +26,7 @@ use ui::{Icon, IconName, List, prelude::*};
use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState, ui::InstructionListItem};
+use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
@@ -119,7 +119,7 @@ impl DeepSeekLanguageModelProvider {
}
fn settings(cx: &App) -> &DeepSeekSettings {
- &AllLanguageModelSettings::get_global(cx).deepseek
+ &crate::AllLanguageModelSettings::get_global(cx).deepseek
}
fn api_url(cx: &App) -> &str {
@@ -220,16 +220,11 @@ impl DeepSeekLanguageModel {
) -> BoxFuture<'static, Result<BoxStream<'static, Result<deepseek::StreamResponse>>>> {
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(api_url);
- (api_key, api_url.to_string())
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(api_url), api_url.to_string())
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
@@ -1,8 +1,8 @@
-use anyhow::{Context as _, Result};
+use anyhow::{Context as _, Result, anyhow};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
-use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
+use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
use google_ai::{
FunctionDeclaration, GenerateContentResponse, GoogleModelMode, Part, SystemInstruction,
ThinkingConfig, UsageMetadata,
@@ -37,8 +37,8 @@ use util::ResultExt;
use zed_env_vars::EnvVar;
use crate::api_key::ApiKey;
+use crate::api_key::ApiKeyState;
use crate::ui::InstructionListItem;
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState};
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
@@ -173,8 +173,12 @@ impl GoogleLanguageModelProvider {
})
}
+ fn settings(cx: &App) -> &GoogleSettings {
+ &crate::AllLanguageModelSettings::get_global(cx).google
+ }
+
fn api_url(cx: &App) -> SharedString {
- let api_url = &AllLanguageModelSettings::get_global(cx).google.api_url;
+ let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
google_ai::API_URL.into()
} else {
@@ -223,10 +227,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
}
// Override with available models from settings
- for model in &AllLanguageModelSettings::get_global(cx)
- .google
- .available_models
- {
+ for model in &GoogleLanguageModelProvider::settings(cx).available_models {
models.insert(
model.name.clone(),
google_ai::Model::Custom {
@@ -295,16 +296,11 @@ impl GoogleLanguageModel {
> {
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = GoogleLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
async move {
@@ -820,6 +816,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -1,7 +1,7 @@
use anyhow::{Result, anyhow};
use collections::BTreeMap;
use editor::{Editor, EditorElement, EditorStyle};
-use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream};
+use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, SharedString, Task, TextStyle, WhiteSpace,
Window,
@@ -244,16 +244,11 @@ impl MistralLanguageModel {
> {
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = MistralLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
@@ -762,6 +757,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -1,7 +1,7 @@
use anyhow::{Result, anyhow};
use collections::{BTreeMap, HashMap};
use futures::Stream;
-use futures::{FutureExt, StreamExt, future::BoxFuture};
+use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
@@ -25,7 +25,7 @@ use ui_input::SingleLineInput;
use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState, ui::InstructionListItem};
+use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::OPEN_AI_PROVIDER_NAME;
@@ -113,7 +113,7 @@ impl OpenAiLanguageModelProvider {
}
fn settings(cx: &App) -> &OpenAiSettings {
- &AllLanguageModelSettings::get_global(cx).openai
+ &crate::AllLanguageModelSettings::get_global(cx).openai
}
fn api_url(cx: &App) -> SharedString {
@@ -166,10 +166,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
}
// Override with available models from settings
- for model in &AllLanguageModelSettings::get_global(cx)
- .openai
- .available_models
- {
+ for model in &OpenAiLanguageModelProvider::settings(cx).available_models {
models.insert(
model.name.clone(),
open_ai::Model::Custom {
@@ -230,16 +227,11 @@ impl OpenAiLanguageModel {
{
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = OpenAiLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
@@ -744,6 +736,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -754,11 +750,8 @@ impl ConfigurationView {
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.api_key_editor.update(cx, |input, cx| {
- input.editor.update(cx, |editor, cx| {
- editor.set_text("", window, cx);
- });
- });
+ self.api_key_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
@@ -1,6 +1,6 @@
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use convert_case::{Case, Casing};
-use futures::{FutureExt, StreamExt, future::BoxFuture};
+use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
@@ -20,8 +20,8 @@ use ui_input::SingleLineInput;
use util::ResultExt;
use zed_env_vars::EnvVar;
+use crate::api_key::ApiKeyState;
use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai};
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState};
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenAiCompatibleSettings {
@@ -98,7 +98,7 @@ impl State {
impl OpenAiCompatibleLanguageModelProvider {
pub fn new(id: Arc<str>, http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
fn resolve_settings<'a>(id: &'a str, cx: &'a App) -> Option<&'a OpenAiCompatibleSettings> {
- AllLanguageModelSettings::get_global(cx)
+ crate::AllLanguageModelSettings::get_global(cx)
.openai_compatible
.get(id)
}
@@ -239,16 +239,14 @@ impl OpenAiCompatibleLanguageModel {
{
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, _cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, _cx| {
let api_url = &state.settings.api_url;
- let api_key = state.api_key_state.key(api_url);
- (api_key, state.settings.api_url.clone())
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (
+ state.api_key_state.key(api_url),
+ state.settings.api_url.clone(),
+ )
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let provider = self.provider_name.clone();
@@ -423,6 +421,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -433,11 +435,8 @@ impl ConfigurationView {
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.api_key_editor.update(cx, |input, cx| {
- input.editor.update(cx, |editor, cx| {
- editor.set_text("", window, cx);
- });
- });
+ self.api_key_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
@@ -1,7 +1,7 @@
use anyhow::{Result, anyhow};
use collections::HashMap;
use editor::{Editor, EditorElement, EditorStyle};
-use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
+use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, SharedString, Task, TextStyle, WhiteSpace,
};
@@ -27,7 +27,7 @@ use ui::{Icon, IconName, List, Tooltip, prelude::*};
use util::ResultExt;
use zed_env_vars::{EnvVar, env_var};
-use crate::{AllLanguageModelSettings, api_key::ApiKeyState, ui::InstructionListItem};
+use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
@@ -96,7 +96,6 @@ pub struct State {
http_client: Arc<dyn HttpClient>,
available_models: Vec<open_router::Model>,
fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
- settings: OpenRouterSettings,
}
impl State {
@@ -171,14 +170,17 @@ impl State {
impl OpenRouterLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
let state = cx.new(|cx| {
- cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
- let current_settings = &AllLanguageModelSettings::get_global(cx).open_router;
- let settings_changed = current_settings != &this.settings;
- if settings_changed {
- this.settings = current_settings.clone();
- this.authenticate(cx).detach();
+ cx.observe_global::<SettingsStore>({
+ let mut last_settings = OpenRouterLanguageModelProvider::settings(cx).clone();
+ move |this: &mut State, cx| {
+ let current_settings = OpenRouterLanguageModelProvider::settings(cx);
+ let settings_changed = current_settings != &last_settings;
+ if settings_changed {
+ last_settings = current_settings.clone();
+ this.authenticate(cx).detach();
+ cx.notify();
+ }
}
- cx.notify();
})
.detach();
State {
@@ -186,7 +188,6 @@ impl OpenRouterLanguageModelProvider {
http_client: http_client.clone(),
available_models: Vec::new(),
fetch_models_task: None,
- settings: OpenRouterSettings::default(),
}
});
@@ -194,7 +195,7 @@ impl OpenRouterLanguageModelProvider {
}
fn settings(cx: &App) -> &OpenRouterSettings {
- &AllLanguageModelSettings::get_global(cx).open_router
+ &crate::AllLanguageModelSettings::get_global(cx).open_router
}
fn api_url(cx: &App) -> SharedString {
@@ -322,17 +323,11 @@ impl OpenRouterLanguageModel {
>,
> {
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = OpenRouterLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(LanguageModelCompletionError::Other(err)))
- .boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
async move {
@@ -795,6 +790,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -1,6 +1,6 @@
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use collections::BTreeMap;
-use futures::{FutureExt, StreamExt, future::BoxFuture};
+use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
@@ -219,16 +219,11 @@ impl VercelLanguageModel {
{
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = VercelLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
@@ -429,6 +424,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -439,11 +438,8 @@ impl ConfigurationView {
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.api_key_editor.update(cx, |input, cx| {
- input.editor.update(cx, |editor, cx| {
- editor.set_text("", window, cx);
- });
- });
+ self.api_key_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
@@ -1,6 +1,6 @@
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use collections::BTreeMap;
-use futures::{FutureExt, StreamExt, future::BoxFuture};
+use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
use http_client::HttpClient;
use language_model::{
@@ -219,16 +219,11 @@ impl XAiLanguageModel {
{
let http_client = self.http_client.clone();
- let api_key_and_url = self.state.read_with(cx, |state, cx| {
+ let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
let api_url = XAiLanguageModelProvider::api_url(cx);
- let api_key = state.api_key_state.key(&api_url);
- (api_key, api_url)
- });
- let (api_key, api_url) = match api_key_and_url {
- Ok(api_key_and_url) => api_key_and_url,
- Err(err) => {
- return futures::future::ready(Err(err)).boxed();
- }
+ (state.api_key_state.key(&api_url), api_url)
+ }) else {
+ return future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
@@ -423,6 +418,10 @@ impl ConfigurationView {
return;
}
+ // url changes can cause the editor to be displayed again
+ self.api_key_editor
+ .update(cx, |editor, cx| editor.set_text("", window, cx));
+
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state
@@ -433,11 +432,8 @@ impl ConfigurationView {
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.api_key_editor.update(cx, |input, cx| {
- input.editor.update(cx, |editor, cx| {
- editor.set_text("", window, cx);
- });
- });
+ self.api_key_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
@@ -9,6 +9,7 @@ use component::{example_group, single_example};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, TextStyle};
use settings::Settings;
+use std::sync::Arc;
use theme::ThemeSettings;
use ui::prelude::*;
@@ -101,6 +102,11 @@ impl SingleLineInput {
pub fn text(&self, cx: &App) -> String {
self.editor().read(cx).text(cx)
}
+
+ pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
+ self.editor()
+ .update(cx, |editor, cx| editor.set_text(text, window, cx))
+ }
}
impl Render for SingleLineInput {