just-lsp 0.1.2

A language server for just
use super::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttributeTarget {
  Alias,
  Module,
  Recipe,
}

impl Display for AttributeTarget {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(
      f,
      "{}",
      match self {
        AttributeTarget::Alias => "alias",
        AttributeTarget::Module => "module",
        AttributeTarget::Recipe => "recipe",
      }
    )
  }
}

impl AttributeTarget {
  pub(crate) fn is_valid_for(&self, target: AttributeTarget) -> bool {
    *self == target
  }
}

#[derive(Debug)]
pub(crate) enum Builtin<'a> {
  Attribute {
    name: &'a str,
    description: &'a str,
    version: &'a str,
    targets: &'a [AttributeTarget],
    parameters: Option<&'a str>,
  },
  Constant {
    name: &'a str,
    description: &'a str,
    value: &'a str,
  },
  Function {
    name: &'a str,
    signature: &'a str,
    description: &'a str,
    required_args: usize,
    accepts_variadic: bool,
  },
  Setting {
    name: &'a str,
    kind: SettingKind,
    description: &'a str,
    default: &'a str,
  },
}

impl Builtin<'_> {
  pub(crate) fn completion_item(&self) -> lsp::CompletionItem {
    match self {
      Self::Attribute { name, .. } => lsp::CompletionItem {
        label: name.to_string(),
        kind: Some(lsp::CompletionItemKind::CONSTANT),
        documentation: Some(lsp::Documentation::MarkupContent(
          self.documentation(),
        )),
        insert_text: Some(format!("[{}]", name)),
        insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
        sort_text: Some(format!("z{}", name)),
        ..Default::default()
      },
      Self::Constant { name, .. } => lsp::CompletionItem {
        label: name.to_string(),
        kind: Some(lsp::CompletionItemKind::CONSTANT),
        documentation: Some(lsp::Documentation::MarkupContent(
          self.documentation(),
        )),
        insert_text: Some(name.to_string()),
        insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
        sort_text: Some(format!("z{}", name)),
        ..Default::default()
      },
      Self::Function { name, .. } => {
        let snippet = match *name {
          "absolute_path" => format!("{}(${{1:path:string}})", name),
          "append" => {
            format!("{}(${{1:suffix:string}}, ${{2:s:string}})", name)
          }
          "arch"
          | "num_cpus"
          | "os"
          | "os_family"
          | "is_dependency"
          | "invocation_directory"
          | "invocation_directory_native"
          | "justfile"
          | "justfile_directory"
          | "source_file"
          | "source_directory"
          | "just_executable"
          | "just_pid"
          | "uuid"
          | "cache_directory"
          | "config_directory"
          | "config_local_directory"
          | "data_directory"
          | "data_local_directory"
          | "executable_directory"
          | "home_directory" => format!("{}()", name),
          "blake3" => format!("{}(${{1:string:string}})", name),
          "blake3_file" => format!("{}(${{1:path:string}})", name),
          "canonicalize" => format!("{}(${{1:path:string}})", name),
          "capitalize" => format!("{}(${{1:s:string}})", name),
          "choose" => {
            format!("{}(${{1:n:string}}, ${{2:alphabet:string}})", name)
          }
          "clean" => format!("{}(${{1:path:string}})", name),
          "datetime" => format!("{}(${{1:format:string}})", name),
          "datetime_utc" => format!("{}(${{1:format:string}})", name),
          "encode_uri_component" => format!("{}(${{1:s:string}})", name),
          "env" => {
            format!("{}(${{1:key:string}}${{2:, default:string}})", name)
          }
          "error" => format!("{}(${{1:message:string}})", name),
          "extension" => format!("{}(${{1:path:string}})", name),
          "file_name" => format!("{}(${{1:path:string}})", name),
          "file_stem" => format!("{}(${{1:path:string}})", name),
          "join" => format!(
            "{}(${{1:a:string}}, ${{2:b:string}}${{3:, more:string...}})",
            name
          ),
          "kebabcase" => format!("{}(${{1:s:string}})", name),
          "lowercase" => format!("{}(${{1:s:string}})", name),
          "lowercamelcase" => format!("{}(${{1:s:string}})", name),
          "parent_directory" => format!("{}(${{1:path:string}})", name),
          "path_exists" => format!("{}(${{1:path:string}})", name),
          "prepend" => {
            format!("{}(${{1:prefix:string}}, ${{2:s:string}})", name)
          }
          "quote" => format!("{}(${{1:s:string}})", name),
          "read" => format!("{}(${{1:path:string}})", name),
          "replace" => format!(
            "{}(${{1:s:string}}, ${{2:from:string}}, ${{3:to:string}})",
            name
          ),
          "replace_regex" => format!(
          "{}(${{1:s:string}}, ${{2:regex:string}}, ${{3:replacement:string}})",
          name
        ),
          "require" => format!("{}(${{1:name:string}})", name),
          "semver_matches" => {
            format!(
              "{}(${{1:version:string}}, ${{2:requirement:string}})",
              name
            )
          }
          "sha256" => format!("{}(${{1:string:string}})", name),
          "sha256_file" => format!("{}(${{1:path:string}})", name),
          "shell" => {
            format!("{}(${{1:command:string}}${{2:, args:string...}})", name)
          }
          "shoutykebabcase" => format!("{}(${{1:s:string}})", name),
          "shoutysnakecase" => format!("{}(${{1:s:string}})", name),
          "snakecase" => format!("{}(${{1:s:string}})", name),
          "style" => format!("{}(${{1:name:string}})", name),
          "titlecase" => format!("{}(${{1:s:string}})", name),
          "trim" => format!("{}(${{1:s:string}})", name),
          "trim_end" => format!("{}(${{1:s:string}})", name),
          "trim_end_match" => {
            format!("{}(${{1:s:string}}, ${{2:substring:string}})", name)
          }
          "trim_end_matches" => {
            format!("{}(${{1:s:string}}, ${{2:substring:string}})", name)
          }
          "trim_start" => format!("{}(${{1:s:string}})", name),
          "trim_start_match" => {
            format!("{}(${{1:s:string}}, ${{2:substring:string}})", name)
          }
          "trim_start_matches" => {
            format!("{}(${{1:s:string}}, ${{2:substring:string}})", name)
          }
          "uppercamelcase" => format!("{}(${{1:s:string}})", name),
          "uppercase" => format!("{}(${{1:s:string}})", name),
          "which" => format!("{}(${{1:name:string}})", name),
          "without_extension" => format!("{}(${{1:path:string}})", name),
          _ => format!("{}(${{1:}})", name),
        };

        lsp::CompletionItem {
          label: name.to_string(),
          kind: Some(lsp::CompletionItemKind::FUNCTION),
          documentation: Some(lsp::Documentation::MarkupContent(
            self.documentation(),
          )),
          insert_text: Some(snippet),
          insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
          sort_text: Some(format!("z{}", name)),
          ..Default::default()
        }
      }
      Self::Setting { name, .. } => lsp::CompletionItem {
        label: name.to_string(),
        kind: Some(lsp::CompletionItemKind::PROPERTY),
        documentation: Some(lsp::Documentation::MarkupContent(
          self.documentation(),
        )),
        insert_text: Some(format!("set {} := ", name)),
        insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
        sort_text: Some(format!("z{}", name)),
        ..Default::default()
      },
    }
  }

  pub(crate) fn documentation(&self) -> lsp::MarkupContent {
    match self {
      Self::Attribute {
        name,
        description,
        version,
        targets,
        parameters,
      } => {
        let mut documentation =
          format!("**Attribute**: [{}]\n{}", name, description);

        if let Some(params) = parameters {
          documentation
            .push_str(&format!("\n**Syntax**: [{}({})]", name, params));
        }

        documentation.push_str(&format!("\n**Introduced in**: {}", version));

        let targets = targets
          .iter()
          .map(|target| target.to_string())
          .collect::<Vec<String>>();

        documentation
          .push_str(&format!("\n**Target(s)**: {}", targets.join(", ")));

        lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value: documentation,
        }
      }
      Self::Constant {
        description, value, ..
      } => lsp::MarkupContent {
        kind: lsp::MarkupKind::Markdown,
        value: format!("{}\n{}", description, value),
      },
      Self::Function {
        name,
        signature,
        description,
        ..
      } => {
        let example = match *name {
          "absolute_path" => "absolute_path(\"./foo\") => \"/path/to/foo\"",
          "append" => "append(\"/src\", \"foo bar\") => \"foo/src bar/src\"",
          "arch" => "arch() => \"x86_64\"",
          "blake3" => "blake3(\"hello\") => \"a1744eeb6b921a9193df*...\"",
          "canonicalize" => "canonicalize(\"../foo/.\") => \"/path/to/foo\"",
          "capitalize" => "capitalize(\"hello\") => \"Hello\"",
          "choose" => "choose(\"5\", \"abcdef\") => \"bcafe\"",
          "clean" => "clean(\"foo//bar/../baz\") => \"foo/baz\"",
          "datetime" => "datetime(\"%Y-%m-%d\") => \"2023-07-14\"",
          "encode_uri_component" => "encode_uri_component(\"a+b\") => \"a%2Bb\"",
          "env" => "env(\"HOME\") => \"/home/user\"\nenv(\"MISSING\", \"default\") => \"default\"",
          "error" => "error(\"Something went wrong\") => *aborts execution*",
          "extension" => "extension(\"foo.txt\") => \"txt\"",
          "file_name" => "file_name(\"/path/to/foo.txt\") => \"foo.txt\"",
          "file_stem" => "file_stem(\"/path/to/foo.txt\") => \"foo\"",
          "home_directory" => "home_directory() => \"/home/user\"",
          "invocation_directory" => "invocation_directory() => \"/path/to/current/dir\"",
          "is_dependency" => "is_dependency() => \"false\"",
          "join" => "join(\"foo\", \"bar\", \"baz\") => \"foo/bar/baz\"",
          "just_executable" => "just_executable() => \"/usr/bin/just\"",
          "just_pid" => "just_pid() => \"12345\"",
          "justfile" => "justfile() => \"/path/to/justfile\"",
          "justfile_directory" => "justfile_directory() => \"/path/to\"",
          "kebabcase" => "kebabcase(\"HelloWorld\") => \"hello-world\"",
          "lowercase" => "lowercase(\"Hello\") => \"hello\"",
          "lowercamelcase" => "lowercamelcase(\"hello_world\") => \"helloWorld\"",
          "num_cpus" => "num_cpus() => 8",
          "os" => "os() => \"linux\"",
          "os_family" => "os_family() => \"unix\"",
          "parent_directory" => "parent_directory(\"/path/to/foo.txt\") => \"/path/to\"",
          "path_exists" => "path_exists(\"/etc/passwd\") => \"true\"",
          "prepend" => "prepend(\"src/\", \"foo bar\") => \"src/foo src/bar\"",
          "quote" => "quote(\"hello 'world'\") => \"'hello \\'world\\''\"",
          "read" => "read(\"foo.txt\") => \"contents of foo.txt\"",
          "replace" => "replace(\"hello\", \"l\", \"x\") => \"hexxo\"",
          "replace_regex" => "replace_regex(\"hello\", \"[aeiou]\", \"X\") => \"hXllX\"",
          "require" => "require(\"bash\") => \"/bin/bash\"",
          "semver_matches" => "semver_matches(\"1.2.3\", \">1.0.0\") => \"true\"",
          "sha256" => "sha256(\"hello\") => \"2cf24dba5fb0a30e*...\"",
          "hell" => "shell(\"echo $1\", \"hello\") => \"hello\"",
          "style" => "style(\"error\") => \"\\e[31m\"",
          "trim" => "trim(\"  hello  \") => \"hello\"",
          "trim_end" => "trim_end(\"hello  \") => \"hello\"",
          "trim_start" => "trim_start(\"  hello\") => \"hello\"",
          "uppercamelcase" => "uppercamelcase(\"hello_world\") => \"HelloWorld\"",
          "uppercase" => "uppercase(\"hello\") => \"HELLO\"",
          "uuid" => "uuid() => \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\"",
          "which" => "which(\"bash\") => \"/bin/bash\"\nwhich(\"nonexistent\") => \"\"",
          "without_extension" => "without_extension(\"/path/to/foo.txt\") => \"/path/to/foo\"",
          _ => "",
        };

        let mut documentation = String::new();

        documentation.push_str(description);

        documentation.push_str(format!("\n```\n{}\n```", signature).as_str());

        if !example.is_empty() {
          documentation.push_str("\n**Examples:**\n```\n");
          documentation.push_str(example);
          documentation.push_str("\n```");
        }

        lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value: documentation,
        }
      }
      Self::Setting {
        name,
        kind,
        description,
        default,
        ..
      } => {
        let mut documentation =
          format!("**Setting**: {}\n{}", name, description);

        documentation.push_str(&format!("\n**Type**: {:?}", kind));
        documentation.push_str(&format!("\n**Default**: {}", default));

        lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value: documentation,
        }
      }
    }
  }
}